cfgfiles.py revision 1516
1516N/A#!/usr/bin/python
281N/A#
281N/A# CDDL HEADER START
281N/A#
281N/A# The contents of this file are subject to the terms of the
281N/A# Common Development and Distribution License (the "License").
281N/A# You may not use this file except in compliance with the License.
281N/A#
281N/A# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
281N/A# or http://www.opensolaris.org/os/licensing.
281N/A# See the License for the specific language governing permissions
281N/A# and limitations under the License.
281N/A#
281N/A# When distributing Covered Code, include this CDDL HEADER in each
281N/A# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
281N/A# If applicable, add the following below this CDDL HEADER, with the
281N/A# fields enclosed by brackets "[]" replaced with your own identifying
281N/A# information: Portions Copyright [yyyy] [name of copyright owner]
281N/A#
281N/A# CDDL HEADER END
281N/A#
281N/A
281N/A#
1507N/A# Copyright 2009 Sun Microsystems, Inc. All rights reserved.
281N/A# Use is subject to license terms.
281N/A#
281N/A
289N/A# NOTE: This module is inherently posix specific. Care is taken in the modules
289N/A# that use this module to not use it on other operating systems.
289N/A
1507N/Aimport datetime
281N/Aimport fcntl
1507N/Aimport os
281N/Aimport re
1507N/Aimport stat
1507N/Aimport sys
281N/Aimport tempfile
281N/Aimport time
281N/A
281N/Aclass CfgFile(object):
281N/A """ Solaris configuration file editor... make it easier to
281N/A modify Solaris line-oriented configuration files from actions """
281N/A
1507N/A def __init__(self, filename, separator, column_names, keys,
281N/A comment_match="#", continuation_lines=False):
1507N/A
281N/A self.filename = filename
281N/A self.separator = separator
281N/A self.continuation_lines = continuation_lines
281N/A self.needswriting = False
281N/A
281N/A l = [(v[1][0], v[1][1], v[0]) for v in column_names.items()]
281N/A l.sort()
281N/A self.column_names = [e[2] for e in l]
281N/A self.default_values = dict((e[2], e[1]) for e in l)
281N/A
281N/A self.comment_regexp = re.compile(comment_match)
281N/A self.max_lengths=dict((n, 8) for n in self.column_names)
281N/A
281N/A if isinstance(keys, str):
281N/A self.keys = [keys]
281N/A else:
281N/A self.keys = keys
1507N/A
281N/A self.index = {}
281N/A
281N/A assert(set(self.column_names) >= set(self.keys))
281N/A
281N/A def __str__(self):
281N/A return "CfgFile(%s):%s:%s:%s" % \
1507N/A (self.filename, self.keys, self.column_names, self.index)
281N/A
281N/A def getcolumnnames(self):
281N/A return self.column_names
281N/A
281N/A def iscommentline(self, line):
281N/A return self.comment_regexp.match(line)
1507N/A
281N/A def splitline(self, line):
281N/A cols = line.split(self.separator)
281N/A
281N/A if len(cols) != len(self.column_names):
281N/A raise RuntimeError, "line %s in %s has %d columns" % \
281N/A (line, self.filename, len(cols))
281N/A return cols
281N/A
281N/A def getfilelines(self):
281N/A """ given self, return list of lines to be printed.
281N/A default impl preserves orignal + insertion order"""
281N/A lines = [[self.index[l][2],self.index[l][0]] for l in self.index]
281N/A lines.sort()
281N/A return [l[1] for l in lines]
281N/A
281N/A
281N/A def readfile(self):
281N/A if os.path.exists(self.filename):
281N/A file = open(self.filename)
281N/A lineno = 1
281N/A for line in file:
281N/A linecnt = 1;
281N/A
281N/A while self.continuation_lines and line[-2:] == "\\\n":
281N/A linecnt += 1
281N/A line += file.next()
1507N/A
281N/A line = line.rstrip("\n")
281N/A if self.iscommentline(line):
281N/A self.index[lineno] = \
281N/A (line, None, lineno)
281N/A else:
281N/A cols = self.splitline(line)
281N/A dic = dict(zip(self.column_names, cols))
281N/A self.index[tuple(dic[k] for k in self.keys)] = \
281N/A (line, dic, lineno)
281N/A# for k, v in dic.iteritems():
281N/A# self.max_lengths[k] = max(len(v), self.max_lengths[k])
281N/A lineno += linecnt
281N/A file.close()
281N/A self.needswriting = False
281N/A else:
281N/A print "no such file %s" % self.filename
281N/A
281N/A def getvalue(self, template):
281N/A val = self.index.get(tuple(template[k] for k in self.keys), None)
281N/A if val:
281N/A return val[1]
281N/A else:
281N/A return {}
281N/A
281N/A def getdefaultvalues(self):
281N/A """ returns dictionary of default string values - ignores
281N/A other types """
281N/A return dict((i, self.default_values[i])
281N/A for i in self.default_values
281N/A if isinstance(self.default_values[i], str))
1507N/A
281N/A def updatevalue(self, template):
1507N/A """ update existing record, using orig values if missing
281N/A in template"""
281N/A orig = self.index[tuple(template[k] for k in self.keys)].copy()
281N/A for name in self.column_names:
281N/A if name in template:
281N/A orig[name] = template[name]
281N/A self.setvalue(orig)
281N/A
281N/A def setvalue(self, template):
281N/A """ set value of record in file, replacing any previous def.
281N/A for any missing info, use defaults. Will insert new value """
281N/A # bring in any missing values as defaults if not None
281N/A for field in self.column_names:
281N/A if field not in template:
281N/A if self.default_values[field] is None:
281N/A raise RuntimeError, \
281N/A "Required attribute %s is missing" % field
281N/A elif callable(self.default_values[field]):
281N/A template[field] = self.default_values[field]()
281N/A else:
281N/A template[field] = self.default_values[field]
281N/A
281N/A orig = self.index.get(tuple(template[k] for k in self.keys), None)
281N/A
281N/A if orig:
281N/A lineno = orig[2]
281N/A del self.index[tuple(orig[1][k] for k in self.keys)]
281N/A else:
281N/A lineno = max((self.index[k][2] for k in self.index)) + 1
281N/A line = self.valuetostr(template)
281N/A self.index[tuple(template[k] for k in self.keys)] = \
281N/A (line, template, lineno)
281N/A self.needswriting = True
281N/A
281N/A def removevalue(self, template):
281N/A del self.index[tuple(template[k] for k in self.keys)]
281N/A self.needswriting = True
281N/A
281N/A def valuetostr(self, template):
281N/A """ print out values in file format """
281N/A return("%s" % self.separator.join(
281N/A [
281N/A "%s" % template[key] for key in self.column_names
281N/A ]))
1507N/A
281N/A def writefile(self):
1507N/A
281N/A if not self.needswriting:
281N/A return
281N/A
281N/A st = os.stat(self.filename)
281N/A
281N/A tempdata = tempfile.mkstemp(dir=os.path.dirname(self.filename))
281N/A file = os.fdopen(tempdata[0], "w")
281N/A name = tempdata[1]
281N/A
281N/A os.chmod(name, st.st_mode)
281N/A os.chown(name, st.st_uid, st.st_gid)
281N/A
281N/A for l in self.getfilelines():
281N/A print >>file, l
1507N/A
281N/A file.close()
281N/A
281N/A os.rename(name, self.filename)
1507N/A
281N/Aclass PasswordFile(CfgFile):
281N/A """Manage the passwd and shadow together. Note that
281N/A insertion/deletion of +/- fields isn't supported"""
281N/A def __init__(self, path_prefix, lock=False):
281N/A self.password_file = \
281N/A CfgFile(os.path.join(path_prefix, "etc/passwd"),
281N/A ":",
281N/A {"username" : (1, None),
281N/A "password" : (2, "x"),
437N/A "uid" : (3, None),
1507N/A "gid" : (4, None),
1507N/A "gcos-field" : (5, "& User"),
281N/A "home-dir" : (6, "/"),
281N/A "login-shell": (7, "")
281N/A },
281N/A "username", comment_match="[-+]")
281N/A days = datetime.timedelta(seconds=time.time()).days
281N/A self.shadow_file = \
281N/A CfgFile(os.path.join(path_prefix, "etc/shadow"),
281N/A ":",
281N/A {"username" : (1, None),
281N/A "password" : (2, "*LK*"),
1507N/A "lastchg" : (3, days),
281N/A "min" : (4, ""),
281N/A "max" : (5, ""),
281N/A "warn" : (6, ""),
281N/A "inactive" : (7, ""),
281N/A "expire" : (8, ""),
281N/A "flag" : (9, "")
281N/A },
281N/A "username", comment_match="[-+]")
281N/A self.path_prefix = path_prefix
281N/A self.lockfd = None
281N/A if lock:
281N/A self.lockfile()
281N/A self.readfile()
437N/A self.password_file.default_values["uid"] = self.getnextuid()
281N/A
281N/A def __str__(self):
281N/A return "PasswordFile: [%s %s]" % (self.password_file, self.shadow_file)
1507N/A
281N/A def getvalue(self, template):
281N/A """ merge dbs... do passwd file first to get right passwd value"""
1507N/A c = self.password_file.getvalue(template).copy()
281N/A c.update(self.shadow_file.getvalue(template))
281N/A return c
281N/A
281N/A def updatevalue(self, template):
281N/A copy = template.copy()
281N/A if "password" in copy:
281N/A copy["password"]=""
281N/A self.password_file.updatevalue(copy)
1507N/A self.shadow_file.updatevalue(template)
281N/A
281N/A def setvalue(self, template):
281N/A # ignore attempts to set passwd for passwd file
281N/A copy = template.copy()
281N/A if "password" in copy:
281N/A copy["password"]="x"
281N/A self.password_file.setvalue(copy)
281N/A self.shadow_file.setvalue(template)
281N/A
281N/A def removevalue(self, template):
281N/A self.password_file.removevalue(template)
281N/A self.shadow_file.removevalue(template)
281N/A
281N/A def getnextuid(self):
1507N/A """returns next free system (<=99) uid"""
281N/A uids=[]
281N/A for t in self.password_file.index.itervalues():
1128N/A if t[1]:
1128N/A uids.append(t[1]["uid"])
281N/A for i in range(100):
437N/A if str(i) not in uids:
281N/A return i
281N/A raise RuntimeError, "No free system uids"
281N/A
281N/A def getcolumnnames(self):
281N/A names = self.password_file.column_names.copy()
281N/A names.update(self.shadow_file.column_names)
281N/A return names
281N/A
281N/A def readfile(self):
281N/A self.password_file.readfile()
281N/A self.shadow_file.readfile()
281N/A
281N/A def writefile(self):
281N/A self.password_file.writefile()
281N/A self.shadow_file.writefile()
281N/A
281N/A def getuser(self, username):
281N/A return self.getvalue({"username" : username})
1507N/A
281N/A def getdefaultvalues(self):
281N/A a = self.password_file.getdefaultvalues()
281N/A a.update(self.shadow_file.getdefaultvalues())
281N/A return a
281N/A
281N/A def lockfile(self):
281N/A fn = os.path.join(self.path_prefix, "etc/.pwd.lock")
281N/A self.lockfd = file(fn, 'w')
1507N/A os.chmod(fn, stat.S_IRUSR|stat.S_IWUSR)
281N/A fcntl.lockf(self.lockfd, fcntl.LOCK_EX, 0,0,0)
1507N/A
281N/A def unlockfile(self):
281N/A fcntl.lockf(self.lockfd, fcntl.LOCK_EX, 0,0,0)
281N/A self.lockfd.close()
281N/A self.lockfd = None
1507N/A
281N/Aclass GroupFile(CfgFile):
281N/A """ manage the group file"""
281N/A def __init__(self, path_prefix):
281N/A CfgFile.__init__(self, os.path.join(path_prefix, "etc/group"),
281N/A ":",
281N/A {"groupname" : (1, None),
281N/A "password" : (2, ""),
437N/A "gid" : (3, None),
281N/A "user-list" : (4, "")
281N/A },
281N/A "groupname", comment_match="[+-]")
1507N/A
281N/A self.readfile()
437N/A self.default_values["gid"] = self.getnextgid()
281N/A
281N/A def getnextgid(self):
1507N/A """returns next free system (<=99) gid"""
281N/A gids=[]
281N/A for t in self.index.itervalues():
1128N/A if t[1]:
1128N/A gids.append(t[1]["gid"])
281N/A for i in range(100):
437N/A if str(i) not in gids:
281N/A return i
281N/A raise RuntimeError, "No free system gids"
281N/A
281N/A def adduser(self, groupname, username):
281N/A """"add named user to group; does not check if user exists"""
281N/A group = self.getvalue({"groupname":groupname})
281N/A if not group:
281N/A raise RuntimeError, "No such group %s" % groupname
281N/A users = set(group["user-list"].replace(","," ").split())
281N/A users.add(username)
281N/A group["user-list"] = ",".join(users)
281N/A self.setvalue(group)
281N/A
281N/A def subuser(self, groupname, username):
281N/A """ remove named user from group """
281N/A group = self.getvalue({"groupname":groupname})
281N/A if not group:
281N/A raise RuntimeError, "No such group %s" % groupname
281N/A users = set(group["user-list"].replace(","," ").split())
281N/A if username not in users:
281N/A raise RuntimeError, "User %s not in group %s" % (
281N/A username, groupname)
281N/A users.remove(username)
281N/A group["user-list"] = ",".join(users)
281N/A self.setvalue(group)
1507N/A
281N/A def getgroups(self, username):
281N/A """ return list of additional groups user belongs to """
281N/A return sorted([
1507N/A t[1]["groupname"]
281N/A for t in self.index.values()
281N/A if username in t[1]["user-list"].split(",")
281N/A ])
281N/A
281N/A def setgroups(self, username, groups):
281N/A current = self.getgroups(username)
281N/A
281N/A removals = set(current) - set(groups)
281N/A additions = set(groups) - set(current)
281N/A for g in removals:
281N/A self.subuser(g, username)
281N/A for g in additions:
1507N/A self.adduser(g, username)
281N/A
281N/A def removeuser(self, username):
281N/A for g in self.getgroups(username):
281N/A self.subuser(g, username)
1507N/A
281N/Aclass FtpusersFile(CfgFile):
281N/A """ If a username is present in this file, it denies that user
281N/A the ability to use ftp"""
281N/A
281N/A def __init__(self, path_prefix):
1507N/A
281N/A CfgFile.__init__(self, os.path.join(path_prefix, "etc/ftpd/ftpusers"),
281N/A " ",
281N/A {"username" : (1, None)
281N/A },
281N/A "username")
281N/A self.readfile()
281N/A
281N/A def getuser(self, username):
281N/A """ returns true if user is allowed to use FTP - ie is NOT in file"""
281N/A return not 'username' in self.getvalue({"username" : username})
281N/A
281N/A
281N/A def adduser(self, username):
281N/A """ add specified user to file, removing ability to use ftp"""
281N/A self.setvalue({"username" : username})
281N/A
281N/A def subuser(self, username):
281N/A """ remove specified user from file """
281N/A self.removevalue({"username" : username})
281N/A
281N/A def setuser(self, username, value):
281N/A if not value and self.getuser(username):
281N/A self.adduser(username)
281N/A elif not self.getuser(username):
281N/A self.subuser(username)
281N/A
281N/Aclass UserattrFile(CfgFile):
281N/A """ manage the userattr file """
281N/A def __init__(self, path_prefix):
281N/A CfgFile.__init__(self, os.path.join(path_prefix, "etc/user_attr"),
281N/A ":",
281N/A {"username" : (1, None),
281N/A "qualifier" : (2, ""),
281N/A "reserved1" : (3, ""),
281N/A "reserved2" : (4, ""),
281N/A "attributes" : (5, "")
281N/A },
281N/A "username")
281N/A self.readfile()
281N/A
281N/A def iscommentline(self, line):
281N/A return len(line) == 0 or self.comment_regexp.match(line)
281N/A
281N/A def splitline(self, line):
281N/A """ return tokenized line, with attribute column a dictionary
281N/A w/ lists for values"""
281N/A cols = re.split("(?<=[^\\\\]):", line) #match non-escaped :
281N/A if len(cols) != len(self.column_names):
281N/A raise RuntimeError, "line %s in %s has %d columns rather than %s" % \
281N/A (line, self.filename, len(cols), len(self.column_names))
281N/A attributes=re.split("(?<=[^\\\\]);", cols[4]) # match non escaped ;
281N/A
281N/A d = {}
281N/A for attr in attributes:
281N/A a = re.split("(?<=[^\\\\])=", attr)
1507N/A d[a[0]] = a[1].split(",")
281N/A cols[4] = d
281N/A return cols
281N/A
281N/A def valuetostr(self, template):
281N/A """ print out string; replace attribute dictionary with proper
281N/A string and use base class to convert entire record to a string """
281N/A c = template.copy() # since we're mucking w/ this....
281N/A attrdict = c["attributes"]
281N/A
281N/A str = "%s" % ";".join(
281N/A [
281N/A "%s=%s" % (key, ",".join(attrdict[key])) for key in attrdict
281N/A ])
281N/A c["attributes"] = str
281N/A return CfgFile.valuetostr(self, c)
281N/A