diff options
author | Simo Sorce <ssorce@redhat.com> | 2007-09-06 17:57:54 -0400 |
---|---|---|
committer | Simo Sorce <ssorce@redhat.com> | 2007-09-06 17:57:54 -0400 |
commit | 450e2661d5289b061cf3af78c96078802668565f (patch) | |
tree | 2faea543ed91f821ae62f956039ffcc25b294b7c | |
parent | 438b548f1927bec83a3fd8a1b24b3a69c0aefc7a (diff) | |
download | freeipa-450e2661d5289b061cf3af78c96078802668565f.tar.gz freeipa-450e2661d5289b061cf3af78c96078802668565f.tar.xz freeipa-450e2661d5289b061cf3af78c96078802668565f.zip |
Better file parsing routines,
also switch to recreate ldap.conf and krb5.conf from scratch on clients,
avoid nasty failures in case the original files contained strange directives
-rw-r--r-- | ipa-client/ipa-install/ipa-client-install | 80 | ||||
-rw-r--r-- | ipa-client/ipaclient/ipachangeconf.py | 460 |
2 files changed, 385 insertions, 155 deletions
diff --git a/ipa-client/ipa-install/ipa-client-install b/ipa-client/ipa-install/ipa-client-install index f32cc2d6e..c1f3ed5be 100644 --- a/ipa-client/ipa-install/ipa-client-install +++ b/ipa-client/ipa-install/ipa-client-install @@ -31,6 +31,7 @@ from optparse import OptionParser import ipaclient.ipadiscovery import ipaclient.ipachangeconf from ipa.ipautil import run +import shutil def parse_options(): parser = OptionParser(version=VERSION) @@ -123,21 +124,19 @@ def main(): # Configure ldap.conf ldapconf = ipaclient.ipachangeconf.IPAChangeConf("IPA Installer") - opts = [{'name':'host', 'action':'comment'}, - {'name':'port', 'action':'comment'}, - {'name':'binddn', 'action':'comment'}, - {'name':'bindpw', 'action':'comment'}, - {'name':'rootbinddn', 'action':'comment'}, - {'name':'nss_base_passwd', 'value':ds.getBaseDN()+'?sub', 'action':'set'}, - {'name':'nss_base_group', 'value':ds.getBaseDN()+'?sub', 'action':'set'}, - {'name':'base', 'value':ds.getBaseDN(), 'action':'set'}, - {'name':'ldap_version', 'value':'3', 'action':'set'}] - if dnsok and not options.force: - opts.insert(0, {'name':'uri', 'action':'comment'}) - else: - opts.append({'name':'uri', 'value':'ldap://'+ds.getServerName(), 'action':'set'}) ldapconf.setOptionAssignment(" ") - ldapconf.changeConf("/etc/ldap.conf", opts) + + opts = [{'name':'comment', 'type':'comment', 'value':'File modified by ipa-client-install'}, + {'name':'empty', 'type':'empty'}, + {'name':'nss_base_passwd', 'type':'option', 'value':ds.getBaseDN()+'?sub'}, + {'name':'nss_base_group', 'type':'option', 'value':ds.getBaseDN()+'?sub'}, + {'name':'base', 'type':'option', 'value':ds.getBaseDN()}, + {'name':'ldap_version', 'type':'option', 'value':'3'}] + if not dnsok or options.force: + opts.append({'name':'uri', 'type':'option', 'value':'ldap://'+ds.getServerName()}) + + opts.append({'name':'empty', 'type':'empty'}) + ldapconf.newConf("/etc/ldap.conf", opts) #Check if kerberos is already configured properly krbctx = krbV.default_context() @@ -149,33 +148,52 @@ def main(): krbconf = ipaclient.ipachangeconf.IPAChangeConf("IPA Installer") krbconf.setOptionAssignment(" = ") krbconf.setSectionNameDelimiters(("[","]")) + krbconf.setSubSectionDelimiters(("{","}")) + krbconf.setIndent((""," "," ")) + + opts = [{'name':'comment', 'type':'comment', 'value':'File modified by ipa-client-install'}, + {'name':'empty', 'type':'empty'}] #[libdefaults] - opts = [{'name':'default_realm', 'value':ds.getRealmName(), 'action':'set'}, - {'name':'ticket_lifetime', 'value':'24h', 'action':'set'}, - {'name':'forwardable', 'value':'yes', 'action':'set'}] + libopts = [{'name':'default_realm', 'type':'option', 'value':ds.getRealmName()}] if dnsok and not options.force: - opts.insert(1, {'name':'dns_lookup_realm', 'value':'true', 'action':'set'}) - opts.insert(2, {'name':'dns_lookup_kdc', 'value':'true', 'action':'set'}) + libopts.append({'name':'dns_lookup_realm', 'type':'option', 'value':'true'}) + libopts.append({'name':'dns_lookup_kdc', 'type':'option', 'value':'true'}) else: - opts.insert(1, {'name':'dns_lookup_realm', 'value':'false', 'action':'set'}) - opts.insert(2, {'name':'dns_lookup_kdc', 'value':'false', 'action':'set'}) - krbconf.changeConf("/etc/krb5.conf", opts, "libdefaults"); + libopts.append({'name':'dns_lookup_realm', 'type':'option', 'value':'false'}) + libopts.append({'name':'dns_lookup_kdc', 'type':'option', 'value':'false'}) + libopts.append({'name':'ticket_lifetime', 'type':'option', 'value':'24h'}) + libopts.append({'name':'forwardable', 'type':'option', 'value':'yes'}) + + opts.append({'name':'libdefaults', 'type':'section', 'value':libopts}) + opts.append({'name':'empty', 'type':'empty'}) #the following are necessary only if DNS discovery does not work if not dnsok or options.force: #[realms] - opts = [{'name':ds.getRealmName(), 'value':'{', 'action':'set'}, - {'name':'kdc', 'value':ds.getServerName()+':88', 'action':'set'}, - {'name':'admin_server', 'value':ds.getServerName()+':749', 'action':'set'}, - # adding '\n}' is a dirty hack because we still don't have subsections support - {'name':'default_domain', 'value':ds.getDomainName()+'\n}', 'action':'set'}] - krbconf.changeConf("/etc/krb5.conf", opts, "realms"); + kropts =[{'name':'kdc', 'type':'option', 'value':ds.getServerName()+':88'}, + {'name':'admin_server', 'type':'option', 'value':ds.getServerName()+':749'}, + {'name':'default_domain', 'type':'option', 'value':ds.getDomainName()}] + ropts = [{'name':ds.getRealmName(), 'type':'subsection', 'value':kropts}] + opts.append({'name':'realms', 'type':'section', 'value':ropts}) + opts.append({'name':'empty', 'type':'empty'}) #[domain_realm] - opts = [{'name':'.'+ds.getDomainName(), 'value':ds.getRealmName(), 'action':'set'}, - {'name':ds.getDomainName(), 'value':ds.getRealmName(), 'action':'set'}] - krbconf.changeConf("/etc/krb5.conf", opts, "domain_realm"); + dropts = [{'name':'.'+ds.getDomainName(), 'type':'option', 'value':ds.getRealmName()}, + {'name':ds.getDomainName(), 'type':'option', 'value':ds.getRealmName()}] + opts.append({'name':'domain_realm', 'type':'section', 'value':dropts}) + opts.append({'name':'empty', 'type':'empty'}) + + #[appdefaults] + pamopts = [{'name':'debug', 'type':'option', 'value':'false'}, + {'name':'ticket_lifetime', 'type':'option', 'value':'36000'}, + {'name':'renew_lifetime', 'type':'option', 'value':'36000'}, + {'name':'forwardable', 'type':'option', 'value':'true'}, + {'name':'krb4_convert', 'type':'option', 'value':'false'}] + appopts = [{'name':'pam', 'type':'subsection', 'value':pamopts}] + opts.append({'name':'appdefaults', 'type':'section', 'value':appopts}) + + krbconf.newConf("/etc/krb5.conf", opts); #Modify nsswitch to add nss_ldap run(["/usr/sbin/authconfig", "--enableldap", "--update"]) diff --git a/ipa-client/ipaclient/ipachangeconf.py b/ipa-client/ipaclient/ipachangeconf.py index 646e0424e..38e5f9973 100644 --- a/ipa-client/ipaclient/ipachangeconf.py +++ b/ipa-client/ipaclient/ipachangeconf.py @@ -23,21 +23,22 @@ import fcntl import os import string import time +import shutil def openLocked(filename, perms): - fd = -1 - try: - fd = os.open(filename, os.O_RDWR | os.O_CREAT, perms) - - fcntl.lockf(fd, fcntl.LOCK_EX) - except OSError, (errno, strerr): - if fd != -1: - try: - os.close(fd) - except OSError: - pass - raise IOError(errno, strerr) - return os.fdopen(fd, "r+") + fd = -1 + try: + fd = os.open(filename, os.O_RDWR | os.O_CREAT, perms) + + fcntl.lockf(fd, fcntl.LOCK_EX) + except OSError, (errno, strerr): + if fd != -1: + try: + os.close(fd) + except OSError: + pass + raise IOError(errno, strerr) + return os.fdopen(fd, "r+") #TODO: add subsection as a concept @@ -49,44 +50,43 @@ class IPAChangeConf: def __init__(self, name): self.progname = name - self.optpre = ("",) - self.doptpre = self.optpre[0] - self.assign = (" = ",) + self.indent = ("","","") + self.assign = (" = ","=") self.dassign = self.assign[0] self.comment = ("#",) self.dcomment = self.comment[0] self.eol = ("\n",) self.deol = self.eol[0] - #self.sectnamdel = ("[","]") - self.sectnamdel = () - self.newsection = False + self.sectnamdel = ("[","]") + self.subsectdel = ("{","}") def setProgName(self, name): self.progname = name - def setOptionPrefix(self, prefix): - if type(prefix) is list: - self.optpre = prefix + def setIndent(self, indent): + if type(indent) is tuple: + self.indent = indent + elif type(indent) is str: + self.indent = (indent, ) else: - self.optpre = (prefix, ) - self.doptpre = self.optpre[0] + raise ValueError, 'Indent must be a list of strings' def setOptionAssignment(self, assign): - if type(assign) is list: + if type(assign) is tuple: self.assign = assign else: self.assign = (assign, ) self.dassign = self.assign[0] def setCommentPrefix(self, comment): - if type(comment) is list: + if type(comment) is tuple: self.comment = comment else: self.comment = (comment, ) self.dcomment = self.comment[0] def setEndLine(self, eol): - if type(eol) is list: + if type(eol) is tuple: self.eol = eol else: self.eol = (eol, ) @@ -95,56 +95,19 @@ class IPAChangeConf: def setSectionNameDelimiters(self, delims): self.sectnamdel = delims - def confDump(self, options): - output = "" - - #pre conf options delimiter - output += self.deol - output += self.dcomment+"["+self.progname+"]--start-line--"+self.deol - output += self.dcomment+" Generated by authconfig on " + time.strftime("%Y/%m/%d %H:%M:%S") + self.deol - output += self.dcomment+" DO NOT EDIT THIS SECTION (delimited by --start-line--/--end-line--)"+self.deol - output += self.dcomment+" Any modification may be deleted or altered by authconfig in future"+self.deol - output += self.deol - - if self.newsection: - output += getSectionLine(section) - - #set options - for opt in options: - if opt['action'] == "set": - output += self.doptpre+opt['name']+self.dassign+opt['value']+self.deol - - #post conf options delimiter - output += self.deol - output += self.dcomment+"["+self.progname+"]--end-line--"+self.deol - output += self.deol - - return output - - def matchAutoEnd(self, line): - if line.endswith("]--end-line--"): - return True - return False - - def matchAutoStart(self, line): - if line.endswith("]--start-line--"): - return True - return False + def setSubSectionDelimiters(self, delims): + self.subsectdel = delims def matchComment(self, line): for v in self.comment: - if line.strip().startswith(v): - return True + if line.lstrip().startswith(v): + return line.lstrip()[len(v):] return False - def matchLineOption(self, line, opt): - parts = line.split(self.dassign, 1) - if len(parts) < 2: - return False - elif parts[0].split() == opt['name'].split(): + def matchEmpty(self, line): + if line.strip() == "": return True - else: - return False + return False def matchSection(self, line): cl = "".join(line.strip().split()).lower() @@ -156,88 +119,337 @@ class IPAChangeConf: return False return cl[len(self.sectnamdel[0]):-len(self.sectnamdel[1])] + def matchSubSection(self, line): + if self.matchComment(line): + return False + + parts = line.split(self.dassign, 1) + if len(parts) < 2: + return False + + if parts[1].strip() == self.subsectdel[0]: + return parts[0].strip() + + return False + + def matchSubSectionEnd(self, line): + if self.matchComment(line): + return False + + if line.strip() == self.subsectdel[1]: + return True + + return False + def getSectionLine(self, section): if len(self.sectnamdel) != 2: return section return self.sectnamdel[0]+section+self.sectnamdel[1]+self.deol - def checkLineOption(self, line, options): + def dump(self, options, level=0): output = "" + if level >= len(self.indent): + level = len(self.indent)-1 + + for o in options: + if o['type'] == "section": + output += self.sectnamdel[0]+o['name']+self.sectnamdel[1]+self.deol + output += self.dump(o['value'], level+1) + continue + if o['type'] == "subsection": + output += self.indent[level]+o['name']+self.dassign+self.subsectdel[0]+self.deol + output += self.dump(o['value'], level+1) + output += self.indent[level]+self.subsectdel[1]+self.deol + continue + if o['type'] == "option": + output += self.indent[level]+o['name']+self.dassign+o['value']+self.deol + continue + if o['type'] == "comment": + output += self.dcomment+o['value']+self.deol + continue + if o['type'] == "empty": + output += self.deol + continue + raise SyntaxError, 'Unknown type: ['+o['type']+']' - # Check if this is a setting we care about. - for opt in options: - if self.matchLineOption(line.strip(), opt): - output = self.dcomment - break + return output + + def parseLine(self, line): + + if self.matchEmpty(line): + return {'name':'empty', 'type':'empty'} + + value = self.matchComment(line) + if value: + return {'name':'comment', 'type':'comment', 'value':value.rstrip()} + + parts = line.split(self.dassign, 1) + if len(parts) < 2: + raise SyntaxError, 'Syntax Error: Unknown line format' + + return {'name':parts[0].strip(), 'type':'option', 'value':parts[1].rstrip()} + + def findOpts(self, opts, type, name, exclude_sections=False): + + num = 0 + for o in opts: + if o['type'] == type and o['name'] == name: + return (num, o) + if exclude_sections and (o['type'] == "section" or o['type'] == "subsection"): + return (num, None) + num += 1 + return (num, None) + + def commentOpts(self, inopts, level = 0): + + opts = [] + + if level >= len(self.indent): + level = len(self.indent)-1 + + for o in inopts: + if o['type'] == 'section': + no = self.commentOpts(o['value'], level+1) + val = self.dcomment+self.sectnamdel[0]+o['name']+self.sectnamdel[1] + opts.append({'name':'comment', 'type':'comment', 'value':val}) + for n in no: + opts.append(n) + continue + if o['type'] == 'subsection': + no = self.commentOpts(o['value'], level+1) + val = self.indent[level]+o['name']+self.dassign+self.subsectdel[0] + opts.append({'name':'comment', 'type':'comment', 'value':val}) + for n in no: + opts.append(n) + val = self.indent[level]+self.subsectdel[1] + opts.append({'name':'comment', 'type':'comment', 'value':val}) + continue + if o['type'] == 'option': + val = self.indent[level]+o['name']+self.dassign+o['value'] + opts.append({'name':'comment', 'type':'comment', 'value':val}) + continue + if o['type'] == 'comment': + opts.append(o) + continue + if o['type'] == 'empty': + opts.append({'name':'comment', 'type':'comment', 'value':''}) + continue + raise SyntaxError, 'Unknown type: ['+o['type']+']' + + return opts + + def mergeOld(self, oldopts, newopts): + + opts = [] + + for o in oldopts: + if o['type'] == "section" or o['type'] == "subsection": + (num, no) = self.findOpts(newopts, o['type'], o['name']) + if not no: + opts.append(o) + continue + if no['action'] == "set": + mo = self.mergeOld(o['value'], no['value']) + opts.append({'name':o['name'], 'type':o['type'], 'value':mo}) + continue + if no['action'] == "comment": + co = self.commentOpts(o['value']) + for c in co: + opts.append(c) + continue + if no['action'] == "remove": + continue + raise SyntaxError, 'Unknown action: ['+no['action']+']' - output += line - return output; + if o['type'] == "comment" or o['type'] == "empty": + opts.append(o) + continue + + if o['type'] == "option": + (num, no) = self.findOpts(newopts, 'option', o['name'], True) + if not no: + opts.append(o) + continue + if no['action'] == 'comment' or no['action'] == 'remove': + if no['value'] != None and o['value'] != no['value']: + opts.append(o) + continue + if no['action'] == 'comment': + opts.append({'name':'comment', 'type':'comment', + 'value':self.dcomment+o['name']+self.dassign+o['value']}) + continue + if no['action'] == 'set': + opts.append(no) + continue + raise SyntaxError, 'Unknown action: ['+o['action']+']' + + raise SyntaxError, 'Unknown type: ['+o['type']+']' + + return opts + + def mergeNew(self, opts, newopts): + + cline = 0 + + for no in newopts: + + if no['type'] == "section" or no['type'] == "subsection": + (num, o) = self.findOpts(opts, no['type'], no['name']) + if not o: + if no['action'] == 'set': + opts.append(no) + continue + if no['action'] == "set": + self.mergeNew(o['value'], no['value']) + continue + cline = num+1 + continue + + if no['type'] == "option": + (num, o) = self.findOpts(opts, no['type'], no['name'], True) + if not o: + if no['action'] == 'set': + opts.append(no) + continue + cline = num+1 + continue + + if no['type'] == "comment" or no['type'] == "empty": + opts.insert(cline, no) + cline += 1 + continue + + raise SyntaxError, 'Unknown type: ['+no['type']+']' + + + def merge(self, oldopts, newopts): + + #Use a two pass strategy + #First we create a new opts tree from oldopts removing/commenting + # the options as indicated by the contents of newopts + #Second we fill in the new opts tree with options as indicated + # in the newopts tree (this is becaus eentire (sub)sections may + # exist in the newopts that do not exist in oldopts) + + opts = self.mergeOld(oldopts, newopts) + self.mergeNew(opts, newopts) + return opts + + #TODO: Make parse() recursive? + def parse(self, f): + + opts = [] + sectopts = [] + section = None + subsectopts = [] + subsection = None + curopts = opts + fatheropts = opts + + # Read in the old file. + for line in f: + + # It's a section start. + value = self.matchSection(line) + if value: + if section is not None: + opts.append({'name':section, 'type':'section', 'value':sectopts}) + sectopts = [] + curopts = sectopts + fatheropts = sectopts + section = value + continue + + # It's a subsection start. + value = self.matchSubSection(line) + if value: + if subsection is not None: + raise SyntaxError, 'nested subsections are not supported yet' + subsectopts = [] + curopts = subsectopts + subsection = value + continue + + value = self.matchSubSectionEnd(line) + if value: + if subsection is None: + raise SyntaxError, 'Unmatched end subsection terminator found' + fatheropts.append({'name':subsection, 'type':'subsection', 'value':subsectopts}) + subsection = None + curopts = fatheropts + continue + + # Copy anything else as is. + curopts.append(self.parseLine(line)) + + #Add last section if any + if len(sectopts) is not 0: + opts.append({'name':section, 'type':'section', 'value':sectopts}) + + return opts # Write settings to configuration file # file is a path # options is a set of dictionaries in the form: # [{'name': 'foo', 'value': 'bar', 'action': 'set/comment'}] # section is a section name like 'global' - def changeConf(self, file, options, section=None): + def changeConf(self, file, newopts): autosection = False - savedsection = None + savedsection = None done = False output = "" f = None try: - f = openLocked(file, 0644) + #Do not catch an unexisting file error, we want to fail in that case + shutil.copy2(file, file+".ipabkp") - # Read in the old file. - for line in f: + f = openLocked(file, 0644) - if autosection: - if self.matchAutoEnd(line): - autosection = False - #skip all previous auto-generated lines - continue + oldopts = self.parse(f) - if self.matchAutoStart(line): - autosection = True - continue + options = self.merge(oldopts, newopts) - # If it's a comment, just pass it through. - if self.matchComment(line): - output += line - continue + output = self.dump(options) - # If it's a section start, note the section name. - value = self.matchSection(line) - if value: - - # Here we are at start if a new section, if the previous one matches - # the specified section, dump here before starting the new one - # the comparison with section == None is intentional and matches the - # request to dump the options at the end of an implicit initial - # unmarked section before any section is started - if savedsection == section: - output += self.confDump(options) - done = True - - savedsection = value - output += line - continue - - # Comment out options we care about. - if savedsection == section: - output += self.checkLineOption(line, options) - continue + # Write it out and close it. + f.seek(0) + f.truncate(0) + f.write(output) + finally: + try: + if f: + f.close() + except IOError: + pass + return True - # Copy anything else as is. - output += line + # Write settings to new file, backup old + # file is a path + # options is a set of dictionaries in the form: + # [{'name': 'foo', 'value': 'bar', 'action': 'set/comment'}] + # section is a section name like 'global' + def newConf(self, file, options): + autosection = False + savedsection = None + done = False + output = "" + f = None + try: + try: + shutil.copy2(file, file+".ipabkp") + except IOError, err: + if err.errno == 2: + # The orign file did not exist + pass - if not done: - if section: - self.newsection = True - output += self.confDump(options) + f = openLocked(file, 0644) - # Write it out and close it. + # Trunkate f.seek(0) f.truncate(0) + + output = self.dump(options) + f.write(output) finally: try: |