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
This commit is contained in:
Simo Sorce 2007-09-06 17:57:54 -04:00
parent 584baa7ee2
commit 566018f4d4
2 changed files with 387 additions and 157 deletions

View File

@ -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"])

View File

@ -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,84 +119,297 @@ 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
# Check if this is a setting we care about.
for opt in options:
if self.matchLineOption(line.strip(), opt):
output = self.dcomment
break
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']+']'
output += line
return output;
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']+']'
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:
#Do not catch an unexisting file error, we want to fail in that case
shutil.copy2(file, file+".ipabkp")
f = openLocked(file, 0644)
# Read in the old file.
for line in f:
oldopts = self.parse(f)
if autosection:
if self.matchAutoEnd(line):
autosection = False
#skip all previous auto-generated lines
continue
options = self.merge(oldopts, newopts)
if self.matchAutoStart(line):
autosection = True
continue
# If it's a comment, just pass it through.
if self.matchComment(line):
output += line
continue
# 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
# Copy anything else as is.
output += line
if not done:
if section:
self.newsection = True
output += self.confDump(options)
output = self.dump(options)
# Write it out and close it.
f.seek(0)
@ -246,3 +422,39 @@ class IPAChangeConf:
except IOError:
pass
return True
# 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
f = openLocked(file, 0644)
# Trunkate
f.seek(0)
f.truncate(0)
output = self.dump(options)
f.write(output)
finally:
try:
if f:
f.close()
except IOError:
pass
return True