# Copyright (C) 1996 Red Hat Software, Inc.
# Use of this software is subject to the terms of the GNU General
# Public License

# This module manages standard configuration file handling
# These classes are available:
# Conf:
#  This is the base class.  This is good for working with just about
#  any line-oriented configuration file.
#  Currently does not deal with newline escaping; may never...
# ConfShellVar(Conf):
#  This is a derived class which implements a dictionary for standard
#  VARIABLE=value
#  shell variable setting.
#  Limitations:
#    o one variable per line
#    o assumes everything on the line after the '=' is the value
# ConfESNetwork(ConfShellVar):
#  This is a derived class specifically intended for /etc/sysconfig/network
#  It is another dictionary, but magically fixes /etc/HOSTNAME when the
#  hostname is changed.
# ConfEHosts(Conf):
#  Yet another dictionary, this one for /etc/hosts
#  Dictionary keys are numeric IP addresses in string form, values are
#  2-item lists, the first item of which is the canonical hostname,
#  and the second of which is a list of nicknames.
# ConfEResolv(Conf):
#  Yet another dictionary, this one for /etc/resolv.conf
#  This ugly file has two different kinds of entries.  All but one
#  take the form "key list of arguments", but one entry (nameserver)
#  instead takes multiple lines of "key argument" pairs.
#  In this dictionary, all keys have the same name as the keys in
#  the file, EXCEPT that the multiple nameserver entries are all
#  stored under 'nameservers'.  Each value (even singleton values)
#  is a list.
# ConfESStaticRoutes(Conf):
#  Yet another dictionary, this one for /etc/sysconfig/static-routes
#  This file has a syntax similar to that of /etc/gateways;
#  the interface name is added and active/passive is deleted:
#  <interface> net <netaddr> netmask <netmask> gw <gateway>
#  The key is the interface, the value is a list of
#  [<netaddr>, <netmask>, <gateway>] lists
# ConfChat(Conf):
#  Not a dictionary!
#  This reads chat files, and writes a subset of chat files that
#  has all items enclosed in '' and has one expect/send pair on
#  each line.
#  Uses a list of two-element tuples.
# ConfDIP:
#  This reads chat files, and writes a dip file based on that chat script.
#  Takes three arguments:
#   o The chatfile
#   o The name of the dipfile
#   o The ConfSHellVar instance from which to take variables in the dipfile
# ConfModules(Conf)
#  This reads /etc/conf.modules into a dictionary keyed on device type,
#  holding dictionaries: cm['eth0']['alias'] --> 'smc-ultra'
#                        cm['eth0']['options'] --> {'io':'0x300', 'irq':'10'}
#                        cm['eth0']['post-install'] --> ['/bin/foo','arg1','arg2']
#  path[*] entries are ignored (but not removed)
#  New entries are added at the end to make sure that they
#  come after any path[*] entries.
#  Comments are delimited by initial '#'
# ConfModInfo(Conf)
#  This READ-ONLY class reads /boot/module-info.
#  The first line of /boot/module-info is "Version = <version>";
#  this class reads version 0 module-info files.
# ConfPw(Conf)
#  This class implements a dictionary based on a :-separated file.
#  It takes as arguments the filename and the field number to key on;
#  The data provided is a list including all fields including the key.
#  Has its own write method to keep files sane.
# ConfPasswd(ConfPw)
#  This class presents a data-oriented class for making changes
#  to the /etc/passwd file.
# ConfShadow(ConfPw)
#  This class presents a data-oriented class for making changes
#  to the /etc/shadow file.
# ConfGroup(ConfPw)
#  This class presents a data-oriented class for making changes
#  to the /etc/group file.
#  May be replaced by a pwdb-based module, we hope.
# ConfUnix()
#  This class presents a data-oriented class which uses the ConfPasswd
#  and ConfShadow classes (if /etc/shadow exists) to hold data.
#  Designed to be replaced by a pwdb module eventually, we hope.

from string import *
from regex import *
import regsub
import os

# Implementation:
# A configuration file is a list of lines.
# a line is a string.

class Conf:
    def __init__(self, filename, commenttype='#',
                 separators='\t ', separator='\t', merge=1):
        self.commenttype = commenttype
        self.separators = separators
        self.separator = separator
	self.merge = merge
        self.line = 0
        self.mode = -1
        # self.line is a "point" -- 0 is before the first line;
        # 1 is between the first and second lines, etc.
        # The "current" line is the line after the point.
        self.filename = filename
        self.read()
    def rewind(self):
        self.line = 0
    def fsf(self):
        self.line = len(self.lines)
    def tell(self):
        return self.line
    def seek(self, line):
        self.line = line
    def nextline(self):
        self.line = min([self.line + 1, len(self.lines)])
    def findnextline(self, regexp='.*'):
        # returns false if no more lines matching pattern
        while self.line < len(self.lines):
            if search(regexp, self.lines[self.line]) > -1:
                return 1
            self.line = self.line + 1
        # if while loop terminated, pattern not found.
        return 0
    def findnextcodeline(self):
        # optional whitespace followed by non-comment character
        # defines a codeline.  blank lines, lines with only whitespace,
        # and comment lines do not count.
        return self.findnextline('^[' + self.separators + ']*' +
                             '[^' + self.commenttype + self.separators + ']+')
    def findlinewithfield(self, fieldnum, value):
	if self.merge:
	    seps = '['+self.separators+']+'
	else:
	    seps = '['+self.separators+']'
	rx = '^'
	for i in range(fieldnum - 1):
	    rx = rx + '[^'+self.separators+']*' + seps
	# Need to use a ? so that an end of line is also accepted
	rx = rx + value + '['+self.separators+']?'
	return self.findnextline(rx)
    def getline(self):
        if self.line >= len(self.lines):
            return ''
        return self.lines[self.line]
    def getfields(self):
        # returns list of fields split by self.separators
        if self.line >= len(self.lines):
            return []
	if self.merge:
	    seps = '['+self.separators+']+'
	else:
	    seps = '['+self.separators+']'
        return regsub.split(self.lines[self.line], seps)
    def setfields(self, list):
	# replaces current line with line built from list
	# appends if off the end of the array
	if self.line < len(self.lines):
	    self.deleteline()
	self.insertlinelist(list)
    def insertline(self, line=''):
        self.lines.insert(self.line, line)
    def insertlinelist(self, linelist):
        self.insertline(joinfields(linelist, self.separator))
    def sedline(self, pat, repl):
        if self.line < len(self.lines):
            self.lines[self.line] = regsub.gsub(pat, repl, \
                                                self.lines[self.line])
    def changefield(self, fieldno, fieldtext):
        fields = self.getfields()
        fields[fieldno:fieldno+1] = [fieldtext]
        self.setfields(fields)
    def setline(self, line=[]):
        self.deleteline()
        self.insertline(line)
    def deleteline(self):
        self.lines[self.line:self.line+1] = []
    def chmod(self, mode=-1):
	self.mode = mode
    def read(self):
        if os.path.isfile(self.filename):
            self.file = open(self.filename, 'r', -1)
            self.lines = self.file.readlines()
            # strip newlines
            for index in range(len(self.lines)):
                self.lines[index] = \
                    self.lines[index][0:len(self.lines[index])-1]
            self.file.close()
	else:
	    self.lines = []
    def write(self):
        self.file = open(self.filename, 'w', -1)
	if self.mode >= 0:
	    os.chmod(self.filename, self.mode)
        # add newlines
        for index in range(len(self.lines)):
            self.file.write(self.lines[index] + '\n')
        self.file.close()

class ConfShellVar(Conf):
    def __init__(self, filename):
        Conf.__init__(self, filename, commenttype='#',
                      separators='=', separator='=')
    def read(self):
        Conf.read(self)
        self.initvars()
    def initvars(self):
        self.vars = {}
        self.rewind()
        while self.findnextline('^[\t ]*[A-Za-z_][A-Za-z0-9_]*='):
            # initialize dictionary of variable/name pairs
            var = self.getfields()
	    if len(var[1]) and var[1][0] in '\'"':
		# found quote; strip from beginning and end
		quote = var[1][0]
		var[1] = var[1][1:]
		p = -1
		while cmp(var[1][p], quote):
		    p = p - 1
		var[1] = var[1][:p]
            self.vars[var[0]] = var[1]
            self.nextline()
        self.rewind()
    def __getitem__(self, varname):
        if self.vars.has_key(varname):
            return self.vars[varname]
        else:
            return ''
    def __setitem__(self, varname, value):
        # set *every* instance of varname to value to avoid surprises
        place=self.tell()
        self.rewind()
        missing=1
        while self.findnextline('[\t ]*' + varname + '='):
            if search('[ \t${}*@!~<>?;%^()#]', value) > -1:
        	self.sedline('=.*', "='" + value + "'")
	    else:
        	self.sedline('=.*', '=' + value)
            missing=0
            self.nextline()
        if missing:
            self.seek(place)
            if search('[ \t${}*@!~<>?;%^()#]', value) > -1:
        	self.insertline(varname + "='" + value + "'")
	    else:
        	self.insertline(varname + '=' + value)
        self.vars[varname] = value
    def __delitem__(self, varname):
        # delete *every* instance...
        self.rewind()
        while self.findnextline('[\t ]*' + varname + '='):
            self.deleteline()
        del self.vars[varname]


class ConfESNetwork(ConfShellVar):
    # explicitly for /etc/sysconfig/network: HOSTNAME is magical value
    # that writes /etc/HOSTNAME as well
    def __init__ (self):
	ConfShellVar.__init__(self, '/etc/sysconfig/network')
	self.writehostname = 0
    def __setitem__(self, varname, value):
	ConfShellVar.__setitem__(self, varname, value)
	if varname == 'HOSTNAME':
	    self.writehostname = 1
    def write(self):
	ConfShellVar.write(self)
	if self.writehostname:
	    file = open('/etc/HOSTNAME', 'w', -1)
	    file.write(self.vars['HOSTNAME'] + '\n')
	    file.close()
	    os.chmod('/etc/HOSTNAME', 0644)
    def keys(self):
	# There doesn't appear to be a need to return keys in order
	# here because we normally always have the same entries in this
	# file, and order isn't particularly important.
	return self.vars.keys()
    def has_key(self, key):
	return self.vars.has_key(key)


class ConfEHosts(Conf):
    # for /etc/hosts
    # implements a dictionary keyed by IP address, with values
    # consisting of a list: [ hostname, [list, of, nicknames] ]
    def __init__(self):
        Conf.__init__(self, '/etc/hosts')
    def read(self):
        Conf.read(self)
        self.initvars()
    def initvars(self):
        self.vars = {}
        self.rewind()
        while self.findnextcodeline():
            # initialize dictionary of variable/name pairs
            var = self.getfields()
            if len(var) > 2:
		# has nicknames
                self.vars[var[0]] = [ var[1], var[2:] ]
	    else:
                self.vars[var[0]] = [ var[1], [] ]
            self.nextline()
        self.rewind()
    def __getitem__(self, varname):
        if self.vars.has_key(varname):
            return self.vars[varname]
        else:
            return ''
    def __setitem__(self, varname, value):
        # set first (should be only) instance to values in list value
        place=self.tell()
        self.rewind()
        if self.findnextline('^' + regsub.gsub('\.', '\\\\.', varname) +
                             '[' + self.separators + ']+'):
            self.deleteline()
	    self.insertlinelist([ varname, value[0],
                                  joinfields(value[1], self.separator) ])
            self.seek(place)
        else:
            self.seek(place)
	    self.insertlinelist([ varname, value[0],
                                  joinfields(value[1], self.separator) ])
        self.vars[varname] = value
    def __delitem__(self, varname):
        # delete *every* instance...
        self.rewind()
        while self.findnextline('[' + self.separators + ']*' +
                                regsub.gsub('\.', '\\\\.', varname) +
				'[' + self.separators + ']'):
            self.deleteline()
        del self.vars[varname]
    def keys(self):
	# It is rather important to return the keys in order here,
	# in order to maintain a consistent presentation in apps.
        place=self.tell()
        self.rewind()
        keys = []
        while self.findnextcodeline():
            # initialize dictionary of variable/name pairs
            var = self.getfields()
            keys.append(var[0])
            self.nextline()
	self.seek(place)
	return keys


class ConfEResolv(Conf):
    # /etc/resolv.conf
    def __init__(self):
	Conf.__init__(self, '/etc/resolv.conf', '#', '\t ', ' ')
    def read(self):
        Conf.read(self)
        self.initvars()
    def initvars(self):
        self.vars = {}
        self.rewind()
        while self.findnextcodeline():
            var = self.getfields()
	    if var[0] == 'nameserver':
		if self.vars.has_key('nameservers'):
		    self.vars['nameservers'].append(var[1])
		else:
		    self.vars['nameservers'] = [ var[1] ]
	    else:
		self.vars[var[0]] = var[1:]
            self.nextline()
        self.rewind()
    def __getitem__(self, varname):
        if self.vars.has_key(varname):
            return self.vars[varname]
        else:
            return []
    def __setitem__(self, varname, value):
        # set first (should be only) instance to values in list value
        place=self.tell()
        self.rewind()
	if varname == 'nameservers':
            if self.findnextline('^nameserver[' + self.separators + ']+'):
		# if there is a nameserver line, save the place,
		# remove all nameserver lines, then put in new ones in order
		placename=self.tell()
		while self.findnextline('^nameserver['+self.separators+']+'):
                    self.deleteline()
		self.seek(placename)
                for nameserver in value:
		    self.insertline('nameserver' + self.separator + nameserver)
		    self.nextline()
                self.seek(place)
            else:
		# no nameservers entries so far
                self.seek(place)
                for nameserver in value:
		    self.insertline('nameserver' + self.separator + nameserver)
	else:
	    # not a nameserver, so all items on one line...
            if self.findnextline('^' + varname + '[' + self.separators + ']+'):
                self.deleteline()
	        self.insertlinelist([ varname, 
                                      joinfields(value, self.separator) ])
                self.seek(place)
            else:
                self.seek(place)
	        self.insertlinelist([ varname,
                                      joinfields(value, self.separator) ])
	# no matter what, update our idea of the variable...
        self.vars[varname] = value
    def __delitem__(self, varname):
        # delete *every* instance...
        self.rewind()
        while self.findnextline('[' + self.separators + ']*' + varname +
				'[' + self.separators + ']'):
            self.deleteline()
        del self.vars[varname]
    def write(self):
	# Need to make sure __setitem__ is called for each item to
	# maintain consistancy, in case some did something like
	# resolv['nameservers'].append('123.123.123.123')
	# or
	# resolv['search'].append('another.domain')
	for key in self.vars.keys():
	    self[key] = self.vars[key]
	Conf.write(self)
    def keys(self):
	# no need to return list in order here, I think.
	return self.vars.keys()
    def has_key(self, key):
	return self.vars.has_key(key)


# ConfESStaticRoutes(Conf):
#  Yet another dictionary, this one for /etc/sysconfig/static-routes
#  This file has a syntax similar to that of /etc/gateways;
#  the interface name is added and active/passive is deleted:
#  <interface> net <netaddr> netmask <netmask> gw <gateway>
#  The key is the interface, the value is a list of
#  [<netaddr>, <netmask>, <gateway>] lists
class ConfESStaticRoutes(Conf):
    def __init__(self):
	Conf.__init__(self, '/etc/sysconfig/static-routes', '#', '\t ', ' ')
    def read(self):
        Conf.read(self)
        self.initvars()
    def initvars(self):
        self.vars = {}
        self.rewind()
        while self.findnextcodeline():
            var = self.getfields()
	    if not self.vars.has_key(var[0]) and len(var) == 7:
		self.vars[var[0]] = [[var[2], var[4], var[6]]]
	    else:
		self.vars[var[0]].append([var[2], var[4], var[6]])
            self.nextline()
        self.rewind()
    def __getitem__(self, varname):
        if self.vars.has_key(varname):
            return self.vars[varname]
        else:
            return [[]]
    def __setitem__(self, varname, value):
	# since we re-write the file completely on close, we don't
	# need to alter it piecemeal here.
	if len(value) == 3:
            self.vars[varname] = value
    def __delitem__(self, varname):
	# again, since we re-write the file completely on close, we don't
	# need to alter it piecemeal here.
        del self.vars[varname]
    def delroute(self, device, route):
	# deletes a route from a device if the route exists,
	# and if it is the only route for the device, removes
	# the device from the dictionary
	# Note: This could normally be optimized considerably,
	# except that our input may have come from the file,
	# which others may have hand-edited, and this makes it
	# possible for us to deal with hand-inserted multiple
	# identical routes in a reasonably correct way.
	if self.vars.has_key(device):
	    for i in range(len(self.vars[device])):
		if i < len(self.vars[device]) and \
		   not cmp(self.vars[device][i], route):
		    # need first comparison because list shrinks
		    self.vars[device][i:i+1] = []
		    if len(self.vars[device]) == 0:
			del self.vars[device]
    def addroute(self, device, route):
	# adds a route to a device, deleteing it first to avoid dups
	self.delroute(device, route)
	if self.vars.has_key(device):
	    self.vars[device].append(route)
	else:
	    self.vars[device] = [route]
    def write(self):
	# forget current version of file
	self.rewind()
	self.lines = []
	for device in self.vars.keys():
	    for route in self.vars[device]:
		if len(route) == 3:
		    self.insertlinelist((device, 'net', route[0],
                                         'netmask', route[1], 'gw', route[2]))
	Conf.write(self)
    def keys(self):
	# no need to return list in order here, I think.
	return self.vars.keys()
    def has_key(self, key):
	return self.vars.has_key(key)



# ConfChat(Conf):
#  Not a dictionary!
#  This reads chat files, and writes a subset of chat files that
#  has all items enclosed in '' and has one expect/send pair on
#  each line.
#  Uses a list of two-element tuples.
class ConfChat(Conf):
    def __init__(self, filename):
	Conf.__init__(self, filename, '', '\t ', ' ')
    def read(self):
        Conf.read(self)
        self.initlist()
    def initlist(self):
	self.list = []
	i = 0
	hastick = 0
	s = '' 
	chatlist = []
	for line in self.lines:
	    s = s + line + ' '
	while i < len(s) and s[i] in " \t":
	    i = i + 1
	while i < len(s):
	    str = ''
	    # here i points to a new entry
	    if s[i] in "'":
		hastick = 1
		i = i + 1
		while i < len(s) and s[i] not in "'":
		    if s[i] in '\\':
			if not s[i+1] in " \t":
			    str = str + '\\'
			i = i + 1
		    str = str + s[i]
		    i = i + 1
		# eat up the ending '
		i = i + 1
	    else:
		while i < len(s) and s[i] not in " \t":
		    str = str + s[i]
		    i = i + 1
	    chatlist.append(str)
	    # eat whitespace between strings
	    while i < len(s) and s[i] in ' \t':
		i = i + 1
	# now form self.list from chatlist
	if len(chatlist) % 2:
	    chatlist.append('')
	while chatlist:
	    self.list.append((chatlist[0], chatlist[1]))
	    chatlist[0:2] = []
    def getlist(self):
	return self.list
    def putlist(self, list):
	self.list = list
    def write(self):
	# create self.lines for Conf.write...
	self.lines = []
	for (p,q) in self.list:
	    p = regsub.gsub("'", "\'", p)
	    q = regsub.gsub("'", "\'", q)
	    self.lines.append("'"+p+"' '"+q+"'")
	Conf.write(self)



# ConfDIP:
#  This reads chat files, and writes a dip file based on that chat script.
#  Takes three arguments:
#   o The chatfile
#   o The name of the dipfile
#   o The ConfSHellVar instance from which to take variables in the dipfile
class ConfDIP:
    def __init__(self, chatfile, dipfilename, configfile):
	self.dipfilename = dipfilename
	self.chatfile = chatfile
	self.cf = configfile
    def write(self):
        self.file = open(self.dipfilename, 'w', -1)
	os.chmod(self.dipfilename, 0600)
	self.file.write('# dip script for interface '+self.cf['DEVICE']+'\n' +
	  '# DO NOT HAND-EDIT; ALL CHANGES *WILL* BE LOST BY THE netcfg PROGRAM\n' +
	  '# This file is created automatically from several other files by netcfg\n' +
	  '# Re-run netcfg to modify this file\n\n' +
	  'main:\n' +
	  '  get $local '+self.cf['IPADDR']+'\n' +
	  '  get $remote '+self.cf['REMIP']+'\n' +
	  '  port '+self.cf['MODEMPORT']+'\n' +
	  '  speed '+self.cf['LINESPEED']+'\n')
	if self.cf['MTU']:
	    self.file.write('  get $mtu '+self.cf['MTU']+'\n')
	for pair in self.chatfile.list:
	    if cmp(pair[0], 'ABORT') and cmp(pair[0], 'TIMEOUT'):
		if pair[0]:
		    self.file.write('  wait '+pair[0]+' 30\n' +
			    '  if $errlvl != 0 goto error\n')
		self.file.write('  send '+pair[1]+'\\r\\n\n' +
			'  if $errlvl != 0 goto error\n')
	if not cmp(self.cf['DEFROUTE'], 'yes'):
	    self.file.write('  default\n')
	self.file.write('  mode '+self.cf['MODE']+'\n' +
	  '  exit\n' +
	  'error:\n' +
	  '  print connection to $remote failed.\n')
        self.file.close()


# ConfModules(Conf)
#  This reads /etc/conf.modules into a dictionary keyed on device type,
#  holding dictionaries: cm['eth0']['alias'] --> 'smc-ultra'
#                        cm['eth0']['options'] --> {'io':'0x300', 'irq':'10'}
#                        cm['eth0']['post-install'] --> ['/bin/foo','arg1','arg2']
#  path[*] entries are ignored (but not removed)
#  New entries are added at the end to make sure that they
#  come after any path[*] entries.
#  Comments are delimited by initial '#'
class ConfModules(Conf):
    def __init__(self, filename = '/etc/conf.modules'):
	Conf.__init__(self, filename, '#', '\t ', ' ')
    def read(self):
        Conf.read(self)
        self.initvars()
    def initvars(self):
        self.vars = {}
	keys = ('alias', 'options', 'post-install')
        self.rewind()
        while self.findnextcodeline():
            var = self.getfields()
	    if len(var) > 2 and var[0] in keys:
		if not self.vars.has_key(var[1]):
		    self.vars[var[1]] = {'alias':'', 'options':{}, 'post-install':[]}
		if not cmp(var[0], 'alias'):
		    self.vars[var[1]]['alias'] = var[2]
		elif not cmp(var[0], 'options'):
		    self.vars[var[1]]['options'] = self.splitoptlist(var[2:])
		elif not cmp(var[0], 'post-install'):
		    self.vars[var[1]]['post-install'] = var[2:]
            self.nextline()
        self.rewind()
    def splitoptlist(self, optlist):
	dict = {}
	for opt in optlist:
	    optup = self.splitopt(opt)
	    if optup:
		dict[optup[0]] = optup[1]
	return dict
    def splitopt(self, opt):
	eq = search('=', opt)
	if eq > 0:
	    return (opt[:eq], opt[eq+1:])
	else:
	    return ()
    def joinoptlist(self, dict):
	optstring = ''
	for key in dict.keys():
	    optstring = optstring + key + '=' + dict[key] + ' '
	return optstring
    def __getitem__(self, varname):
        if self.vars.has_key(varname):
            return self.vars[varname]
        else:
            return {}
    def __setitem__(self, varname, value):
        # set *every* instance (should only be one, but...) to avoid surprises
        place=self.tell()
        self.vars[varname] = value
	for key in value.keys():
            self.rewind()
            missing=1
	    if not cmp(key, 'alias'):
		endofline = value[key]
	    elif not cmp(key, 'options'):
		endofline = self.joinoptlist(value[key])
	    elif not cmp(key, 'post-install'):
		endofline = joinfields(value[key], ' ')
	    else:
		# some idiot apparantly put an unrecognized key in
		# the dictionary; ignore it...
		continue
	    if endofline:
		# there's something to write...
        	while self.findnextline('[\t ]*' + key + '[\t ]+' + varname):
        	    self.setline(key + ' ' + varname + ' ' + endofline)
        	    missing=0
        	    self.nextline()
        	if missing:
		    self.fsf()
        	    self.insertline(key + ' ' + varname + ' ' + endofline)
	    else:
		# delete any instances of this if they exist.
        	while self.findnextline('[\t ]*' + key + '[\t ]+' + varname):
        	    self.deleteline()
	self.seek(place)
    def __delitem__(self, varname):
        # delete *every* instance...
        place=self.tell()
	for key in self.vars[varname].keys():
            self.rewind()
            while self.findnextline('[\t ]*' + key + '[\t ]+' + varname):
        	self.deleteline()
        del self.vars[varname]
	self.seek(place)
    def write(self):
	# need to make sure everything is set, because program above may
	# well have done cm['eth0']['post-install'] = ['/bin/foo', '-f', '/tmp/bar']
	# which is completely reasonable, but won't invoke __setitem__
	for key in self.vars.keys():
	    self[key] = self.vars[key]
	Conf.write(self)
    def keys(self):
	return self.vars.keys()
    def has_key(self, key):
	return self.vars.has_key(key)


# ConfModInfo(Conf)
#  This READ-ONLY class reads /boot/module-info.
#  The first line of /boot/module-info is "Version <version>";
#  this class reads versions 0 and 1 module-info files.
class ConfModInfo(Conf):
    def __init__(self, filename = '/boot/module-info'):
	Conf.__init__(self, filename, '#', '\t ', ' ')
    def read(self):
        Conf.read(self)
        self.initvars()
    def initvars(self):
        self.vars = {}
        self.rewind()
	device = 0
	modtype = 1
	description = 2
	arguments = 3
	lookingfor = device
	version = self.getfields()
        self.nextline()
	if not cmp(version[1], '0'):
	    # Version 0 file format
            while self.findnextcodeline():
        	line = self.getline()
		if not line[0] in self.separators:
		    curdev = line
		    self.vars[curdev] = {}
		    lookingfor = modtype
		elif lookingfor == modtype:
		    fields = self.getfields()
		    # first "field" is null (before separators)
		    self.vars[curdev]['type'] = fields[1]
		    if len(fields) > 2:
			self.vars[curdev]['typealias'] = fields[2]
		    lookingfor = description
		elif lookingfor == description:
		    self.vars[curdev]['description'] = regsub.gsub(
			'^"', '', regsub.gsub(
			    '^['+self.separators+']', '', regsub.gsub(
				'"['+self.separators+']*$', '', line)))
		    lookingfor = arguments
		elif lookingfor == arguments:
		    if not self.vars[curdev].has_key('arguments'):
			self.vars[curdev]['arguments'] = {}
		    # get argument name (first "field" is null again)
		    thislist = []
		    # point at first character of argument description
		    p = search('"', line)
		    while p != -1 and p < len(line):
			q = search('"', line[p+1:])
			# deal with escaped quotes (\")
			while q != -1 and not cmp(line[p+q-1], '\\'):
			    q = search('"', line[p+q+1:])
			if q == -1:
			    break
			thislist.append(line[p+1:p+q+1])
			# advance to beginning of next string, if any
			r = search('"', line[p+q+2:])
			if r >= 0:
			    p = p+q+2+r
			else:
			    # end of the line
			    p = r
		    self.vars[curdev]['arguments'][self.getfields()[1]] = thislist
        	self.nextline()
	elif not cmp(version[1], '1'):
	    # Version 1 file format
	    # Version 1 format uses ' ' and ':' characters as field separators
	    # but only uses ' ' in one place, where we explicitly look for it.
            self.separators = ':'
            while self.findnextcodeline():
        	line = self.getline()
		fields = self.getfields()
		# pull out module and linetype from the first field...
        	(module, linetype) = regsub.split(fields[0],'[ \t]+')
		if not cmp(linetype, 'type'):
		    pass
		elif not cmp(linetype, 'alias'):
		    pass
		elif not cmp(linetype, 'desc'):
		    pass
		elif not cmp(linetype, 'argument'):
		    pass
		elif not cmp(linetype, 'supports'):
		    pass
		elif not cmp(linetype, 'arch'):
		    pass
		elif not cmp(linetype, 'pcimagic'):
		    pass
		else:
		    # error: unknown flag...
		    raise 'BadFileFormat: unknown flag' + linetype
	else:
	    print 'Only versions 0 and 1 module-info files are supported'
	    raise 'ModInfoVersionMismatch'
        self.rewind()
    def __getitem__(self, varname):
        if self.vars.has_key(varname):
            return self.vars[varname]
        else:
            return {}
    def keys(self):
	return self.vars.keys()
    def has_key(self, key):
	return self.vars.has_key(key)
    def write(self):
	pass


# ConfPw(Conf)
#  This class implements a dictionary based on a :-separated file.
#  It takes as arguments the filename and the field number to key on;
#  The data provided is a list including all fields including the key.
#  Has its own write method to keep files sane.
class ConfPw(Conf):
    def __init__(self, filename, keyfield, numfields):
	self.keyfield = keyfield
	self.numfields = numfields
	Conf.__init__(self, filename, '', ':', ':', 0)
    def read(self):
	Conf.read(self)
	self.initvars()
    def initvars(self):
	self.vars = {}
	# need to be able to return the keys in order to keep
	# things consistent...
	self.ordered_keys = []
	self.rewind()
	while self.findnextcodeline():
	    fields = self.getfields()
	    self.vars[fields[self.keyfield]] = fields
	    self.ordered_keys.append(fields[self.keyfield])
            self.nextline()
	self.rewind()
    def __setitem__(self, key, value):
	if not self.findlinewithfield(self.keyfield, key):
	    self.fsf()
	    self.ordered_keys.append(key)
	self.setfields(value)
	self.vars[key] = value
    def __getitem__(self, key):
	if self.vars.has_key(key):
	    return self.vars[key]
	return []
    def __delitem__(self, key):
	if self.findlinewithfield(self.keyfield, key):
	    self.deleteline()
	if self.vars.has_key(key):
	    del self.vars[key]
	for i in range(len(self.ordered_keys)):
	    if key in self.ordered_keys[i:i+1]:
		self.ordered_keys[i:i+1] = []
		break
    def keys(self):
	return self.ordered_keys
    def has_key(self, key):
	return self.vars.has_key(key)
    def write(self):
        self.file = open(self.filename + '.new', 'w', -1)
	# change the mode of the new file to that of the old one
        if os.path.isfile(self.filename) and self.mode == 0:
	    os.chmod(self.filename + '.new', os.stat(self.filename)[0])
	if self.mode >= 0:
	    os.chmod(self.filename + '.new', self.mode)
        # add newlines while writing
        for index in range(len(self.lines)):
            self.file.write(self.lines[index] + '\n')
        self.file.close()
	os.rename(self.filename + '.new', self.filename)
    def changefield(self, key, fieldno, fieldtext):
	self.rewind()
	self.findlinewithfield(self.keyfield, key)
	Conf.changefield(self, fieldno, fieldtext)
	self.vars[key][fieldno:fieldno+1] = [fieldtext]

# ConfPwO
#  This class presents a data-oriented meta-class for making
#  changes to ConfPw-managed files.  Applications should not
#  instantiate this class directly.
class ConfPwO(ConfPw):
    def __init__(self, filename, keyfield, numfields, reflector):
	ConfPw.__init__(self, filename, keyfield, numfields)
	self.reflector = reflector
    def __getitem__(self, key):
	if self.vars.has_key(key):
	    return self.reflector(self, key)
	else:
	    return None
    def __setitem__(self, key):
	# items are objects which the higher-level code can't touch
	raise 'AttributeError: Object ' + self + ' is immutable'
    # __delitem__ is inherited from ConfPw
    # Do *not* use setitem for this; adding an entry should be
    # a much different action than accessing an entry or changing
    # fields in an entry.
    def addentry(self, key, list):
	if self.vars.has_key(key):
	    raise 'AttributeError: ' + key + ' exists'
	ConfPw.__setitem__(self, key, list)

# ConfPasswd(ConfPwO)
#  This class presents a data-oriented class for making changes
#  to the /etc/passwd file.
class _passwd_reflector:
    # first, we need a helper class...
    def __init__(self, pw, user):
	self.pw = pw
	self.user = user
    def __getattr__(self, name):
	if not self.pw.has_key(self.user):
	    raise 'AttributeError: ' + self.user + ' has been deleted'
	if not cmp(name,'username'):
	    return self.pw.vars[self.user][0]
	elif not cmp(name,'password'):
	    return self.pw.vars[self.user][1]
	elif not cmp(name,'uid'):
	    return self.pw.vars[self.user][2]
	elif not cmp(name,'gid'):
	    return self.pw.vars[self.user][3]
	elif not cmp(name,'gecos'):
	    return self.pw.vars[self.user][4]
	elif not cmp(name,'homedir'):
	    return self.pw.vars[self.user][5]
	elif not cmp(name,'shell'):
	    return self.pw.vars[self.user][6]
	else:
	    raise 'AttributeError: ' + name
    def __setattr__(self, name, value):
	if not cmp(name, 'pw') or not cmp(name, 'user'):
	    self.__dict__[name] = value
	    return None
	if not self.pw.has_key(self.user):
	    raise 'AttributeError: ' + self.user + ' has been deleted'
	if not cmp(name,'username'):
	    # username is not an lvalue...
	    raise 'AttributeError: ' + name + ': key is immutable'
	elif not cmp(name,'password'):
	    self.pw.changefield(self.user, 1, value)
	elif not cmp(name,'uid'):
	    self.pw.changefield(self.user, 2, str(value))
	elif not cmp(name,'gid'):
	    self.pw.changefield(self.user, 3, str(value))
	elif not cmp(name,'gecos'):
	    self.pw.changefield(self.user, 4, value)
	elif not cmp(name,'homedir'):
	    self.pw.changefield(self.user, 5, value)
	elif not cmp(name,'shell'):
	    self.pw.changefield(self.user, 6, value)
	else:
	    raise 'AttributeError: ' + name
class ConfPasswd(ConfPwO):
    def __init__(self):
	ConfPwO.__init__(self, '/etc/passwd', 0, 7, _passwd_reflector)
    def addentry(self, username, password, uid, gid, gecos, homedir, shell):
	ConfPwO.addentry(self, username, [username, password, uid, gid, gecos, homedir, shell])
    def getfreeuid(self):
	freeuid = 500
	for user in self.vars.keys():
	    id = atoi(self.vars[user][2])
	    if id >= freeuid:
		freeuid = id + 1
	if freeuid > 65533:
	    raise 'No UIDs available'
	return freeuid
	

# ConfShadow(ConfPwO)
#  This class presents a data-oriented class for making changes
#  to the /etc/shadow file.
class _shadow_reflector:
    # first, we need a helper class...
    def __init__(self, pw, user):
	self.pw = pw
	self.user = user
    def __getattr__(self, name):
	if not self.pw.has_key(self.user):
	    raise 'AttributeError: ' + self.user + ' has been deleted'
	if not cmp(name,'username'):
	    return self.pw.vars[self.user][0]
	elif not cmp(name,'password'):
	    return self.pw.vars[self.user][1]
	elif not cmp(name,'lastchanged'):
	    return self.pw.vars[self.user][2]
	elif not cmp(name,'mindays'):
	    return self.pw.vars[self.user][3]
	elif not cmp(name,'maxdays'):
	    return self.pw.vars[self.user][4]
	elif not cmp(name,'warndays'):
	    return self.pw.vars[self.user][5]
	elif not cmp(name,'gracedays'):
	    return self.pw.vars[self.user][6]
	elif not cmp(name,'expires'):
	    return self.pw.vars[self.user][7]
	else:
	    raise 'AttributeError: ' + name
    def __setattr__(self, name, value):
	if not cmp(name, 'pw') or not cmp(name, 'user'):
	    self.__dict__[name] = value
	    return None
	if not self.pw.has_key(self.user):
	    raise 'AttributeError: ' + self.user + ' has been deleted'
	if not cmp(name,'username'):
	    # username is not an lvalue...
	    raise 'AttributeError: ' + name + ': key is immutable'
	elif not cmp(name,'password'):
	    self.pw.changefield(self.user, 1, value)
	elif not cmp(name,'lastchanged'):
	    if not len(str(value)):
		value = '-1'
	    self.pw.changefield(self.user, 2, str(value))
	elif not cmp(name,'mindays'):
	    if not len(str(value)):
		value = '-1'
	    self.pw.changefield(self.user, 3, str(value))
	elif not cmp(name,'maxdays'):
	    if not len(str(value)):
		value = '-1'
	    self.pw.changefield(self.user, 4, str(value))
	elif not cmp(name,'warndays'):
	    if not len(str(value)):
		value = '-1'
	    self.pw.changefield(self.user, 5, str(value))
	elif not cmp(name,'gracedays'):
	    if not len(str(value)):
		value = '-1'
	    self.pw.changefield(self.user, 6, str(value))
	elif not cmp(name,'expires'):
	    if not len(str(value)):
		value = '-1'
	    self.pw.changefield(self.user, 7, str(value))
	else:
	    raise 'AttributeError: ' + name
class ConfShadow(ConfPwO):
    def __init__(self):
	ConfPwO.__init__(self, '/etc/shadow', 0, 9, _shadow_reflector)
    def addentry(self, username, password, lastchanged, mindays, maxdays, warndays, gracedays, expires):
	ConfPwO.addentry(self, username, [username, password, lastchanged, mindays, maxdays, warndays, gracedays, expires])

# ConfGroup(ConfPwO)
#  This class presents a data-oriented class for making changes
#  to the /etc/group file.
#  May be replaced by a pwdb-based module, we hope.
class _group_reflector:
    # first, we need a helper class...
    def __init__(self, pw, group):
	self.pw = pw
	self.group = group
    def __getattr__(self, name):
	if not self.pw.has_key(self.group):
	    raise 'AttributeError: ' + self.group + ' has been deleted'
	if not cmp(name,'name'):
	    return self.pw.vars[self.group][0]
	elif not cmp(name,'password'):
	    return self.pw.vars[self.group][1]
	elif not cmp(name,'gid'):
	    return self.pw.vars[self.group][2]
	elif not cmp(name,'userlist'):
	    return self.pw.vars[self.group][3]
	else:
	    raise 'AttributeError: ' + name
    def __setattr__(self, name, value):
	if not cmp(name, 'pw') or not cmp(name, 'group'):
	    self.__dict__[name] = value
	    return None
	if not self.pw.has_key(self.group):
	    raise 'AttributeError: ' + self.group + ' has been deleted'
	if not cmp(name,'name'):
	    # username is not an lvalue...
	    raise 'AttributeError: ' + name + ': key is immutable'
	elif not cmp(name,'password'):
	    self.pw.changefield(self.group, 1, value)
	elif not cmp(name,'gid'):
	    self.pw.changefield(self.group, 2, str(value))
	elif not cmp(name,'userlist'):
	    self.pw.changefield(self.group, 3, value)
	else:
	    raise 'AttributeError: ' + name
class ConfGroup(ConfPwO):
    def __init__(self):
	ConfPwO.__init__(self, '/etc/group', 0, 4, _group_reflector)
    def addentry(self, group, password, gid, userlist):
	ConfPwO.addentry(self, group, [group, password, gid, userlist])
    def getfreegid(self):
	freegid = 500
	for group in self.vars.keys():
	    id = atoi(self.vars[group][2])
	    if id >= freegid:
		freegid = id + 1
	if freegid > 65533:
	    raise 'No GIDs available'
	return freegid


# ConfUnix()
#  This class presents a data-oriented class which uses the ConfPasswd
#  and ConfShadow classes (if /etc/shadow exists) to hold data.
#  Designed to be replaced by a pwdb module eventually, we hope.
class _unix_reflector:
    # first, we need a helper class...
    def __init__(self, pw, user):
	self.pw = pw
	self.user = user
    def __getattr__(self, name):
	if not self.pw.passwd.has_key(self.user):
	    raise 'AttributeError: ' + self.user + ' has been deleted'
	if not cmp(name,'username'):
	    if self.pw.shadow:
		return self.pw.shadow[self.user].username
	    else:
		return self.pw.passwd[self.user].username
	elif not cmp(name,'password'):
	    if self.pw.shadow:
		return self.pw.shadow[self.user].password
	    else:
		return self.pw.passwd[self.user].password
	elif not cmp(name,'uid'):
	    return self.pw.passwd[self.user].uid
	elif not cmp(name,'gid'):
	    return self.pw.passwd[self.user].gid
	elif not cmp(name,'gecos'):
	    return self.pw.passwd[self.user].gecos
	elif not cmp(name,'homedir'):
	    return self.pw.passwd[self.user].homedir
	elif not cmp(name,'shell'):
	    return self.pw.passwd[self.user].shell
	elif not cmp(name,'lastchanged'):
	    if self.pw.shadowexists():
		return self.pw.shadow[self.user].lastchanged
	    else:
		return -1
	elif not cmp(name,'mindays'):
	    if self.pw.shadowexists():
		return self.pw.shadow[self.user].mindays
	    else:
		return -1
	elif not cmp(name,'maxdays'):
	    if self.pw.shadowexists():
		return self.pw.shadow[self.user].maxdays
	    else:
		return -1
	elif not cmp(name,'warndays'):
	    if self.pw.shadowexists():
		return self.pw.shadow[self.user].warndays
	    else:
		return -1
	elif not cmp(name,'gracedays'):
	    if self.pw.shadowexists():
		return self.pw.shadow[self.user].gracedays
	    else:
		return -1
	elif not cmp(name,'expires'):
	    if self.pw.shadowexists():
		return self.pw.shadow[self.user].expires
	    else:
		return -1
	else:
	    raise 'AttributeError: ' + name
    def __setattr__(self, name, value):
	if not cmp(name, 'pw') or not cmp(name, 'user'):
	    self.__dict__[name] = value
	    return None
	if not self.pw.passwd.has_key(self.user):
	    raise 'AttributeError: ' + self.user + ' has been deleted'
	if not cmp(name,'username'):
	    # username is not an lvalue...
	    raise 'AttributeError: ' + name + ': key is immutable'
	elif not cmp(name,'password'):
	    if self.pw.shadow:
		self.pw.shadow[self.user].password = value
	    else:
		self.pw.passwd[self.user].password = value
	elif not cmp(name,'uid'):
	    self.pw.passwd[self.user].uid = value
	elif not cmp(name,'gid'):
	    self.pw.passwd[self.user].gid = value
	elif not cmp(name,'gecos'):
	    self.pw.passwd[self.user].gecos = value
	elif not cmp(name,'homedir'):
	    self.pw.passwd[self.user].homedir = value
	elif not cmp(name,'shell'):
	    self.pw.passwd[self.user].shell = value
	elif not cmp(name,'lastchanged'):
	    if self.pw.shadowexists():
		self.pw.shadow[self.user].lastchanged = value
	elif not cmp(name,'mindays'):
	    if self.pw.shadowexists():
		self.pw.shadow[self.user].mindays = value
	elif not cmp(name,'maxdays'):
	    if self.pw.shadowexists():
		self.pw.shadow[self.user].maxdays = value
	elif not cmp(name,'warndays'):
	    if self.pw.shadowexists():
		self.pw.shadow[self.user].warndays = value
	elif not cmp(name,'gracedays'):
	    if self.pw.shadowexists():
		self.pw.shadow[self.user].gracedays = value
	elif not cmp(name,'expires'):
	    if self.pw.shadowexists():
		self.pw.shadow[self.user].expires = value
	else:
	    raise 'AttributeError: ' + name
class ConfUnix:
    def __init__(self):
	self.passwd = ConfPasswd()
	if os.path.isfile('/etc/shadow'):
	    self.shadow = ConfShadow()
	else:
	    self.shadow = None
    def getfreeuid(self):
	return self.passwd.getfreeuid()
    def addentry(self, username, password, uid, gid, gecos, homedir, shell,
		 lastchanged=-1, mindays=-1, maxdays=-1,
		 warndays=-1, gracedays=-1, expires=-1):
	if self.shadow:
	    self.passwd.addentry(username, 'x', uid, gid, gecos,
		homedir, shell)
	    self.shadow.addentry(username, password,
		lastchanged, mindays, maxdays, warndays, gracedays, expires)
	else:
	    self.passwd.addentry(username, password, uid, gid, gecos,
		homedir, shell)
    def shadowexists(self):
	if self.shadow:
	    return 1
	else:
	    return 0
    def write(self):
	self.passwd.write()
	if self.shadow:
	    self.shadow.write()

    def __getitem__(self, key):
	if self.passwd.has_key(key):
	    return _unix_reflector(self, key)
	else:
	    return None
    def __setitem__(self, key):
	# items are objects which the higher-level code can't touch
	raise 'AttributeError: Object is immutable'
    def __delitem__(self, key):
	del self.passwd[key]
	if self.shadow:
	    del self.shadow[key]
    def keys(self):
	return self.passwd.keys()
    def has_key(self, key):
	return self.passwd.has_key(key)

