diff options
Diffstat (limited to 'ipa-client/ipaclient')
-rw-r--r-- | ipa-client/ipaclient/Makefile.am | 16 | ||||
-rw-r--r-- | ipa-client/ipaclient/__init__.py | 21 | ||||
-rw-r--r-- | ipa-client/ipaclient/ipachangeconf.py | 459 | ||||
-rw-r--r-- | ipa-client/ipaclient/ipadiscovery.py | 248 | ||||
-rw-r--r-- | ipa-client/ipaclient/ntpconf.py | 111 |
5 files changed, 855 insertions, 0 deletions
diff --git a/ipa-client/ipaclient/Makefile.am b/ipa-client/ipaclient/Makefile.am new file mode 100644 index 00000000..1d7df526 --- /dev/null +++ b/ipa-client/ipaclient/Makefile.am @@ -0,0 +1,16 @@ +NULL = + +appdir = $(pythondir)/ipaclient +app_PYTHON = \ + __init__.py \ + ipachangeconf.py \ + ipadiscovery.py \ + ntpconf.py \ + $(NULL) + +EXTRA_DIST = \ + $(NULL) + +MAINTAINERCLEANFILES = \ + *~ \ + Makefile.in diff --git a/ipa-client/ipaclient/__init__.py b/ipa-client/ipaclient/__init__.py new file mode 100644 index 00000000..3eabc0f3 --- /dev/null +++ b/ipa-client/ipaclient/__init__.py @@ -0,0 +1,21 @@ +# Authors: Simo Sorce <ssorce@redhat.com> +# +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 or later +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +__all__ = ["ipadiscovery", "ipachangeconf"] + diff --git a/ipa-client/ipaclient/ipachangeconf.py b/ipa-client/ipaclient/ipachangeconf.py new file mode 100644 index 00000000..34c08d10 --- /dev/null +++ b/ipa-client/ipaclient/ipachangeconf.py @@ -0,0 +1,459 @@ +# +# ipachangeconf - configuration file manipulation classes and functions +# partially based on authconfig code +# Copyright (c) 1999-2007 Red Hat, Inc. +# Author: Simo Sorce <ssorce@redhat.com> +# +# This is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# + +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+") + + + #TODO: add subsection as a concept + # (ex. REALM.NAME = { foo = x bar = y } ) + #TODO: put section delimiters as separating element of the list + # so that we can process multiple sections in one go + #TODO: add a comment all but provided options as a section option +class IPAChangeConf: + + def __init__(self, name): + self.progname = name + 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.subsectdel = ("{","}") + + def setProgName(self, name): + self.progname = name + + def setIndent(self, indent): + if type(indent) is tuple: + self.indent = indent + elif type(indent) is str: + self.indent = (indent, ) + else: + raise ValueError, 'Indent must be a list of strings' + + def setOptionAssignment(self, assign): + 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 tuple: + self.comment = comment + else: + self.comment = (comment, ) + self.dcomment = self.comment[0] + + def setEndLine(self, eol): + if type(eol) is tuple: + self.eol = eol + else: + self.eol = (eol, ) + self.deol = self.eol[0] + + def setSectionNameDelimiters(self, delims): + self.sectnamdel = delims + + def setSubSectionDelimiters(self, delims): + self.subsectdel = delims + + def matchComment(self, line): + for v in self.comment: + if line.lstrip().startswith(v): + return line.lstrip()[len(v):] + return False + + def matchEmpty(self, line): + if line.strip() == "": + return True + return False + + def matchSection(self, line): + cl = "".join(line.strip().split()).lower() + if len(self.sectnamdel) != 2: + return False + if not cl.startswith(self.sectnamdel[0]): + return False + if not cl.endswith(self.sectnamdel[1]): + 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 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']+']' + + 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, newopts): + autosection = False + 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) + + oldopts = self.parse(f) + + options = self.merge(oldopts, newopts) + + output = self.dump(options) + + # 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 + + # 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 diff --git a/ipa-client/ipaclient/ipadiscovery.py b/ipa-client/ipaclient/ipadiscovery.py new file mode 100644 index 00000000..2bd15192 --- /dev/null +++ b/ipa-client/ipaclient/ipadiscovery.py @@ -0,0 +1,248 @@ +# Authors: Simo Sorce <ssorce@redhat.com> +# +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import socket +import logging +import ipa.dnsclient +import ldap +from ldap import LDAPError + +class IPADiscovery: + + def __init__(self): + self.realm = None + self.domain = None + self.server = None + self.basedn = None + + def getServerName(self): + return self.server + + def getDomainName(self): + return self.domain + + def getRealmName(self): + return self.realm + + def getBaseDN(self): + return self.basedn + + def search(self, domain = "", server = ""): + hostname = "" + qname = "" + results = [] + result = [] + krbret = [] + ldapret = [] + + if not server: + + if not domain: #domain not provided do full DNS discovery + + # get the local host name + hostname = socket.getfqdn() + if not hostname: + return -10 #bad host configuration + + # first, check for an LDAP server for the local domain + p = hostname.find(".") + if p == -1: #no domain name + return -1 + domain = hostname[p+1:] + + while not self.server: + logging.debug("[ipadnssearchldap("+domain+")]") + self.server = self.ipadnssearchldap(domain) + if self.server: + self.domain = domain + else: + p = domain.find(".") + if p == -1: #no ldap server found and last component of the domain already tested + return -1 + domain = domain[p+1:] + else: + logging.debug("[ipadnssearchldap]") + self.server = self.ipadnssearchldap(domain) + if self.server: + self.domain = domain + else: + return -2 #no ldap server found + + + #search for kerberos TODO: move this after ipacheckldap() + logging.debug("[ipadnssearchkrb]") + krbret = self.ipadnssearchkrb(self.domain) + if not krbret: + return -3 #no krb server found + + self.realm = krbret[0] + + else: #server forced on us, this means DNS doesn't work :/ + + self.domain = domain + self.server = server + + logging.debug("[ipacheckldap]") + # check ldap now + ldapret = self.ipacheckldap(self.server, self.realm) + + if not ldapret: + return -4 # not an IPA server (or broken config) + + self.server = ldapret[0] + self.realm = ldapret[1] + + return 0 + + def ipacheckldap(self, thost, trealm): + + lret = [] + lres = [] + lattr = "" + linfo = "" + lrealms = [] + + i = 0 + + #now verify the server is really an IPA server + try: + logging.debug("Init ldap with: ldap://"+thost+":389") + lh = ldap.initialize("ldap://"+thost+":389") + lh.simple_bind_s("","") + + logging.debug("Search rootdse") + lret = lh.search_s("", ldap.SCOPE_BASE, "(objectClass=*)") + for lattr in lret[0][1]: + if lattr.lower() == "namingcontexts": + self.basedn = lret[0][1][lattr][0] + + logging.debug("Search for (info=*) in "+self.basedn+"(base)") + lret = lh.search_s(self.basedn, ldap.SCOPE_BASE, "(info=IPA*)") + if not lret: + return [] + logging.debug("Found: "+str(lret)) + + for lattr in lret[0][1]: + if lattr.lower() == "info": + linfo = lret[0][1][lattr][0].lower() + break + + if not linfo: + return [] + + #search and return known realms + logging.debug("Search for (objectClass=krbRealmContainer) in "+self.basedn+"(sub)") + lret = lh.search_s("cn=kerberos,"+self.basedn, ldap.SCOPE_SUBTREE, "(objectClass=krbRealmContainer)") + if not lret: + #something very wrong + return [] + logging.debug("Found: "+str(lret)) + + for lres in lret: + for lattr in lres[1]: + if lattr.lower() == "cn": + lrealms.append(lres[1][lattr][0]) + + + if trealm: + for r in lrealms: + if trealm == r: + return [thost, trealm] + # must match or something is very wrong + return [] + else: + if len(lrealms) != 1: + #which one? we can't attach to a multi-realm server without DNS working + return [] + else: + return [thost, lrealms[0]] + + #we shouldn't get here + return [] + + except LDAPError, err: + #no good + try: + if type(err.message) == dict: + for (k, v) in err.message.iteritems(): + logging.error("LDAP Error: %s" % v ) + else: + logging.error("LDAP Error: "+err.message) + except AttributeError: + logging.error("LDAP Error: "+str(err)) + return [] + + + def ipadnssearchldap(self, tdomain): + servers = "" + rserver = "" + + qname = "_ldap._tcp."+tdomain + # terminate the name + if not qname.endswith("."): + qname += "." + results = ipa.dnsclient.query(qname, ipa.dnsclient.DNS_C_IN, ipa.dnsclient.DNS_T_SRV) + + for result in results: + if result.dns_type == ipa.dnsclient.DNS_T_SRV: + rserver = result.rdata.server.rstrip(".") + if result.rdata.port and result.rdata.port != 389: + rserver += ":" + str(result.rdata.port) + if servers: + servers += "," + rserver + else: + servers = rserver + break + + return servers + + def ipadnssearchkrb(self, tdomain): + realm = "" + kdc = "" + # now, check for a Kerberos realm the local host or domain is in + qname = "_kerberos." + tdomain + # terminate the name + if not qname.endswith("."): + qname += "." + results = ipa.dnsclient.query(qname, ipa.dnsclient.DNS_C_IN, ipa.dnsclient.DNS_T_TXT) + + for result in results: + if result.dns_type == ipa.dnsclient.DNS_T_TXT: + realm = result.rdata.data + if realm: + break + + if realm: + # now fetch server information for the realm + qname = "_kerberos._udp." + tdomain + # terminate the name + if not qname.endswith("."): + qname += "." + results = ipa.dnsclient.query(qname, ipa.dnsclient.DNS_C_IN, ipa.dnsclient.DNS_T_SRV) + for result in results: + if result.dns_type == ipa.dnsclient.DNS_T_SRV: + qname = result.rdata.server.rstrip(".") + if result.rdata.port and result.rdata.port != 88: + qname += ":" + str(result.rdata.port) + if kdc: + kdc += "," + qname + else: + kdc = qname + + return [realm, kdc] diff --git a/ipa-client/ipaclient/ntpconf.py b/ipa-client/ipaclient/ntpconf.py new file mode 100644 index 00000000..14e720c2 --- /dev/null +++ b/ipa-client/ipaclient/ntpconf.py @@ -0,0 +1,111 @@ +# Authors: Karl MacMillan <kmacmillan@redhat.com> +# +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +from ipa.ipautil import * +import shutil + +ntp_conf = """# Permit time synchronization with our time source, but do not +# permit the source to query or modify the service on this system. +restrict default kod nomodify notrap nopeer noquery +restrict -6 default kod nomodify notrap nopeer noquery + +# Permit all access over the loopback interface. This could +# be tightened as well, but to do so would effect some of +# the administrative functions. +restrict 127.0.0.1 +restrict -6 ::1 + +# Hosts on local network are less restricted. +#restrict 192.168.1.0 mask 255.255.255.0 nomodify notrap + +# Use public servers from the pool.ntp.org project. +# Please consider joining the pool (http://www.pool.ntp.org/join.html). +server $SERVER + +#broadcast 192.168.1.255 key 42 # broadcast server +#broadcastclient # broadcast client +#broadcast 224.0.1.1 key 42 # multicast server +#multicastclient 224.0.1.1 # multicast client +#manycastserver 239.255.254.254 # manycast server +#manycastclient 239.255.254.254 key 42 # manycast client + +# Undisciplined Local Clock. This is a fake driver intended for backup +# and when no outside source of synchronized time is available. +server 127.127.1.0 # local clock +#fudge 127.127.1.0 stratum 10 + +# Drift file. Put this in a directory which the daemon can write to. +# No symbolic links allowed, either, since the daemon updates the file +# by creating a temporary in the same directory and then rename()'ing +# it to the file. +driftfile /var/lib/ntp/drift + +# Key file containing the keys and key identifiers used when operating +# with symmetric key cryptography. +keys /etc/ntp/keys + +# Specify the key identifiers which are trusted. +#trustedkey 4 8 42 + +# Specify the key identifier to use with the ntpdc utility. +#requestkey 8 + +# Specify the key identifier to use with the ntpq utility. +#controlkey 8 +""" + +ntp_sysconfig = """# Drop root to id 'ntp:ntp' by default. +OPTIONS="-x -u ntp:ntp -p /var/run/ntpd.pid" + +# Set to 'yes' to sync hw clock after successful ntpdate +SYNC_HWCLOCK=yes + +# Additional options for ntpdate +NTPDATE_OPTIONS="" +""" + +def config_ntp(server_fqdn, fstore = None): + sub_dict = { } + sub_dict["SERVER"] = server_fqdn + + nc = template_str(ntp_conf, sub_dict) + + if fstore: + fstore.backup_file("/etc/ntp.conf") + else: + shutil.copy("/etc/ntp.conf", "/etc/ntp.conf.ipasave") + + fd = open("/etc/ntp.conf", "w") + fd.write(nc) + fd.close() + + if fstore: + fstore.backup_file("/etc/sysconfig/ntpd") + else: + shutil.copy("/etc/sysconfig/ntpd", "/etc/sysconfig/ntpd.ipasave") + + fd = open("/etc/sysconfig/ntpd", "w") + fd.write(ntp_sysconfig) + fd.close() + + # Set the ntpd to start on boot + run(["/sbin/chkconfig", "ntpd", "on"]) + + # Restart ntpd + run(["/sbin/service", "ntpd", "restart"]) |