From 7beb2f08d8dab26d9fe34235c91c1b2a7a2794bd Mon Sep 17 00:00:00 2001 From: Narayan Desai Date: Fri, 6 Oct 2006 21:06:16 +0000 Subject: Merge of the client/refactor branch (revisions r2218-r2400) git-svn-id: https://svn.mcs.anl.gov/repos/bcfg/trunk/bcfg2@2401 ce84e21b-d406-0410-9b95-82705330c041 --- src/lib/Client/Tools/APT.py | 73 +++++++++++ src/lib/Client/Tools/Blast.py | 22 ++++ src/lib/Client/Tools/Chkconfig.py | 62 +++++++++ src/lib/Client/Tools/DebInit.py | 65 ++++++++++ src/lib/Client/Tools/Encap.py | 60 +++++++++ src/lib/Client/Tools/POSIX.py | 243 ++++++++++++++++++++++++++++++++++ src/lib/Client/Tools/PostInstall.py | 21 +++ src/lib/Client/Tools/RPM.py | 82 ++++++++++++ src/lib/Client/Tools/SMF.py | 123 ++++++++++++++++++ src/lib/Client/Tools/SYSV.py | 83 ++++++++++++ src/lib/Client/Tools/__init__.py | 252 ++++++++++++++++++++++++++++++++++++ 11 files changed, 1086 insertions(+) create mode 100644 src/lib/Client/Tools/APT.py create mode 100644 src/lib/Client/Tools/Blast.py create mode 100644 src/lib/Client/Tools/Chkconfig.py create mode 100644 src/lib/Client/Tools/DebInit.py create mode 100644 src/lib/Client/Tools/Encap.py create mode 100644 src/lib/Client/Tools/POSIX.py create mode 100644 src/lib/Client/Tools/PostInstall.py create mode 100644 src/lib/Client/Tools/RPM.py create mode 100644 src/lib/Client/Tools/SMF.py create mode 100644 src/lib/Client/Tools/SYSV.py create mode 100644 src/lib/Client/Tools/__init__.py (limited to 'src/lib/Client/Tools') diff --git a/src/lib/Client/Tools/APT.py b/src/lib/Client/Tools/APT.py new file mode 100644 index 000000000..4c049310a --- /dev/null +++ b/src/lib/Client/Tools/APT.py @@ -0,0 +1,73 @@ +'''This is the bcfg2 support for apt-get''' +__revision__ = '$Revision$' + +import apt_pkg, os, re +import Bcfg2.Client.Tools + +class APT(Bcfg2.Client.Tools.PkgTool): + '''The Debian toolset implements package and service operations and inherits + the rest from Toolset.Toolset''' + __name__ = 'APT' + __execs__ = ['/usr/bin/debsums', '/usr/bin/apt-get', '/usr/bin/dpkg'] + __important__ = ["/etc/apt/sources.list", "/var/cache/debconf/config.dat", \ + "/var/cache/debconf/templates.dat", '/etc/passwd', '/etc/group', \ + '/etc/apt/apt.conf', '/etc/dpkg/dpkg.cfg'] + __handles__ = [('Package', 'deb')] + __req__ = {'Package': ['name', 'version']} + pkgtype = 'deb' + pkgtool = ('apt-get --reinstall -q=2 --force-yes -y install %s', + ('%s=%s', ['name', 'version'])) + + svcre = re.compile("/etc/.*/[SK]\d\d(?P\S+)") + + def __init__(self, logger, cfg, setup, states): + Bcfg2.Client.Tools.PkgTool.__init__(self, logger, cfg, setup, states) + self.cfg = cfg + os.environ["DEBIAN_FRONTEND"] = 'noninteractive' + if not self.setup['dryrun']: + if self.setup['kevlar']: + self.cmd.run("dpkg --force-confold --configure --pending") + self.cmd.run("apt-get clean") + self.cmd.run("apt-get -q=2 -y update") + self.installed = {} + self.RefreshPackages() + + def RefreshPackages(self): + '''Refresh memory hashes of packages''' + apt_pkg.init() + cache = apt_pkg.GetCache() + self.installed = {} + for pkg in cache.Packages: + if pkg.CurrentVer: + self.installed[pkg.Name] = pkg.CurrentVer.VerStr + + def VerifyPackage(self, entry, modlist): + '''Verify package for entry''' + if not entry.attrib.has_key('version'): + self.logger.info("Cannot verify unversioned package %s" % + (entry.attrib['name'])) + return False + if self.installed.has_key(entry.attrib['name']): + if self.installed[entry.attrib['name']] == entry.attrib['version']: + if not self.setup['quick']: + output = self.cmd.run("/usr/bin/debsums -s %s" % entry.get('name'))[1] + if [filename for filename in output if filename not in modlist]: + return False + return True + else: + entry.set('current_version', self.installed[entry.get('name')]) + return False + entry.set('current_exists', 'false') + return False + + def RemovePackages(self, packages): + '''Deal with extra configuration detected''' + if len(packages) > 0: + self.logger.info('Removing packages:') + self.logger.info(packages) + self.cmd.run("apt-get remove -y --force-yes %s" % \ + " ".join(packages)) + self.RefreshPackages() + self.extra = self.FindExtraPackages() + + diff --git a/src/lib/Client/Tools/Blast.py b/src/lib/Client/Tools/Blast.py new file mode 100644 index 000000000..6133ab2c1 --- /dev/null +++ b/src/lib/Client/Tools/Blast.py @@ -0,0 +1,22 @@ +# This is the bcfg2 support for blastwave packages (pkg-get) +'''This provides bcfg2 support for blastwave''' +__revision__ = '$Revision$' + +import Bcfg2.Client.Tools.SYSV + +class Blast(Bcfg2.Client.Tools.SYSV.SYSV): + '''Support for Blastwave packages''' + pkgtype = 'blast' + pkgtool = ("/opt/csw/bin/pkg-get install %s", ("%s", ["name"])) + __name__ = 'Blast' + __execs__ = ['/opt/csw/bin/pkg-get'] + __handles__ = [('Package', 'blast')] + + # VerifyPackage comes from Bcfg2.Client.Tools.SYSV + # Install comes from Bcfg2.Client.Tools.PkgTool + # Extra comes from Bcfg2.Client.Tools.Tool + # Remove comes from Bcfg2.Client.Tools.SYSV + + def FindExtraPackages(self): + '''Pass through to null FindExtra call''' + return [] diff --git a/src/lib/Client/Tools/Chkconfig.py b/src/lib/Client/Tools/Chkconfig.py new file mode 100644 index 000000000..a1c1160c9 --- /dev/null +++ b/src/lib/Client/Tools/Chkconfig.py @@ -0,0 +1,62 @@ +# This is the bcfg2 support for chkconfig +# $Id$ + +'''This is chkconfig support''' +__revision__ = '$Revision$' + +import Bcfg2.Client.Tools, Bcfg2.Client.XML + +class Chkconfig(Bcfg2.Client.Tools.SvcTool): + '''Chkconfig support for Bcfg2''' + __name__ = 'Chkconfig' + __execs__ = ['/sbin/chkconfig'] + __handles__ = [('Service', 'chkconfig')] + __req__ = {'Service': ['name', 'status']} + + def VerifyService(self, entry, _): + '''Verify Service status for entry''' + try: + srvdata = self.cmd.run('/sbin/chkconfig --list %s | grep -v "unknown service"' + % entry.attrib['name'])[1][0].split() + except IndexError: + # Ocurrs when no lines are returned (service not installed) + entry.set('current_status', 'off') + return False + if entry.attrib['type'] == 'xinetd': + return entry.attrib['status'] == srvdata[1] + + try: + onlevels = [level.split(':')[0] for level in srvdata[1:] if level.split(':')[1] == 'on'] + except IndexError: + onlevels = [] + + # chkconfig/init.d service + if entry.get('status') == 'on': + status = len(onlevels) > 0 + else: + status = len(onlevels) == 0 + + if not status: + if entry.get('status') == 'on': + entry.set('current_status', 'off') + else: + entry.set('current_status', 'on') + return status + + def InstallService(self, entry): + '''Install Service entry''' + self.cmd.run("/sbin/chkconfig --add %s"%(entry.attrib['name'])) + self.logger.info("Installing Service %s" % (entry.get('name'))) + return self.cmd.run("/sbin/chkconfig %s %s" % (entry.get('name'), + entry.get('status')))[0] == 0 + + def FindExtra(self): + '''Locate extra chkconfig Services''' + allsrv = [line.split()[0] for line in \ + self.cmd.run("/sbin/chkconfig --list|grep :on")[1]] + self.logger.debug('Found active services:') + self.logger.debug(allsrv) + specified = [srv.get('name') for srv in self.getSupportedEntries()] + return [Bcfg2.Client.XML.Element('Service', type='chkconfig', name=name) \ + for name in allsrv if name not in specified] + diff --git a/src/lib/Client/Tools/DebInit.py b/src/lib/Client/Tools/DebInit.py new file mode 100644 index 000000000..5cfabb4f0 --- /dev/null +++ b/src/lib/Client/Tools/DebInit.py @@ -0,0 +1,65 @@ +'''Debian Init Support for Bcfg2''' +__revision__ = '$Revision$' + +import glob, os, re +import Bcfg2.Client.Tools + +class DebInit(Bcfg2.Client.Tools.SvcTool): + '''Debian Service Support for Bcfg2''' + __name__ = 'DebInit' + __execs__ = ['/usr/sbin/update-rc.d'] + __handles__ = [('Service', 'deb')] + __req__ = {'Service': ['name', 'status']} + svcre = re.compile("/etc/.*/[SK]\d\d(?P\S+)") + + # implement entry (Verify|Install) ops + def VerifyService(self, entry, _): + '''Verify Service status for entry''' + rawfiles = glob.glob("/etc/rc*.d/*%s" % (entry.get('name'))) + files = [filename for filename in rawfiles if \ + self.svcre.match(filename).group('name') == entry.get('name')] + if entry.get('status') == 'off': + if files: + entry.set('current_status', 'on') + return False + else: + return True + else: + if files: + return True + else: + entry.set('current_status', 'off') + return False + + def InstallService(self, entry): + '''Install Service for entry''' + self.logger.info("Installing Service %s" % (entry.get('name'))) + try: + os.stat('/etc/init.d/%s' % entry.get('name')) + except OSError: + self.logger.debug("Init script for service %s does not exist" % entry.get('name')) + return False + + if entry.get('status') == 'off': + self.cmd.run("/etc/init.d/%s stop" % (entry.get('name'))) + cmdrc = self.cmd.run("/usr/sbin/update-rc.d -f %s remove" % entry.get('name'))[0] + else: + cmdrc = self.cmd.run("/usr/sbin/update-rc.d %s defaults" % \ + (entry.get('name')))[0] + return cmdrc == 0 + + def FindExtra(self): + '''Find Extra Debian Service Entries''' + specified = [entry.get('name') for entry in self.getSupportedEntries()] + extra = [self.svcre.match(fname).group('name') for fname in + glob.glob("/etc/rc[12345].d/S*") \ + if self.svcre.match(fname).group('name') not in specified] + return [Bcfg2.Client.XML.Element('Service', name=name, type='deb') for name \ + in extra] + + def Remove(self, entries): + '''Remove extra service entries''' + self.logger.info('Removing services:') + self.logger.info([entry.get('name') for entry in entries]) + for entry in entries: + self.cmd.run("rm -f /etc/rc*.d/S??%s" % (entry.get('name'))) diff --git a/src/lib/Client/Tools/Encap.py b/src/lib/Client/Tools/Encap.py new file mode 100644 index 000000000..af12ddcd0 --- /dev/null +++ b/src/lib/Client/Tools/Encap.py @@ -0,0 +1,60 @@ +# dclark: This is just stuff from Solaris.py munged together into what looked +# like the right places, before making anything actually work. + +'''Bcfg2 Support for Encap Packages''' + +__revision__ = '$Revision$' + +import Bcfg2.Client.Tools, glob, re + +class Encap(Bcfg2.Client.Tools.PkgTool): + '''Support for Encap packages''' + __name__ = 'Encap' + __execs__ = ['/usr/local/bin/epkg'] + __handles__ = [('Package', 'encap')] + __req__ = {'Package': ['version', 'url']} + pkgtype = 'encap' + pkgtool = ("/usr/local/bin/epkg -l -f -q %s", ("%s", ["url"])) + splitter = re.compile('.*/(?P[\w-]+)\-(?P[\w\.-]+)') + +# If you define self.pkgtool and self.pkgname it will [use] the Pkgtool.Install +# method will do the installation stuff for you + + def __init__(self, logger, setup, config, states): + Bcfg2.Client.Tools.PkgTool.__init__(self, logger, setup, config, states) + self.installed = {} + + def RefreshPackages(self): + '''Try to find encap packages''' + self.installed = {} + for pkg in glob.glob("/usr/local/encap/*"): + match = self.splitter.match(pkg) + if match: + self.installed[match.group('name')] = match.group('version') + else: + print "Failed to split name %s" % pkg + #print self.installed.keys() + + def VerifyPackage(self, entry, _): + '''Verify Package status for entry''' + if not entry.get('version'): + self.logger.info("Insufficient information of Package %s; cannot Verify" % entry.get('name')) + return False + cmdrc = self.cmd.run("/usr/local/bin/epkg -q -S -k %s-%s >/dev/null" % + (entry.get('name'), entry.get('version')))[0] + if cmdrc != 0: + self.logger.debug("Package %s version incorrect" % entry.get('name')) + else: + return True + return False + + # Can use the FindExtraPackages method from Bcfg2.Client.Tools.PkgTool + + def RemovePackages(self, packages): + '''Deal with extra configuration detected''' + names = " ".join([pkg.get('name') for pkg in packages]) + self.logger.info("Removing packages: %s" % (names)) + self.cmd.run("/usr/local/bin/epkg -l -q -r %s" % (names)) + self.RefreshPackages() + self.extra = self.FindExtraPackages() + diff --git a/src/lib/Client/Tools/POSIX.py b/src/lib/Client/Tools/POSIX.py new file mode 100644 index 000000000..c5d82a403 --- /dev/null +++ b/src/lib/Client/Tools/POSIX.py @@ -0,0 +1,243 @@ +'''All POSIX Type client support for Bcfg2''' +__revision__ = '$Revision$' + +from stat import S_ISVTX, S_ISGID, S_ISUID, S_IXUSR, S_IWUSR, S_IRUSR, S_IXGRP +from stat import S_IWGRP, S_IRGRP, S_IXOTH, S_IWOTH, S_IROTH, ST_MODE, S_ISDIR +from stat import S_IFREG, ST_UID, ST_GID, S_ISREG, S_IFDIR, S_ISLNK + +import binascii, difflib, grp, os, pwd, xml.sax.saxutils +import Bcfg2.Client.Tools + +def calcPerms(initial, perms): + '''This compares ondisk permissions with specified ones''' + pdisp = [{1:S_ISVTX, 2:S_ISGID, 4:S_ISUID}, {1:S_IXUSR, 2:S_IWUSR, 4:S_IRUSR}, + {1:S_IXGRP, 2:S_IWGRP, 4:S_IRGRP}, {1:S_IXOTH, 2:S_IWOTH, 4:S_IROTH}] + tempperms = initial + if len(perms) == 3: + perms = '0%s' % (perms) + pdigits = [int(perms[digit]) for digit in range(4)] + for index in range(4): + for (num, perm) in pdisp[index].iteritems(): + if pdigits[index] & num: + tempperms |= perm + return tempperms + +class POSIX(Bcfg2.Client.Tools.Tool): + '''POSIX File support code''' + __name__ = 'POSIX' + __handles__ = [('ConfigFile', None), ('Directory', None), ('Permissions', None), \ + ('SymLink', None)] + __req__ = {'ConfigFile': ['name', 'owner', 'group', 'perms'], + 'Directory': ['name', 'owner', 'group', 'perms'], + 'Permissions': ['name', 'owner', 'group', 'perms'], + 'SymLink': ['name', 'to']} + + def VerifySymLink(self, entry, _): + '''Verify SymLink Entry''' + try: + sloc = os.readlink(entry.get('name')) + if sloc == entry.get('to'): + return True + self.logger.debug("Symlink %s points to %s, should be %s" % \ + (entry.get('name'), sloc, entry.get('to'))) + entry.set('current_to', sloc) + return False + except OSError: + entry.set('current_exists', 'false') + return False + + def InstallSymLink(self, entry): + '''Install SymLink Entry''' + self.logger.info("Installing Symlink %s" % (entry.get('name'))) + try: + fmode = os.lstat(entry.get('name'))[ST_MODE] + if S_ISREG(fmode) or S_ISLNK(fmode): + self.logger.debug("Non-directory entry already exists at %s" % \ + (entry.get('name'))) + os.unlink(entry.get('name')) + elif S_ISDIR(fmode): + self.logger.debug("Directory entry already exists at %s" % (entry.get('name'))) + self.cmd.run("mv %s/ %s.bak" % (entry.get('name'), entry.get('name'))) + else: + os.unlink(entry.get('name')) + except OSError: + self.logger.info("Symlink %s cleanup failed" % (entry.get('name'))) + try: + os.symlink(entry.get('to'), entry.get('name')) + return True + except OSError: + return False + + def VerifyDirectory(self, entry, _): + '''Verify Directory Entry''' + while len(entry.get('perms', '')) < 4: + entry.set('perms', '0' + entry.get('perms', '')) + try: + ondisk = os.stat(entry.get('name')) + except OSError: + entry.set('current_exists', 'false') + self.logger.debug("%s %s does not exist" % + (entry.tag, entry.get('name'))) + return False + try: + owner = pwd.getpwuid(ondisk[ST_UID])[0] + group = grp.getgrgid(ondisk[ST_GID])[0] + except (OSError, KeyError): + self.logger.error('User resolution failing') + owner = 'root' + group = 'root' + perms = oct(os.stat(entry.get('name'))[ST_MODE])[-4:] + if ((owner == entry.get('owner')) and + (group == entry.get('group')) and + (perms == entry.get('perms'))): + return True + else: + if owner != entry.get('owner'): + entry.set('current_owner', owner) + self.logger.debug("%s %s ownership wrong" % (entry.tag, entry.get('name'))) + if group != entry.get('group'): + entry.set('current_group', group) + self.logger.debug("%s %s group wrong" % (entry.tag, entry.get('name'))) + if perms != entry.get('perms'): + entry.set('current_perms', perms) + self.logger.debug("%s %s permissions wrong: are %s should be %s" % + (entry.tag, entry.get('name'), perms, entry.get('perms'))) + return False + + def InstallDirectory(self, entry): + '''Install Directory Entry''' + self.logger.info("Installing Directory %s" % (entry.get('name'))) + try: + fmode = os.lstat(entry.get('name')) + if not S_ISDIR(fmode[ST_MODE]): + self.logger.debug("Found a non-directory entry at %s" % (entry.get('name'))) + try: + os.unlink(entry.get('name')) + except OSError: + self.logger.info("Failed to unlink %s" % (entry.get('name'))) + return False + else: + self.logger.debug("Found a pre-existing directory at %s" % (entry.get('name'))) + exists = True + except OSError: + # stat failed + exists = False + + if not exists: + parent = "/".join(entry.get('name').split('/')[:-1]) + if parent: + try: + os.lstat(parent) + except: + self.logger.debug('Creating parent path for directory %s' % (entry.get('name'))) + for idx in xrange(len(parent.split('/')[:-1])): + current = '/'+'/'.join(parent.split('/')[1:2+idx]) + try: + sloc = os.lstat(current) + try: + if not S_ISDIR(sloc[ST_MODE]): + os.unlink(current) + os.mkdir(current) + except OSError: + return False + except OSError: + try: + os.mkdir(current) + except OSError: + return False + + try: + os.mkdir(entry.get('name')) + except OSError: + self.logger.error('Failed to create directory %s' % (entry.get('name'))) + return False + try: + os.chown(entry.get('name'), + pwd.getpwnam(entry.get('owner'))[2], grp.getgrnam(entry.get('group'))[2]) + os.chmod(entry.get('name'), calcPerms(S_IFDIR, entry.get('perms'))) + return True + except (OSError, KeyError): + self.logger.error('Permission fixup failed for %s' % (entry.get('name'))) + return False + + def VerifyConfigFile(self, entry, _): + '''Install ConfigFile Entry''' + # configfile verify is permissions check + content check + permissionStatus = self.VerifyDirectory(entry, _) + if entry.get('encoding', 'ascii') == 'base64': + tempdata = binascii.a2b_base64(entry.text) + elif entry.get('empty', 'false') == 'true': + tempdata = '' + else: + if entry.text == None: + self.logger.error("Cannot verify incomplete ConfigFile %s" % (entry.get('name'))) + return False + tempdata = entry.text + + try: + content = open(entry.get('name')).read() + except IOError: + # file does not exist + return False + contentStatus = content == tempdata + if not contentStatus: + diff = '\n'.join([x for x in difflib.unified_diff(content.split('\n'), tempdata.split('\n'))]) + try: + entry.set("current_diff", xml.sax.saxutils.quoteattr(diff)) + except: + pass + return contentStatus and permissionStatus + + def InstallConfigFile(self, entry): + '''Install ConfigFile Entry''' + self.logger.info("Installing ConfigFile %s" % (entry.get('name'))) + + parent = "/".join(entry.get('name').split('/')[:-1]) + if parent: + try: + os.lstat(parent) + except: + self.logger.debug('Creating parent path for config file %s' % (entry.get('name'))) + for idx in xrange(len(parent.split('/')[:-1])): + current = '/'+'/'.join(parent.split('/')[1:2+idx]) + try: + sloc = os.lstat(current) + try: + if not S_ISDIR(sloc[ST_MODE]): + os.unlink(current) + os.mkdir(current) + except OSError: + return False + except OSError: + try: + os.mkdir(current) + except OSError: + return False + + # If we get here, then the parent directory should exist + try: + newfile = open("%s.new"%(entry.get('name')), 'w') + if entry.get('encoding', 'ascii') == 'base64': + filedata = binascii.a2b_base64(entry.text) + elif entry.get('empty', 'false') == 'true': + filedata = '' + else: + filedata = entry.text + newfile.write(filedata) + newfile.close() + try: + os.chown(newfile.name, pwd.getpwnam(entry.get('owner'))[2], + grp.getgrnam(entry.get('group'))[2]) + except KeyError: + os.chown(newfile.name, 0, 0) + os.chmod(newfile.name, calcPerms(S_IFREG, entry.get('perms'))) + if entry.get("paranoid", False) and self.setup.get("paranoid", False): + self.cmd.run("cp %s /var/cache/bcfg2/%s" % (entry.get('name'))) + os.rename(newfile.name, entry.get('name')) + return True + except (OSError, IOError), err: + if err.errno == 13: + self.logger.info("Failed to open %s for writing" % (entry.get('name'))) + else: + print err + return False diff --git a/src/lib/Client/Tools/PostInstall.py b/src/lib/Client/Tools/PostInstall.py new file mode 100644 index 000000000..34627cacf --- /dev/null +++ b/src/lib/Client/Tools/PostInstall.py @@ -0,0 +1,21 @@ +'''PostInstall Support''' +__revision__ = '$Revision$' + +import Bcfg2.Client.Tools + +class PostInstall(Bcfg2.Client.Tools.Tool): + '''Implement PostInstalls''' + __name__ = 'PostInstall' + __handles__ = [('PostInstall', None)] + __req__ = {'PostInstall': ['name']} + + def VerifyPostInstall(self, dummy, _): + '''PostInstalls always verify true''' + return True + + def BundleUpdated(self, bundle): + '''Run postinstalls when bundles have been updated''' + for entry in bundle: + if entry.tag == 'PostInstall': + self.cmd.run(entry.get('name')) + diff --git a/src/lib/Client/Tools/RPM.py b/src/lib/Client/Tools/RPM.py new file mode 100644 index 000000000..661c0a167 --- /dev/null +++ b/src/lib/Client/Tools/RPM.py @@ -0,0 +1,82 @@ +'''Bcfg2 Support for RPMS''' + +__revision__ = '$Revision$' + +import Bcfg2.Client.Tools, time + +class RPM(Bcfg2.Client.Tools.PkgTool): + '''Support for RPM packages''' + __name__ = 'RPM' + __execs__ = ['/bin/rpm'] + __handles__ = [('Package', 'rpm')] + __req__ = {'Package': ['name', 'version', 'url']} + pkgtype = 'rpm' + pkgtool = ("rpm --oldpackage --replacepkgs --quiet -U %s", ("%s", ["url"])) + + def RefreshPackages(self, level=0): + '''Cache package states''' + self.installed = {} + if level > 5: + return + for line in self.cmd.run("rpm -qa --qf '%{NAME} %{VERSION}-%{RELEASE}\n'")[1]: + try: + (name, version) = line.split() + self.installed[name] = version + except ValueError: + self.logger.error("Failed to access RPM db; retrying after 30s") + time.sleep(30) + return self.RefreshPackages(level + 1) + + def VerifyPackage(self, entry, modlist): + '''Verify Package status for entry''' + if not entry.get('version'): + self.logger.error("Can't install package %s, not enough data." % (entry.get('name'))) + return False + rpm_options = [] + if entry.get('verify', 'false') == 'nomtime': + self.logger.debug("Skipping mtime verification for package %s" % (entry.get('name'))) + rpm_options.append("--nomtime") + if self.installed.has_key(entry.get('name')): + if entry.get('version') == self.installed[entry.get('name')]: + if entry.get('multiarch'): + archs = entry.get('multiarch').split() + info = self.cmd.run('rpm -q %s --qf "%%{NAME} %%{VERSION}-%%{RELEASE} %%{ARCH}\n"' % (entry.get('name')))[1] + while info: + arch = info.pop().split()[2] + if arch in archs: + archs.remove(arch) + else: + self.logger.error("Got pkg install for Package %s: arch %s" % (entry.get('name'), arch)) + return False + if archs: + self.logger.error("Package %s not installed for arch: %s" % (entry.get('name'), archs)) + return False + if (self.setup['quick'] or (entry.get('verify', 'true') == 'false')) or entry.get('multiarch'): + if entry.get('verify', 'true') == 'false': + self.logger.debug("Skipping checksum verification for package %s" % (entry.get('name'))) + return True + else: + self.logger.debug("Package %s: wrong version installed. want %s have %s" % + (entry.get('name'), entry.get('version'), self.installed[entry.get('name')])) + entry.set('current_version', self.installed[entry.get('name')]) + return False + else: + self.logger.debug("Package %s: not installed" % (entry.get('name'))) + entry.set('current_exists', 'false') + return False + + (vstat, output) = self.cmd.run("rpm --verify -q %s %s-%s" % (" ".join(rpm_options), entry.get('name'), entry.get('version'))) + if vstat != 0: + if [name for name in output if name.split()[-1] not in modlist]: + self.logger.debug("Package %s content verification failed" % entry.get('name')) + return False + return True + + def RemovePackages(self, entries): + '''Remove specified entries''' + pkgnames = " ".join([entry[2] for entry in entries]) + if len(pkgnames) > 0: + self.logger.info("Removing packages: %s" % pkgnames) + self.cmd.run("rpm --quiet -e --allmatches %s" % " ".join(pkgnames)) + self.RefreshPackages() + self.extra = self.FindExtraPackages() diff --git a/src/lib/Client/Tools/SMF.py b/src/lib/Client/Tools/SMF.py new file mode 100644 index 000000000..321c3cc17 --- /dev/null +++ b/src/lib/Client/Tools/SMF.py @@ -0,0 +1,123 @@ +'''SMF support for Bcfg2''' +__revision__ = '$Revision$' + +import glob, os +import Bcfg2.Client.Tools + +class SMF(Bcfg2.Client.Tools.Tool): + '''Support for Solaris SMF Services''' + __handles__ = [('Service', 'smf')] + __execs__ = ['/usr/sbin/svcadm', '/usr/bin/svcs'] + __name__ = 'SMF' + __req__ = {'Service':['name', 'status']} + __ireq__ = {'Service': ['name', 'status', 'FMRI']} + + def GetFMRI(self, entry): + '''Perform FMRI resolution for service''' + if not entry.attrib.has_key('FMRI'): + name = self.cmd.run("/usr/bin/svcs -H -o FMRI %s 2>/dev/null" % \ + entry.get('name'))[1] + if name: + entry.set('FMRI', name[0]) + return True + else: + self.logger.info('Failed to locate FMRI for service %s' % entry.get('name')) + return False + + def VerifyService(self, entry, _): + '''Verify SMF Service Entry''' + if not self.GetFMRI(entry): + return False + if entry.get('FMRI').startswith('lrc'): + filename = entry.get('FMRI').split('/')[-1] + # this is a legacy service + gname = "/etc/rc*.d/%s" % filename + files = glob.glob(gname.replace('_', '.')) + if files: + self.logger.debug("Matched %s with %s" % \ + (entry.get("FMRI"), ":".join(files))) + return entry.get('status') == 'on' + else: + self.logger.debug("No service matching %s" % (entry.get("FMRI"))) + return entry.get('status') == 'off' + try: + srvdata = self.cmd.run("/usr/bin/svcs -H -o STA %s" % entry.attrib['name'])[1][0].split() + except IndexError: + # Ocurrs when no lines are returned (service not installed) + return False + + if entry.get('status') == 'on': + return srvdata[0] == 'ON' + else: + return srvdata[0] in ['OFF', 'UN', 'MNT', 'DIS', 'DGD'] + + def InstallService(self, entry): + '''Install SMF Service Entry''' + self.logger.info("Installing Service %s" % (entry.get('name'))) + if entry.get('status') == 'off': + if entry.get("FMRI").startswith('lrc'): + try: + loc = entry.get("FMRI")[4:].replace('_', '.') + self.logger.debug("Renaming file %s to %s" % \ + (loc, loc.replace('/S', '/DISABLED.S'))) + os.rename(loc, loc.replace('/S', '/DISABLED.S')) + return True + except OSError: + self.logger.error("Failed to rename init script %s" % (loc)) + return False + else: + cmdrc = self.cmd.run("/usr/sbin/svcadm disable %s" % \ + (entry.get('FMRI')))[0] + else: + if entry.get('FMRI').startswith('lrc'): + loc = entry.get("FMRI")[4:].replace('_', ',') + try: + os.stat(loc.replace('/S', '/Disabled.')) + self.logger.debug("Renaming file %s to %s" % \ + (loc.replace('/S', '/DISABLED.S'), loc)) + os.rename(loc.replace('/S', '/DISABLED.S'), loc) + cmdrc = 0 + except OSError: + self.logger.debug("Failed to rename %s to %s" \ + % (loc.replace('/S', '/DISABLED.S'), loc)) + cmdrc = 1 + else: + cmdrc = self.cmd.run("/usr/sbin/svcadm enable -r %s" % \ + (entry.get('FMRI')))[0] + return cmdrc == 0 + + def Remove(self, svcs): + '''Remove Extra SMF entries''' + pass + + def FindExtra(self): + '''Find Extra SMF Services''' + allsrv = [name for name, version in \ + [ srvc.strip().split() for srvc in + self.cmd.run("/usr/bin/svcs -a -H -o FMRI,STATE")[1] ] + if version != 'disabled'] + + for svc in self.getSupportedEntries(): + name = self.cmd.run("/usr/bin/svcs -H -o FMRI %s 2>/dev/null" % \ + svc.get('name'))[1] + if name: + svc.set('FMRI', name[0]) + if name in allsrv: + allsrv.remove(name) + else: + self.logger.info("Failed to locate FMRI for service %s" % svc.get('name')) + + return [Bcfg2.Client.XML.Element("Service", type='smf', name=name) for name in allsrv] + + def BundleUpdated(self, bundle): + '''Restart smf services''' + for entry in [entry for entry in bundle if self.handlesEntry(entry)]: + if not self.canInstall(entry): + self.logger.error("Insufficient information to restart service %s" % (entry.get('name'))) + else: + if entry.get('status') == 'on': + self.logger.info("Restarting smf service %s" % (entry.get("FMRI"))) + self.cmd.run("svcadm restart %s" % (entry.get("FMRI"))) + else: + self.cmd.run("svcadm disable %s" % (entry.get("FMRI"))) + diff --git a/src/lib/Client/Tools/SYSV.py b/src/lib/Client/Tools/SYSV.py new file mode 100644 index 000000000..ad9bb00c5 --- /dev/null +++ b/src/lib/Client/Tools/SYSV.py @@ -0,0 +1,83 @@ +# This is the bcfg2 support for solaris sysv packages +'''This provides bcfg2 support for Solaris SYSV packages''' +__revision__ = '$Revision$' + +import tempfile, Bcfg2.Client.Tools, Bcfg2.Client.XML + + +noask = ''' +mail= +instance=overwrite +partial=nocheck +runlevel=nocheck +idepend=nocheck +rdepend=nocheck +space=ask +setuid=nocheck +conflict=nocheck +action=nocheck +basedir=default +''' + +class SYSV(Bcfg2.Client.Tools.PkgTool): + '''Solaris SYSV package support''' + __execs__ = ["/usr/sbin/pkgadd"] + __handles__ = [('Package', 'sysv')] + __req__ = {'Package': ['name', 'version']} + __name__ = 'SYSV' + pkgtype = 'sysv' + pkgtool = ("/usr/sbin/pkgadd %s -d %%s -n %%%%s", (("%s", ["name"]))) + + def __init__(self, logger, setup, config, states): + Bcfg2.Client.Tools.PkgTool.__init__(self, logger, setup, config, states) + self.noaskname = tempfile.mktemp() + try: + open(self.noaskname, 'w+').write(noask) + self.pkgtool = (self.pkgtool[0] % ("-a %s" % (self.noaskname)), \ + self.pkgtool[1]) + except: + self.pkgtool = (self.pkgtool[0] % (""), self.pkgtool[1]) + + def RefreshPackages(self): + '''Refresh memory hashes of packages''' + self.installed = {} + # Build list of packages + lines = self.cmd.run("/usr/bin/pkginfo -x")[1] + while lines: + version = lines.pop().split()[1] + pkg = lines.pop().split()[0] + self.installed[pkg] = version + + def VerifyPackage(self, entry, modlist): + '''Verify Package status for entry''' + if not entry.get('version'): + self.logger.info("Insufficient information of Package %s; cannot Verify" % entry.get('name')) + return False + cmdrc = self.cmd.run("/usr/bin/pkginfo -q -v \"%s\" %s" % \ + (entry.get('version'), entry.get('name')))[0] + + if cmdrc != 0: + self.logger.debug("Package %s version incorrect" % entry.get('name')) + else: + if self.setup['quick'] or entry.attrib.get('verify', 'true') == 'false': + return True + (vstat, odata) = self.cmd.run("/usr/sbin/pkgchk -n %s" % (entry.get('name'))) + if vstat == 0: + return True + else: + output = [line for line in odata if line[:5] == 'ERROR'] + if len([name for name in output if name.split()[-1] not in modlist]): + self.logger.debug("Package %s content verification failed" % \ + (entry.get('name'))) + else: + return True + return False + + def RemovePackages(self, packages): + '''Remove specified Sysv packages''' + names = [pkg.get('name') for pkg in packages] + self.logger.info("Removing packages: %s" % (names)) + self.cmd.run("/usr/sbin/pkgrm -a %s -n %s" % \ + (self.noaskname, names)) + self.RefreshPackages() + self.extra = self.FindExtraPackages() diff --git a/src/lib/Client/Tools/__init__.py b/src/lib/Client/Tools/__init__.py new file mode 100644 index 000000000..da9f47eeb --- /dev/null +++ b/src/lib/Client/Tools/__init__.py @@ -0,0 +1,252 @@ +'''This contains all Bcfg2 Tool modules''' +__revision__ = '$Revision$' + +__all__ = ["APT", "Blast", "Chkconfig", "DebInit", "Launchd", "PostInstall", + "POSIX", "RPM", "SMF", "SYSV"] + +import os, popen2, stat, sys, Bcfg2.Client.XML + +class toolInstantiationError(Exception): + '''This error is called if the toolset cannot be instantiated''' + pass + +class readonlypipe(popen2.Popen4): + '''This pipe sets up stdin --> /dev/null''' + def __init__(self, cmd, bufsize=-1): + popen2._cleanup() + c2pread, c2pwrite = os.pipe() + null = open('/dev/null', 'w+') + self.pid = os.fork() + if self.pid == 0: + # Child + os.dup2(null.fileno(), sys.__stdin__.fileno()) + #os.dup2(p2cread, 0) + os.dup2(c2pwrite, 1) + os.dup2(c2pwrite, 2) + self._run_child(cmd) + os.close(c2pwrite) + self.fromchild = os.fdopen(c2pread, 'r', bufsize) + popen2._active.append(self) + +class executor: + '''this class runs stuff for us''' + def __init__(self, logger): + self.logger = logger + + def run(self, command): + '''Run a command in a pipe dealing with stdout buffer overloads''' + self.logger.debug('> %s' % command) + + runpipe = readonlypipe(command, bufsize=16384) + output = '' + cmdstat = -1 + while cmdstat == -1: + runpipe.fromchild.flush() + moreOutput = runpipe.fromchild.read() + if len(moreOutput) > 0: + self.logger.debug('< %s' % moreOutput) + output += moreOutput + cmdstat = runpipe.poll() + + return (cmdstat, [line for line in output.split('\n') if line]) + +class Tool: + '''All tools subclass this. It defines all interfaces that need to be defined''' + __name__ = 'Tool' + __execs__ = [] + __handles__ = [] + __req__ = {} + __important__ = [] + + def __init__(self, logger, setup, config, states): + self.setup = setup + self.logger = logger + if not hasattr(self, '__ireq__'): + self.__ireq__ = self.__req__ + self.config = config + self.cmd = executor(logger) + self.states = states + self.modified = [] + self.extra = [] + self.handled = [entry for struct in self.config for entry in struct \ + if self.handlesEntry(entry)] + for filename in self.__execs__: + try: + mode = stat.S_IMODE(os.stat(filename)[stat.ST_MODE]) + if mode & stat.S_IEXEC != stat.S_IEXEC: + self.logger.debug("%s: %s not executable" % \ + (self.__name__, filename)) + raise toolInstantiationError + except OSError: + raise toolInstantiationError + except: + self.logger.debug("%s failed" % filename, exc_info=1) + raise toolInstantiationError + + def BundleUpdated(self, _): + '''This callback is used when bundle updates occur''' + pass + + def Inventory(self, structures=[]): + '''Dispatch verify calls to underlying methods''' + if not structures: + structures = self.config.getchildren() + for (struct, entry) in [(struct, entry) for struct in structures \ + for entry in struct.getchildren() \ + if self.canVerify(entry)]: + try: + func = getattr(self, "Verify%s" % (entry.tag)) + self.states[entry] = func(entry, self.buildModlist(entry, struct)) + except: + self.logger.error( + "Unexpected failure of verification method for entry type %s" \ + % (entry.tag), exc_info=1) + self.extra = self.FindExtra() + + def Install(self, entries): + '''Install all entries in sublist''' + for entry in entries: + try: + func = getattr(self, "Install%s" % (entry.tag)) + self.states[entry] = func(entry) + self.modified.append(entry) + except: + self.logger.error("Unexpected failure of install method for entry type %s" \ + % (entry.tag), exc_info=1) + + def Remove(self, entries): + '''Remove specified extra entries''' + pass + + def getSupportedEntries(self): + '''return a list of supported entries''' + return [entry for struct in self.config.getchildren() for entry in struct.getchildren() \ + if self.handlesEntry(entry)] + + def handlesEntry(self, entry): + '''return if entry is handled by this Tool''' + return (entry.tag, entry.get('type')) in self.__handles__ + + def buildModlist(self, entry, struct): + '''Build a list of potentially modified POSIX paths for this entry''' + if entry.tag != 'Package' or struct.tag != 'Bundle': + return [] + return [sentry.get('name') for sentry in struct if sentry.tag in \ + ['ConfigFile', 'SymLink', 'Directory']] + + def canVerify(self, entry): + '''test if entry has enough information to be verified''' + if not self.handlesEntry(entry): + return False + + if [attr for attr in self.__req__[entry.tag] if attr not in entry.attrib]: + self.logger.error("Incomplete information for entry %s:%s; cannot verify" \ + % (entry.tag, entry.get('name'))) + return False + return True + + + def FindExtra(self): + '''Return a list of extra entries''' + return [] + + def canInstall(self, entry): + '''test if entry has enough information to be installed''' + if not self.handlesEntry(entry): + return False + if [attr for attr in self.__ireq__[entry.tag] if attr not in entry.attrib]: + self.logger.error("Incomplete information for entry %s:%s; cannot install" \ + % (entry.tag, entry.get('name'))) + return False + return True + +class PkgTool(Tool): + '''PkgTool provides a one-pass install with fallback for use with packaging systems''' + pkgtool = ('echo %s', ('%s', ['name'])) + pkgtype = 'echo' + __name__ = 'PkgTool' + + def __init__(self, logger, setup, config, states): + Tool.__init__(self, logger, setup, config, states) + self.installed = {} + self.Remove = self.RemovePackages + self.FindExtra = self.FindExtraPackages + + def VerifyPackage(self, dummy, _): + '''Dummy verification method''' + return False + + def Install(self, packages): + '''Run a one-pass install, followed by single pkg installs in case of failure''' + self.logger.info("Trying single pass package install for pkgtype %s" % \ + self.pkgtype) + + data = [tuple([pkg.get(field) for field in self.pkgtool[1][1]]) for pkg in packages] + pkgargs = " ".join([self.pkgtool[1][0] % datum for datum in data]) + + self.logger.debug("Installing packages: :%s:" % pkgargs) + self.logger.debug("Running command ::%s::" % (self.pkgtool[0] % pkgargs)) + + cmdrc = self.cmd.run(self.pkgtool[0] % pkgargs)[0] + if cmdrc == 0: + self.logger.info("Single Pass Succeded") + # set all package states to true and flush workqueues + pkgnames = [pkg.get('name') for pkg in packages] + for entry in [entry for entry in self.states.keys() + if entry.tag == 'Package' and entry.get('type') == self.pkgtype + and entry.get('name') in pkgnames]: + self.logger.debug('Setting state to true for pkg %s' % (entry.get('name'))) + self.states[entry] = True + self.RefreshPackages() + else: + self.logger.error("Single Pass Failed") + # do single pass installs + self.RefreshPackages() + for pkg in packages: + # handle state tracking updates + if self.VerifyPackage(pkg, []): + self.logger.info("Forcing state to true for pkg %s" % (pkg.get('name'))) + self.states[pkg] = True + else: + self.logger.info("Installing pkg %s version %s" % + (pkg.get('name'), pkg.get('version'))) + cmdrc = self.cmd.run(self.pkgtool[0] % + (self.pkgtool[1][0] % + tuple([pkg.get(field) for field in self.pkgtool[1][1]]))) + if cmdrc[0] == 0: + self.states[pkg] = True + else: + self.logger.error("Failed to install package %s" % (pkg.get('name'))) + for entry in [ent for ent in packages if self.states[ent]]: + self.modified.append(entry) + + def RefreshPackages(self): + '''Dummy state refresh method''' + pass + + def RemovePackages(self, packages): + '''Dummy implementation of package removal method''' + pass + + def FindExtraPackages(self): + '''Find extra packages''' + packages = [entry.get('name') for entry in self.getSupportedEntries()] + extras = [key for key in self.installed if key not in packages] + return [Bcfg2.Client.XML.Element('Package', name=name, type=self.pkgtype) \ + for name in extras] + +class SvcTool(Tool): + '''This class defines basic Service behavior''' + __name__ = 'SvcTool' + + def BundleUpdated(self, bundle): + '''The Bundle has been updated''' + for entry in bundle: + if self.handlesEntry(entry): + if entry.get('status') == 'on': + self.logger.debug('Restarting service %s' % entry.get('name')) + self.cmd.run('/etc/init.d/%s %s' % \ + (entry.get('name'), entry.get('reload', 'reload'))) + else: + self.logger.debug('Stopping service %s' % entry.get('name')) + self.cmd.run('/etc/init.d/%s stop' % (entry.get('name'))) -- cgit v1.2.3-1-g7c22