diff options
Diffstat (limited to 'src/lib/Bcfg2/Client/Tools')
28 files changed, 2320 insertions, 1306 deletions
diff --git a/src/lib/Bcfg2/Client/Tools/APK.py b/src/lib/Bcfg2/Client/Tools/APK.py index aaaf2472f..d70916792 100644 --- a/src/lib/Bcfg2/Client/Tools/APK.py +++ b/src/lib/Bcfg2/Client/Tools/APK.py @@ -24,8 +24,8 @@ class APK(Bcfg2.Client.Tools.PkgTool): for pkg in zip(names, nameversions): pkgname = pkg[0] version = pkg[1][len(pkgname) + 1:] - self.logger.debug(" pkgname: %s\n version: %s" % - (pkgname, version)) + self.logger.debug(" pkgname: %s" % pkgname) + self.logger.debug(" version: %s" % version) self.installed[pkgname] = version def VerifyPackage(self, entry, modlist): diff --git a/src/lib/Bcfg2/Client/Tools/APT.py b/src/lib/Bcfg2/Client/Tools/APT.py index 6b839ffbc..ce7e9701f 100644 --- a/src/lib/Bcfg2/Client/Tools/APT.py +++ b/src/lib/Bcfg2/Client/Tools/APT.py @@ -6,22 +6,7 @@ warnings.filterwarnings("ignore", "apt API not stable yet", FutureWarning) import apt.cache import os - import Bcfg2.Client.Tools -import Bcfg2.Options - -# Options for tool locations -opts = {'install_path': Bcfg2.Options.CLIENT_APT_TOOLS_INSTALL_PATH, - 'var_path': Bcfg2.Options.CLIENT_APT_TOOLS_VAR_PATH, - 'etc_path': Bcfg2.Options.CLIENT_SYSTEM_ETC_PATH} -setup = Bcfg2.Options.OptionParser(opts) -setup.parse([]) -install_path = setup['install_path'] -var_path = setup['var_path'] -etc_path = setup['etc_path'] -DEBSUMS = '%s/bin/debsums' % install_path -APTGET = '%s/bin/apt-get' % install_path -DPKG = '%s/bin/dpkg' % install_path class APT(Bcfg2.Client.Tools.Tool): """The Debian toolset implements package and service operations and inherits @@ -29,18 +14,26 @@ class APT(Bcfg2.Client.Tools.Tool): """ name = 'APT' - __execs__ = [DEBSUMS, APTGET, DPKG] + __execs__ = [] __handles__ = [('Package', 'deb'), ('Path', 'ignore')] __req__ = {'Package': ['name', 'version'], 'Path': ['type']} def __init__(self, logger, setup, config): Bcfg2.Client.Tools.Tool.__init__(self, logger, setup, config) + + self.install_path = setup.get('apt_install_path', '/usr') + self.var_path = setup.get('apt_var_path', '/var') + self.etc_path = setup.get('apt_etc_path', '/etc') + self.debsums = '%s/bin/debsums' % self.install_path + self.aptget = '%s/bin/apt-get' % self.install_path + self.dpkg = '%s/bin/dpkg' % self.install_path + self.__execs__ = [self.debsums, self.aptget, self.dpkg] + path_entries = os.environ['PATH'].split(':') for reqdir in ['/sbin', '/usr/sbin']: if reqdir not in path_entries: os.environ['PATH'] = os.environ['PATH'] + ':' + reqdir - self.pkgcmd = '%s ' % APTGET + \ - '-o DPkg::Options::=--force-overwrite ' + \ + self.pkgcmd = '%s ' % self.aptget + \ '-o DPkg::Options::=--force-confold ' + \ '-o DPkg::Options::=--force-confmiss ' + \ '--reinstall ' + \ @@ -53,21 +46,21 @@ class APT(Bcfg2.Client.Tools.Tool): if entry.tag == 'Path' and \ entry.get('type') == 'ignore'] self.__important__ = self.__important__ + \ - ["%s/cache/debconf/config.dat" % var_path, - "%s/cache/debconf/templates.dat" % var_path, + ["%s/cache/debconf/config.dat" % self.var_path, + "%s/cache/debconf/templates.dat" % self.var_path, '/etc/passwd', '/etc/group', - '%s/apt/apt.conf' % etc_path, - '%s/dpkg/dpkg.cfg' % etc_path] + \ + '%s/apt/apt.conf' % self.etc_path, + '%s/dpkg/dpkg.cfg' % self.etc_path] + \ [entry.get('name') for struct in config for entry in struct \ if entry.tag == 'Path' and \ - entry.get('name').startswith('%s/apt/sources.list' % etc_path)] + entry.get('name').startswith('%s/apt/sources.list' % self.etc_path)] self.nonexistent = [entry.get('name') for struct in config for entry in struct \ if entry.tag == 'Path' and entry.get('type') == 'nonexistent'] os.environ["DEBIAN_FRONTEND"] = 'noninteractive' self.actions = {} if self.setup['kevlar'] and not self.setup['dryrun']: - self.cmd.run("%s --force-confold --configure --pending" % DPKG) - self.cmd.run("%s clean" % APTGET) + self.cmd.run("%s --force-confold --configure --pending" % self.dpkg) + self.cmd.run("%s clean" % self.aptget) try: self.pkg_cache = apt.cache.Cache() except SystemError: @@ -95,7 +88,8 @@ class APT(Bcfg2.Client.Tools.Tool): for (name, version) in extras] def VerifyDebsums(self, entry, modlist): - output = self.cmd.run("%s -as %s" % (DEBSUMS, entry.get('name')))[1] + output = self.cmd.run("%s -as %s" % (self.debsums, + entry.get('name')))[1] if len(output) == 1 and "no md5sums for" in output[0]: self.logger.info("Package %s has no md5sums. Cannot verify" % \ entry.get('name')) diff --git a/src/lib/Bcfg2/Client/Tools/Action.py b/src/lib/Bcfg2/Client/Tools/Action.py index dc49347e9..52d4e6a3f 100644 --- a/src/lib/Bcfg2/Client/Tools/Action.py +++ b/src/lib/Bcfg2/Client/Tools/Action.py @@ -2,6 +2,7 @@ import Bcfg2.Client.Tools from Bcfg2.Client.Frame import matches_white_list, passes_black_list +from Bcfg2.Bcfg2Py3k import input """ <Action timing='pre|post|both' @@ -44,11 +45,7 @@ class Action(Bcfg2.Client.Tools.Tool): if self.setup['interactive']: prompt = ('Run Action %s, %s: (y/N): ' % (entry.get('name'), entry.get('command'))) - # py3k compatibility - try: - ans = raw_input(prompt) - except NameError: - ans = input(prompt) + ans = input(prompt) if ans not in ['y', 'Y']: return False if self.setup['servicemode'] == 'build': @@ -64,7 +61,7 @@ class Action(Bcfg2.Client.Tools.Tool): else: return rc == 0 else: - self.logger.debug("In dryrun mode: not running action:\n %s" % + self.logger.debug("In dryrun mode: not running action: %s" % (entry.get('name'))) return False diff --git a/src/lib/Bcfg2/Client/Tools/Chkconfig.py b/src/lib/Bcfg2/Client/Tools/Chkconfig.py index 12ea5f132..0169b12da 100644 --- a/src/lib/Bcfg2/Client/Tools/Chkconfig.py +++ b/src/lib/Bcfg2/Client/Tools/Chkconfig.py @@ -45,30 +45,14 @@ class Chkconfig(Bcfg2.Client.Tools.SvcTool): except IndexError: onlevels = [] + pstatus = self.check_service(entry) if entry.get('status') == 'on': - status = (len(onlevels) > 0) + status = (len(onlevels) > 0 and pstatus) command = 'start' else: - status = (len(onlevels) == 0) + status = (len(onlevels) == 0 and not pstatus) command = 'stop' - if entry.get('mode', 'default') == 'supervised': - # turn on or off the service in supervised mode - pstatus = self.cmd.run('/sbin/service %s status' % \ - entry.get('name'))[0] - needs_modification = ((command == 'start' and pstatus) or \ - (command == 'stop' and not pstatus)) - if (not self.setup.get('dryrun') and - self.setup['servicemode'] != 'disabled' and - needs_modification): - self.cmd.run(self.get_svc_command(entry, command)) - # service was modified, so it failed - pstatus = False - - # chkconfig/init.d service - if entry.get('status') == 'on': - status = status and not pstatus - if not status: if entry.get('status') == 'on': entry.set('current_status', 'off') @@ -78,22 +62,22 @@ class Chkconfig(Bcfg2.Client.Tools.SvcTool): def InstallService(self, entry): """Install Service entry.""" - # don't take any actions for mode='manual' - if entry.get('mode', 'default') == 'manual': - self.logger.info("Service %s mode set to manual. Skipping " - "installation." % (entry.get('name'))) - return False rcmd = "/sbin/chkconfig %s %s" self.cmd.run("/sbin/chkconfig --add %s" % (entry.attrib['name'])) self.logger.info("Installing Service %s" % (entry.get('name'))) - pass1 = True + rv = True if entry.get('status') == 'off': - rc = self.cmd.run(rcmd % (entry.get('name'), - entry.get('status')) + \ - " --level 0123456")[0] - pass1 = rc == 0 - rc = self.cmd.run(rcmd % (entry.get('name'), entry.get('status')))[0] - return pass1 and rc == 0 + rv &= self.cmd.run(rcmd + " --level 0123456" % + (entry.get('name'), + entry.get('status')))[0] == 0 + if entry.get("current_status") == "on": + rv &= self.stop_service(entry) + else: + rv &= self.cmd.run(rcmd % (entry.get('name'), + entry.get('status')))[0] == 0 + if entry.get("current_status") == "off": + rv &= self.start_service(entry) + return rv def FindExtra(self): """Locate extra chkconfig Services.""" diff --git a/src/lib/Bcfg2/Client/Tools/DebInit.py b/src/lib/Bcfg2/Client/Tools/DebInit.py index ca6fc439e..7d5af1127 100644 --- a/src/lib/Bcfg2/Client/Tools/DebInit.py +++ b/src/lib/Bcfg2/Client/Tools/DebInit.py @@ -76,11 +76,6 @@ class DebInit(Bcfg2.Client.Tools.SvcTool): def InstallService(self, entry): """Install Service for entry.""" - # don't take any actions for mode='manual' - if entry.get('mode', 'default') == 'manual': - self.logger.info("Service %s mode set to manual. Skipping " - "installation." % (entry.get('name'))) - return False self.logger.info("Installing Service %s" % (entry.get('name'))) try: os.stat('/etc/init.d/%s' % entry.get('name')) diff --git a/src/lib/Bcfg2/Client/Tools/OpenCSW.py b/src/lib/Bcfg2/Client/Tools/OpenCSW.py new file mode 100644 index 000000000..6aafe316f --- /dev/null +++ b/src/lib/Bcfg2/Client/Tools/OpenCSW.py @@ -0,0 +1,33 @@ +# This is the bcfg2 support for opencsw packages (pkgutil) +"""This provides Bcfg2 support for OpenCSW packages.""" + +import tempfile +import Bcfg2.Client.Tools.SYSV + + +class OpenCSW(Bcfg2.Client.Tools.SYSV.SYSV): + """Support for OpenCSW packages.""" + pkgtype = 'opencsw' + pkgtool = ("/opt/csw/bin/pkgutil -y -i %s", ("%s", ["bname"])) + name = 'OpenCSW' + __execs__ = ['/opt/csw/bin/pkgutil', "/usr/bin/pkginfo"] + __handles__ = [('Package', 'opencsw')] + __ireq__ = {'Package': ['name', 'version', 'bname']} + + def __init__(self, logger, setup, config): + # dont use the sysv constructor + Bcfg2.Client.Tools.PkgTool.__init__(self, logger, setup, config) + noaskfile = tempfile.NamedTemporaryFile() + self.noaskname = noaskfile.name + try: + noaskfile.write(Bcfg2.Client.Tools.SYSV.noask) + except: + pass + + # 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/Bcfg2/Client/Tools/POSIX.py b/src/lib/Bcfg2/Client/Tools/POSIX.py deleted file mode 100644 index 0d67dbbab..000000000 --- a/src/lib/Bcfg2/Client/Tools/POSIX.py +++ /dev/null @@ -1,943 +0,0 @@ -"""All POSIX Type client support for Bcfg2.""" - -import binascii -from datetime import datetime -import difflib -import errno -import grp -import logging -import os -import pwd -import shutil -import stat -import sys -import time -# py3k compatibility -if sys.hexversion >= 0x03000000: - unicode = str - -import Bcfg2.Client.Tools -import Bcfg2.Options -from Bcfg2.Client import XML - -log = logging.getLogger('POSIX') - -# map between dev_type attribute and stat constants -device_map = {'block': stat.S_IFBLK, - 'char': stat.S_IFCHR, - 'fifo': stat.S_IFIFO} - - -def calcPerms(initial, perms): - """This compares ondisk permissions with specified ones.""" - pdisp = [{1:stat.S_ISVTX, 2:stat.S_ISGID, 4:stat.S_ISUID}, - {1:stat.S_IXUSR, 2:stat.S_IWUSR, 4:stat.S_IRUSR}, - {1:stat.S_IXGRP, 2:stat.S_IWGRP, 4:stat.S_IRGRP}, - {1:stat.S_IXOTH, 2:stat.S_IWOTH, 4:stat.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 list(pdisp[index].items()): - if pdigits[index] & num: - tempperms |= perm - return tempperms - - -def normGid(entry): - """ - This takes a group name or gid and - returns the corresponding gid or False. - """ - try: - try: - return int(entry.get('group')) - except: - return int(grp.getgrnam(entry.get('group'))[2]) - except (OSError, KeyError): - log.error('GID normalization failed for %s. Does group %s exist?' - % (entry.get('name'), entry.get('group'))) - return False - - -def normUid(entry): - """ - This takes a user name or uid and - returns the corresponding uid or False. - """ - try: - try: - return int(entry.get('owner')) - except: - return int(pwd.getpwnam(entry.get('owner'))[2]) - except (OSError, KeyError): - log.error('UID normalization failed for %s. Does owner %s exist?' - % (entry.get('name'), entry.get('owner'))) - return False - - -def isString(strng, encoding): - """ - Returns true if the string contains no ASCII control characters - and can be decoded from the specified encoding. - """ - for char in strng: - if ord(char) < 9 or ord(char) > 13 and ord(char) < 32: - return False - try: - strng.decode(encoding) - return True - except: - return False - - -class POSIX(Bcfg2.Client.Tools.Tool): - """POSIX File support code.""" - name = 'POSIX' - __handles__ = [('Path', 'device'), - ('Path', 'directory'), - ('Path', 'file'), - ('Path', 'hardlink'), - ('Path', 'nonexistent'), - ('Path', 'permissions'), - ('Path', 'symlink')] - __req__ = {'Path': ['name', 'type']} - - # grab paranoid options from /etc/bcfg2.conf - opts = {'ppath': Bcfg2.Options.PARANOID_PATH, - 'max_copies': Bcfg2.Options.PARANOID_MAX_COPIES} - setup = Bcfg2.Options.OptionParser(opts) - setup.parse([]) - ppath = setup['ppath'] - max_copies = setup['max_copies'] - - def canInstall(self, entry): - """Check if entry is complete for installation.""" - if Bcfg2.Client.Tools.Tool.canInstall(self, entry): - if (entry.tag, - entry.get('type'), - entry.text, - entry.get('empty', 'false')) == ('Path', - 'file', - None, - 'false'): - return False - return True - else: - return False - - def gatherCurrentData(self, entry): - if entry.tag == 'Path' and entry.get('type') == 'file': - 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: - entry.set('current_owner', str(ondisk[stat.ST_UID])) - entry.set('current_group', str(ondisk[stat.ST_GID])) - except (OSError, KeyError): - pass - entry.set('perms', str(oct(ondisk[stat.ST_MODE])[-4:])) - - def Verifydevice(self, entry, _): - """Verify device entry.""" - if entry.get('dev_type') == None or \ - entry.get('owner') == None or \ - entry.get('group') == None: - self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % (entry.get('name'))) - return False - if entry.get('dev_type') in ['block', 'char']: - # check if major/minor are properly specified - if entry.get('major') == None or \ - entry.get('minor') == None: - self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % (entry.get('name'))) - return False - try: - # check for file existence - filestat = 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: - # attempt to verify device properties as specified in config - dev_type = entry.get('dev_type') - mode = calcPerms(device_map[dev_type], - entry.get('mode', '0600')) - owner = normUid(entry) - group = normGid(entry) - if dev_type in ['block', 'char']: - # check for incompletely specified entries - if entry.get('major') == None or \ - entry.get('minor') == None: - self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % (entry.get('name'))) - return False - major = int(entry.get('major')) - minor = int(entry.get('minor')) - if major == os.major(filestat.st_rdev) and \ - minor == os.minor(filestat.st_rdev) and \ - mode == filestat.st_mode and \ - owner == filestat.st_uid and \ - group == filestat.st_gid: - return True - else: - return False - elif dev_type == 'fifo' and \ - mode == filestat.st_mode and \ - owner == filestat.st_uid and \ - group == filestat.st_gid: - return True - else: - self.logger.info('Device properties for %s incorrect' % \ - entry.get('name')) - return False - except OSError: - self.logger.debug("%s %s failed to verify" % - (entry.tag, entry.get('name'))) - return False - - def Installdevice(self, entry): - """Install device entries.""" - try: - # check for existing paths and remove them - os.lstat(entry.get('name')) - try: - os.unlink(entry.get('name')) - exists = False - except OSError: - self.logger.info('Failed to unlink %s' % \ - entry.get('name')) - return False - except OSError: - exists = False - - if not exists: - try: - dev_type = entry.get('dev_type') - mode = calcPerms(device_map[dev_type], - entry.get('mode', '0600')) - if dev_type in ['block', 'char']: - # check if major/minor are properly specified - if entry.get('major') == None or \ - entry.get('minor') == None: - self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % (entry.get('name'))) - return False - major = int(entry.get('major')) - minor = int(entry.get('minor')) - device = os.makedev(major, minor) - os.mknod(entry.get('name'), mode, device) - else: - os.mknod(entry.get('name'), mode) - """ - Python uses the OS mknod(2) implementation which modifies the - mode based on the umask of the running process. Therefore, the - following chmod(2) call is needed to make sure the permissions - are set as specified by the user. - """ - os.chmod(entry.get('name'), mode) - os.chown(entry.get('name'), normUid(entry), normGid(entry)) - return True - except KeyError: - self.logger.error('Failed to install %s' % entry.get('name')) - except OSError: - self.logger.error('Failed to install %s' % entry.get('name')) - return False - - def Verifydirectory(self, entry, modlist): - """Verify Path type='directory' entry.""" - if entry.get('perms') == None or \ - entry.get('owner') == None or \ - entry.get('group') == None: - self.logger.error("POSIX: Entry %s not completely specified. " - "Try running bcfg2-lint." % (entry.get('name'))) - return False - 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.info("POSIX: %s %s does not exist" % - (entry.tag, entry.get('name'))) - return False - try: - owner = str(ondisk[stat.ST_UID]) - group = str(ondisk[stat.ST_GID]) - except (OSError, KeyError): - self.logger.info("POSIX: User/Group resolution failed " - "for path %s" % entry.get('name')) - owner = 'root' - group = '0' - finfo = os.stat(entry.get('name')) - perms = oct(finfo[stat.ST_MODE])[-4:] - if entry.get('mtime', '-1') != '-1': - mtime = str(finfo[stat.ST_MTIME]) - else: - mtime = '-1' - pTrue = ((owner == str(normUid(entry))) and - (group == str(normGid(entry))) and - (perms == entry.get('perms')) and - (mtime == entry.get('mtime', '-1'))) - - pruneTrue = True - ex_ents = [] - if entry.get('prune', 'false') == 'true' \ - and (entry.tag == 'Path' and entry.get('type') == 'directory'): - # check for any extra entries when prune='true' attribute is set - try: - entries = ['/'.join([entry.get('name'), ent]) \ - for ent in os.listdir(entry.get('name'))] - ex_ents = [e for e in entries if e not in modlist] - if ex_ents: - pruneTrue = False - self.logger.info("POSIX: Directory %s contains " - "extra entries:" % entry.get('name')) - self.logger.info(ex_ents) - nqtext = entry.get('qtext', '') + '\n' - nqtext += "Directory %s contains extra entries: " % \ - entry.get('name') - nqtext += ":".join(ex_ents) - entry.set('qtest', nqtext) - [entry.append(XML.Element('Prune', path=x)) \ - for x in ex_ents] - except OSError: - ex_ents = [] - pruneTrue = True - - if not pTrue: - if owner != str(normUid(entry)): - entry.set('current_owner', owner) - self.logger.debug("%s %s ownership wrong" % \ - (entry.tag, entry.get('name'))) - nqtext = entry.get('qtext', '') + '\n' - nqtext += "%s owner wrong. is %s should be %s" % \ - (entry.get('name'), owner, entry.get('owner')) - entry.set('qtext', nqtext) - if group != str(normGid(entry)): - entry.set('current_group', group) - self.logger.debug("%s %s group wrong" % \ - (entry.tag, entry.get('name'))) - nqtext = entry.get('qtext', '') + '\n' - nqtext += "%s group is %s should be %s" % \ - (entry.get('name'), group, entry.get('group')) - entry.set('qtext', nqtext) - if perms != entry.get('perms'): - entry.set('current_perms', perms) - self.logger.debug("%s %s permissions are %s should be %s" % - (entry.tag, - entry.get('name'), - perms, - entry.get('perms'))) - nqtext = entry.get('qtext', '') + '\n' - nqtext += "%s %s perms are %s should be %s" % \ - (entry.tag, - entry.get('name'), - perms, - entry.get('perms')) - entry.set('qtext', nqtext) - if mtime != entry.get('mtime', '-1'): - entry.set('current_mtime', mtime) - self.logger.debug("%s %s mtime is %s should be %s" \ - % (entry.tag, entry.get('name'), mtime, - entry.get('mtime'))) - nqtext = entry.get('qtext', '') + '\n' - nqtext += "%s mtime is %s should be %s" % \ - (entry.get('name'), mtime, entry.get('mtime')) - entry.set('qtext', nqtext) - if entry.get('type') != 'file': - nnqtext = entry.get('qtext') - nnqtext += '\nInstall %s %s: (y/N) ' % (entry.get('type'), - entry.get('name')) - entry.set('qtext', nnqtext) - return pTrue and pruneTrue - - def Installdirectory(self, entry): - """Install Path type='directory' entry.""" - if entry.get('perms') == None or \ - entry.get('owner') == None or \ - entry.get('group') == None: - self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % \ - (entry.get('name'))) - return False - self.logger.info("Installing directory %s" % (entry.get('name'))) - try: - fmode = os.lstat(entry.get('name')) - if not stat.S_ISDIR(fmode[stat.ST_MODE]): - self.logger.debug("Found a non-directory entry at %s" % \ - (entry.get('name'))) - try: - os.unlink(entry.get('name')) - exists = False - 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.stat(parent) - except: - self.logger.debug('Creating parent path for directory %s' % (entry.get('name'))) - for idx in range(len(parent.split('/')[:-1])): - current = '/'+'/'.join(parent.split('/')[1:2+idx]) - try: - sloc = os.stat(current) - except OSError: - try: - os.mkdir(current) - continue - except OSError: - return False - if not stat.S_ISDIR(sloc[stat.ST_MODE]): - try: - os.unlink(current) - 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 - if entry.get('prune', 'false') == 'true' and entry.get("qtest"): - for pent in entry.findall('Prune'): - pname = pent.get('path') - ulfailed = False - if os.path.isdir(pname): - self.logger.info("Not removing extra directory %s, " - "please check and remove manually" % pname) - continue - try: - self.logger.debug("Unlinking file %s" % pname) - os.unlink(pname) - except OSError: - self.logger.error("Failed to unlink path %s" % pname) - ulfailed = True - if ulfailed: - return False - return self.Installpermissions(entry) - - def Verifyfile(self, entry, _): - """Verify Path type='file' entry.""" - # permissions check + content check - permissionStatus = self.Verifydirectory(entry, _) - tbin = False - if entry.text == None and entry.get('empty', 'false') == 'false': - self.logger.error("Cannot verify incomplete Path type='%s' %s" % - (entry.get('type'), entry.get('name'))) - return False - if entry.get('encoding', 'ascii') == 'base64': - tempdata = binascii.a2b_base64(entry.text) - tbin = True - elif entry.get('empty', 'false') == 'true': - tempdata = '' - else: - tempdata = entry.text - if type(tempdata) == unicode: - try: - tempdata = tempdata.encode(self.setup['encoding']) - except UnicodeEncodeError: - e = sys.exc_info()[1] - self.logger.error("Error encoding file %s:\n %s" % \ - (entry.get('name'), e)) - - different = False - content = None - if not os.path.exists(entry.get("name")): - # first, see if the target file exists at all; if not, - # they're clearly different - different = True - content = "" - else: - # next, see if the size of the target file is different - # from the size of the desired content - try: - estat = os.stat(entry.get('name')) - except OSError: - err = sys.exc_info()[1] - self.logger.error("Failed to stat %s: %s" % - (err.filename, err)) - return False - if len(tempdata) != estat[stat.ST_SIZE]: - different = True - else: - # finally, read in the target file and compare them - # directly. comparison could be done with a checksum, - # which might be faster for big binary files, but - # slower for everything else - try: - content = open(entry.get('name')).read() - except IOError: - err = sys.exc_info()[1] - self.logger.error("Failed to read %s: %s" % - (err.filename, err)) - return False - different = content != tempdata - - if different: - if self.setup['interactive']: - prompt = [entry.get('qtext', '')] - if not tbin and content is None: - # it's possible that we figured out the files are - # different without reading in the local file. if - # the supplied version of the file is not binary, - # we now have to read in the local file to figure - # out if _it_ is binary, and either include that - # fact or the diff in our prompts for -I - try: - content = open(entry.get('name')).read() - except IOError: - err = sys.exc_info()[1] - self.logger.error("Failed to read %s: %s" % - (err.filename, err)) - return False - if tbin or not isString(content, self.setup['encoding']): - # don't compute diffs if the file is binary - prompt.append('Binary file, no printable diff') - else: - diff = self._diff(content, tempdata, - difflib.unified_diff, - filename=entry.get("name")) - if diff: - udiff = '\n'.join(diff) - try: - prompt.append(udiff.decode(self.setup['encoding'])) - except UnicodeDecodeError: - prompt.append("Binary file, no printable diff") - else: - prompt.append("Diff took too long to compute, no " - "printable diff") - prompt.append("Install %s %s: (y/N): " % (entry.tag, - entry.get('name'))) - entry.set("qtext", "\n".join(prompt)) - - if entry.get('sensitive', 'false').lower() != 'true': - if content is None: - # it's possible that we figured out the files are - # different without reading in the local file. we - # now have to read in the local file to figure out - # if _it_ is binary, and either include the whole - # file or the diff for reports - try: - content = open(entry.get('name')).read() - except IOError: - err = sys.exc_info()[1] - self.logger.error("Failed to read %s: %s" % - (err.filename, err)) - return False - - if tbin or not isString(content, self.setup['encoding']): - # don't compute diffs if the file is binary - entry.set('current_bfile', binascii.b2a_base64(content)) - else: - diff = self._diff(content, tempdata, difflib.ndiff, - filename=entry.get("name")) - if diff: - entry.set("current_bdiff", - binascii.b2a_base64("\n".join(diff))) - elif not tbin and isString(content, self.setup['encoding']): - entry.set('current_bfile', binascii.b2a_base64(content)) - elif permissionStatus == False and self.setup['interactive']: - prompt = [entry.get('qtext', '')] - prompt.append("Install %s %s: (y/N): " % (entry.tag, - entry.get('name'))) - entry.set("qtext", "\n".join(prompt)) - - - return permissionStatus and not different - - def Installfile(self, entry): - """Install Path type='file' entry.""" - self.logger.info("Installing file %s" % (entry.get('name'))) - - parent = "/".join(entry.get('name').split('/')[:-1]) - if parent: - try: - os.stat(parent) - except: - self.logger.debug('Creating parent path for config file %s' % \ - (entry.get('name'))) - current = '/' - for next in parent.split('/')[1:]: - current += next + '/' - try: - sloc = os.stat(current) - try: - if not stat.S_ISDIR(sloc[stat.ST_MODE]): - self.logger.debug('%s is not a directory; recreating' \ - % (current)) - os.unlink(current) - os.mkdir(current) - except OSError: - return False - except OSError: - try: - self.logger.debug("Creating non-existent path %s" % current) - os.mkdir(current) - except OSError: - return False - - # If we get here, then the parent directory should exist - if (entry.get("paranoid", False) in ['true', 'True']) and \ - self.setup.get("paranoid", False) and not \ - (entry.get('current_exists', 'true') == 'false'): - bkupnam = entry.get('name').replace('/', '_') - # current list of backups for this file - try: - bkuplist = [f for f in os.listdir(self.ppath) if - f.startswith(bkupnam)] - except OSError: - e = sys.exc_info()[1] - self.logger.error("Failed to create backup list in %s: %s" % - (self.ppath, e.strerror)) - return False - bkuplist.sort() - while len(bkuplist) >= int(self.max_copies): - # remove the oldest backup available - oldest = bkuplist.pop(0) - self.logger.info("Removing %s" % oldest) - try: - os.remove("%s/%s" % (self.ppath, oldest)) - except: - self.logger.error("Failed to remove %s/%s" % \ - (self.ppath, oldest)) - return False - try: - # backup existing file - shutil.copy(entry.get('name'), - "%s/%s_%s" % (self.ppath, bkupnam, - datetime.isoformat(datetime.now()))) - self.logger.info("Backup of %s saved to %s" % - (entry.get('name'), self.ppath)) - except IOError: - e = sys.exc_info()[1] - self.logger.error("Failed to create backup file for %s" % \ - (entry.get('name'))) - self.logger.error(e) - return False - 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: - if type(entry.text) == unicode: - filedata = entry.text.encode(self.setup['encoding']) - else: - filedata = entry.text - newfile.write(filedata) - newfile.close() - try: - os.chown(newfile.name, normUid(entry), normGid(entry)) - except KeyError: - self.logger.error("Failed to chown %s to %s:%s" % - (newfile.name, entry.get('owner'), - entry.get('group'))) - os.chown(newfile.name, 0, 0) - except OSError: - err = sys.exc_info()[1] - self.logger.error("Could not chown %s: %s" % (newfile.name, - err)) - os.chmod(newfile.name, calcPerms(stat.S_IFREG, entry.get('perms'))) - os.rename(newfile.name, entry.get('name')) - if entry.get('mtime', '-1') != '-1': - try: - os.utime(entry.get('name'), (int(entry.get('mtime')), - int(entry.get('mtime')))) - except: - self.logger.error("File %s mtime fix failed" \ - % (entry.get('name'))) - return False - return True - except (OSError, IOError): - err = sys.exc_info()[1] - if err.errno == errno.EACCES: - self.logger.info("Failed to open %s for writing" % (entry.get('name'))) - else: - print(err) - return False - - def Verifyhardlink(self, entry, _): - """Verify HardLink entry.""" - if entry.get('to') == None: - self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % \ - (entry.get('name'))) - return False - try: - if os.path.samefile(entry.get('name'), entry.get('to')): - return True - self.logger.debug("Hardlink %s is incorrect" % \ - entry.get('name')) - entry.set('qtext', "Link %s to %s? [y/N] " % \ - (entry.get('name'), - entry.get('to'))) - return False - except OSError: - entry.set('current_exists', 'false') - entry.set('qtext', "Link %s to %s? [y/N] " % \ - (entry.get('name'), - entry.get('to'))) - return False - - def Installhardlink(self, entry): - """Install HardLink entry.""" - if entry.get('to') == None: - self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % \ - (entry.get('name'))) - return False - self.logger.info("Installing Hardlink %s" % (entry.get('name'))) - if os.path.lexists(entry.get('name')): - try: - fmode = os.lstat(entry.get('name'))[stat.ST_MODE] - if stat.S_ISREG(fmode) or stat.S_ISLNK(fmode): - self.logger.debug("Non-directory entry already exists at " - "%s. Unlinking entry." % (entry.get('name'))) - os.unlink(entry.get('name')) - elif stat.S_ISDIR(fmode): - self.logger.debug("Directory 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("Hardlink %s cleanup failed" % \ - (entry.get('name'))) - try: - os.link(entry.get('to'), entry.get('name')) - return True - except OSError: - return False - - def Verifynonexistent(self, entry, _): - """Verify nonexistent entry.""" - # return true if path does _not_ exist - return not os.path.lexists(entry.get('name')) - - def Installnonexistent(self, entry): - '''Remove nonexistent entries''' - ename = entry.get('name') - if entry.get('recursive') in ['True', 'true']: - # ensure that configuration spec is consistent first - if [e for e in self.buildModlist() \ - if e.startswith(ename) and e != ename]: - self.logger.error('Not installing %s. One or more files ' - 'in this directory are specified in ' - 'your configuration.' % ename) - return False - try: - shutil.rmtree(ename) - except OSError: - e = sys.exc_info()[1] - self.logger.error('Failed to remove %s: %s' % (ename, - e.strerror)) - else: - if os.path.isdir(ename): - try: - os.rmdir(ename) - return True - except OSError: - e = sys.exc_info()[1] - self.logger.error('Failed to remove %s: %s' % (ename, - e.strerror)) - return False - try: - os.remove(ename) - return True - except OSError: - e = sys.exc_info()[1] - self.logger.error('Failed to remove %s: %s' % (ename, - e.strerror)) - return False - - def Verifypermissions(self, entry, _): - """Verify Path type='permissions' entry""" - if entry.get('perms') == None or \ - entry.get('owner') == None or \ - entry.get('group') == None: - self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % (entry.get('name'))) - return False - if entry.get('recursive') in ['True', 'true']: - # verify ownership information recursively - owner = normUid(entry) - group = normGid(entry) - - for root, dirs, files in os.walk(entry.get('name')): - for p in dirs + files: - path = os.path.join(root, p) - pstat = os.stat(path) - if owner != pstat.st_uid: - # owner mismatch for path - entry.set('current_owner', str(pstat.st_uid)) - self.logger.debug("%s %s ownership wrong" % \ - (entry.tag, path)) - nqtext = entry.get('qtext', '') + '\n' - nqtext += ("Owner for path %s is incorrect. " - "Current owner is %s but should be %s\n" % \ - (path, pstat.st_uid, entry.get('owner'))) - nqtext += ("\nInstall %s %s: (y/N): " % - (entry.tag, entry.get('name'))) - entry.set('qtext', nqtext) - return False - if group != pstat.st_gid: - # group mismatch for path - entry.set('current_group', str(pstat.st_gid)) - self.logger.debug("%s %s group wrong" % \ - (entry.tag, path)) - nqtext = entry.get('qtext', '') + '\n' - nqtext += ("Group for path %s is incorrect. " - "Current group is %s but should be %s\n" % \ - (path, pstat.st_gid, entry.get('group'))) - nqtext += ("\nInstall %s %s: (y/N): " % - (entry.tag, entry.get('name'))) - entry.set('qtext', nqtext) - return False - return self.Verifydirectory(entry, _) - - def _diff(self, content1, content2, difffunc, filename=None): - rv = [] - start = time.time() - longtime = False - for diffline in difffunc(content1.split('\n'), - content2.split('\n')): - now = time.time() - rv.append(diffline) - if now - start > 5 and not longtime: - if filename: - self.logger.info("Diff of %s taking a long time" % - filename) - else: - self.logger.info("Diff taking a long time") - longtime = True - elif now - start > 30: - if filename: - self.logger.error("Diff of %s took too long; giving up" % - filename) - else: - self.logger.error("Diff took too long; giving up") - return False - return rv - - def Installpermissions(self, entry): - """Install POSIX permissions""" - if entry.get('perms') == None or \ - entry.get('owner') == None or \ - entry.get('group') == None: - self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % (entry.get('name'))) - return False - plist = [entry.get('name')] - if entry.get('recursive') in ['True', 'true']: - # verify ownership information recursively - owner = normUid(entry) - group = normGid(entry) - - for root, dirs, files in os.walk(entry.get('name')): - for p in dirs + files: - path = os.path.join(root, p) - pstat = os.stat(path) - if owner != pstat.st_uid or group != pstat.st_gid: - # owner mismatch for path - plist.append(path) - try: - for p in plist: - os.chown(p, normUid(entry), normGid(entry)) - os.chmod(p, calcPerms(stat.S_IFDIR, entry.get('perms'))) - return True - except (OSError, KeyError): - self.logger.error('Permission fixup failed for %s' % \ - (entry.get('name'))) - return False - - def Verifysymlink(self, entry, _): - """Verify Path type='symlink' entry.""" - if entry.get('to') == None: - self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % \ - (entry.get('name'))) - return False - 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) - entry.set('qtext', "Link %s to %s? [y/N] " % (entry.get('name'), - entry.get('to'))) - return False - except OSError: - entry.set('current_exists', 'false') - entry.set('qtext', "Link %s to %s? [y/N] " % (entry.get('name'), - entry.get('to'))) - return False - - def Installsymlink(self, entry): - """Install Path type='symlink' entry.""" - if entry.get('to') == None: - self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % \ - (entry.get('name'))) - return False - self.logger.info("Installing symlink %s" % (entry.get('name'))) - if os.path.lexists(entry.get('name')): - try: - fmode = os.lstat(entry.get('name'))[stat.ST_MODE] - if stat.S_ISREG(fmode) or stat.S_ISLNK(fmode): - self.logger.debug("Non-directory entry already exists at " - "%s. Unlinking entry." % \ - (entry.get('name'))) - os.unlink(entry.get('name')) - elif stat.S_ISDIR(fmode): - self.logger.debug("Directory 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 InstallPath(self, entry): - """Dispatch install to the proper method according to type""" - ret = getattr(self, 'Install%s' % entry.get('type')) - return ret(entry) - - def VerifyPath(self, entry, _): - """Dispatch verify to the proper method according to type""" - ret = getattr(self, 'Verify%s' % entry.get('type')) - return ret(entry, _) diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/Device.py b/src/lib/Bcfg2/Client/Tools/POSIX/Device.py new file mode 100644 index 000000000..0ea4128f7 --- /dev/null +++ b/src/lib/Bcfg2/Client/Tools/POSIX/Device.py @@ -0,0 +1,66 @@ +import os +import sys +try: + from base import POSIXTool, device_map +except ImportError: + # py3k, incompatible syntax with py2.4 + exec("from .base import POSIXTool, device_map") + +class POSIXDevice(POSIXTool): + __req__ = ['name', 'dev_type', 'perms', 'owner', 'group'] + + def fully_specified(self, entry): + if entry.get('dev_type') in ['block', 'char']: + # check if major/minor are properly specified + if (entry.get('major') == None or + entry.get('minor') == None): + return False + return True + + def verify(self, entry, modlist): + """Verify device entry.""" + ondisk = self._exists(entry) + if not ondisk: + return False + + # attempt to verify device properties as specified in config + rv = True + dev_type = entry.get('dev_type') + if dev_type in ['block', 'char']: + major = int(entry.get('major')) + minor = int(entry.get('minor')) + if major != os.major(ondisk.st_rdev): + msg = ("Major number for device %s is incorrect. " + "Current major is %s but should be %s" % + (entry.get("name"), os.major(ondisk.st_rdev), major)) + self.logger.debug('POSIX: ' + msg) + entry.set('qtext', entry.get('qtext', '') + "\n" + msg) + rv = False + + if minor != os.minor(ondisk.st_rdev): + msg = ("Minor number for device %s is incorrect. " + "Current minor is %s but should be %s" % + (entry.get("name"), os.minor(ondisk.st_rdev), minor)) + self.logger.debug('POSIX: ' + msg) + entry.set('qtext', entry.get('qtext', '') + "\n" + msg) + rv = False + return POSIXTool.verify(self, entry, modlist) and rv + + def install(self, entry): + if not self._exists(entry, remove=True): + try: + dev_type = entry.get('dev_type') + mode = device_map[dev_type] | int(entry.get('perms'), 8) + if dev_type in ['block', 'char']: + major = int(entry.get('major')) + minor = int(entry.get('minor')) + device = os.makedev(major, minor) + os.mknod(entry.get('name'), mode, device) + else: + os.mknod(entry.get('name'), mode) + except (KeyError, OSError, ValueError): + err = sys.exc_info()[1] + self.logger.error('POSIX: Failed to install %s: %s' % + (entry.get('name'), err)) + return False + return POSIXTool.install(self, entry) diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/Directory.py b/src/lib/Bcfg2/Client/Tools/POSIX/Directory.py new file mode 100644 index 000000000..d2d383f66 --- /dev/null +++ b/src/lib/Bcfg2/Client/Tools/POSIX/Directory.py @@ -0,0 +1,90 @@ +import os +import sys +import stat +import shutil +import Bcfg2.Client.XML +try: + from base import POSIXTool +except ImportError: + # py3k, incompatible syntax with py2.4 + exec("from .base import POSIXTool") + +class POSIXDirectory(POSIXTool): + __req__ = ['name', 'perms', 'owner', 'group'] + + def verify(self, entry, modlist): + ondisk = self._exists(entry) + if not ondisk: + return False + + if not stat.S_ISDIR(ondisk[stat.ST_MODE]): + self.logger.info("POSIX: %s is not a directory" % entry.get('name')) + return False + + pruneTrue = True + if entry.get('prune', 'false').lower() == 'true': + # check for any extra entries when prune='true' attribute is set + try: + extras = [os.path.join(entry.get('name'), ent) + for ent in os.listdir(entry.get('name')) + if os.path.join(entry.get('name'), + ent) not in modlist] + if extras: + pruneTrue = False + msg = "Directory %s contains extra entries: %s" % \ + (entry.get('name'), "; ".join(extras)) + self.logger.info("POSIX: " + msg) + entry.set('qtext', entry.get('qtext', '') + '\n' + msg) + for extra in extras: + Bcfg2.Client.XML.SubElement(entry, 'Prune', path=extra) + except OSError: + pruneTrue = True + + return POSIXTool.verify(self, entry, modlist) and pruneTrue + + def install(self, entry): + """Install device entries.""" + fmode = self._exists(entry) + + if fmode and not stat.S_ISDIR(fmode[stat.ST_MODE]): + self.logger.info("POSIX: Found a non-directory entry at %s, " + "removing" % entry.get('name')) + try: + os.unlink(entry.get('name')) + fmode = False + except OSError: + err = sys.exc_info()[1] + self.logger.error("POSIX: Failed to unlink %s: %s" % + (entry.get('name'), err)) + return False + elif fmode: + self.logger.debug("POSIX: Found a pre-existing directory at %s" % + entry.get('name')) + + rv = True + if not fmode: + rv &= self._makedirs(entry) + + if entry.get('prune', 'false') == 'true': + ulfailed = False + for pent in entry.findall('Prune'): + pname = pent.get('path') + ulfailed = False + if os.path.isdir(pname): + rm = shutil.rmtree + else: + rm = os.unlink + try: + self.logger.debug("POSIX: Removing %s" % pname) + rm(pname) + except OSError: + err = sys.exc_info()[1] + self.logger.error("POSIX: Failed to unlink %s: %s" % + (pname, err)) + ulfailed = True + if ulfailed: + # even if prune failed, we still want to install the + # entry to make sure that we get permissions and + # whatnot set + rv = False + return POSIXTool.install(self, entry) and rv diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/File.py b/src/lib/Bcfg2/Client/Tools/POSIX/File.py new file mode 100644 index 000000000..26550078e --- /dev/null +++ b/src/lib/Bcfg2/Client/Tools/POSIX/File.py @@ -0,0 +1,225 @@ +import os +import sys +import stat +import time +import difflib +import tempfile +try: + from base import POSIXTool +except ImportError: + # py3k, incompatible syntax with py2.4 + exec("from .base import POSIXTool") +from Bcfg2.Bcfg2Py3k import unicode, b64encode, b64decode + +class POSIXFile(POSIXTool): + __req__ = ['name', 'perms', 'owner', 'group'] + + def fully_specified(self, entry): + return entry.text is not None or entry.get('empty', 'false') == 'true' + + def _is_string(self, strng, encoding): + """ Returns true if the string contains no ASCII control + characters and can be decoded from the specified encoding. """ + for char in strng: + if ord(char) < 9 or ord(char) > 13 and ord(char) < 32: + return False + if not hasattr(strng, "decode"): + # py3k + return True + try: + strng.decode(encoding) + return True + except: + return False + + def _get_data(self, entry): + is_binary = False + if entry.get('encoding', 'ascii') == 'base64': + tempdata = b64decode(entry.text) + is_binary = True + + elif entry.get('empty', 'false') == 'true': + tempdata = '' + else: + tempdata = entry.text + if isinstance(tempdata, unicode) and unicode != str: + try: + tempdata = tempdata.encode(self.setup['encoding']) + except UnicodeEncodeError: + err = sys.exc_info()[1] + self.logger.error("POSIX: Error encoding file %s: %s" % + (entry.get('name'), err)) + return (tempdata, is_binary) + + def verify(self, entry, modlist): + ondisk = self._exists(entry) + tempdata, is_binary = self._get_data(entry) + + different = False + content = None + if not ondisk: + # first, see if the target file exists at all; if not, + # they're clearly different + different = True + content = "" + elif len(tempdata) != ondisk[stat.ST_SIZE]: + # next, see if the size of the target file is different + # from the size of the desired content + different = True + else: + # finally, read in the target file and compare them + # directly. comparison could be done with a checksum, + # which might be faster for big binary files, but slower + # for everything else + try: + content = open(entry.get('name')).read() + except IOError: + self.logger.error("POSIX: Failed to read %s: %s" % + (entry.get("name"), sys.exc_info()[1])) + return False + different = content != tempdata + + if different: + self.logger.debug("POSIX: %s has incorrect contents" % + entry.get("name")) + self._get_diffs( + entry, interactive=self.setup['interactive'], + sensitive=entry.get('sensitive', 'false').lower() == 'true', + is_binary=is_binary, content=content) + return POSIXTool.verify(self, entry, modlist) and not different + + def _write_tmpfile(self, entry): + filedata, _ = self._get_data(entry) + # get a temp file to write to that is in the same directory as + # the existing file in order to preserve any permissions + # protections on that directory, and also to avoid issues with + # /tmp set nosetuid while creating files that are supposed to + # be setuid + try: + (newfd, newfile) = \ + tempfile.mkstemp(prefix=os.path.basename(entry.get("name")), + dir=os.path.dirname(entry.get("name"))) + except OSError: + err = sys.exc_info()[1] + self.logger.error("POSIX: Failed to create temp file in %s: %s" % + (os.path.dirname(entry.get('name')), err)) + return False + try: + os.fdopen(newfd, 'w').write(filedata) + except (OSError, IOError): + err = sys.exc_info()[1] + self.logger.error("POSIX: Failed to open temp file %s for writing " + "%s: %s" % + (newfile, entry.get("name"), err)) + return False + return newfile + + def _rename_tmpfile(self, newfile, entry): + try: + os.rename(newfile, entry.get('name')) + return True + except OSError: + err = sys.exc_info()[1] + self.logger.error("POSIX: Failed to rename temp file %s to %s: %s" % + (newfile, entry.get('name'), err)) + try: + os.unlink(newfile) + except: + err = sys.exc_info()[1] + self.logger.error("POSIX: Could not remove temp file %s: %s" % + (newfile, err)) + return False + + def install(self, entry): + """Install device entries.""" + if not os.path.exists(os.path.dirname(entry.get('name'))): + if not self._makedirs(entry, + path=os.path.dirname(entry.get('name'))): + return False + newfile = self._write_tmpfile(entry) + if not newfile: + return False + rv = self._set_perms(entry, path=newfile) + if not self._rename_tmpfile(newfile, entry): + return False + + return POSIXTool.install(self, entry) and rv + + def _get_diffs(self, entry, interactive=False, sensitive=False, + is_binary=False, content=None): + if not interactive and sensitive: + return + + prompt = [entry.get('qtext', '')] + attrs = dict() + if content is None: + # it's possible that we figured out the files are + # different without reading in the local file. if the + # supplied version of the file is not binary, we now have + # to read in the local file to figure out if _it_ is + # binary, and either include that fact or the diff in our + # prompts for -I and the reports + try: + content = open(entry.get('name')).read() + except IOError: + self.logger.error("POSIX: Failed to read %s: %s" % + (entry.get("name"), sys.exc_info()[1])) + return False + if not is_binary: + is_binary |= not self._is_string(content, self.setup['encoding']) + if is_binary: + # don't compute diffs if the file is binary + prompt.append('Binary file, no printable diff') + attrs['current_bfile'] = b64encode(content) + else: + if interactive: + diff = self._diff(content, self._get_data(entry)[0], + difflib.unified_diff, + filename=entry.get("name")) + if diff: + udiff = ''.join(diff) + if hasattr(udiff, "decode"): + udiff = udiff.decode(self.setup['encoding']) + try: + prompt.append(udiff) + except UnicodeEncodeError: + prompt.append("Could not encode diff") + else: + prompt.append("Diff took too long to compute, no " + "printable diff") + if not sensitive: + diff = self._diff(content, self._get_data(entry)[0], + difflib.ndiff, filename=entry.get("name")) + if diff: + attrs["current_bdiff"] = b64encode("\n".join(diff)) + else: + attrs['current_bfile'] = b64encode(content) + if interactive: + entry.set("qtext", "\n".join(prompt)) + if not sensitive: + for attr, val in attrs.items(): + entry.set(attr, val) + + def _diff(self, content1, content2, difffunc, filename=None): + rv = [] + start = time.time() + longtime = False + for diffline in difffunc(content1.split('\n'), + content2.split('\n')): + now = time.time() + rv.append(diffline) + if now - start > 5 and not longtime: + if filename: + self.logger.info("POSIX: Diff of %s taking a long time" % + filename) + else: + self.logger.info("POSIX: Diff taking a long time") + longtime = True + elif now - start > 30: + if filename: + self.logger.error("POSIX: Diff of %s took too long; giving " + "up" % filename) + else: + self.logger.error("POSIX: Diff took too long; giving up") + return False + return rv diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/Hardlink.py b/src/lib/Bcfg2/Client/Tools/POSIX/Hardlink.py new file mode 100644 index 000000000..ca7a23717 --- /dev/null +++ b/src/lib/Bcfg2/Client/Tools/POSIX/Hardlink.py @@ -0,0 +1,43 @@ +import os +import sys +try: + from base import POSIXTool +except ImportError: + # py3k, incompatible syntax with py2.4 + exec("from .base import POSIXTool") + +class POSIXHardlink(POSIXTool): + __req__ = ['name', 'to'] + + def verify(self, entry, modlist): + rv = True + + try: + if not os.path.samefile(entry.get('name'), entry.get('to')): + msg = "Hardlink %s is incorrect" % entry.get('name') + self.logger.debug("POSIX: " + msg) + entry.set('qtext', "\n".join([entry.get('qtext', ''), msg])) + rv = False + except OSError: + self.logger.debug("POSIX: %s %s does not exist" % + (entry.tag, entry.get("name"))) + entry.set('current_exists', 'false') + return False + + return POSIXTool.verify(self, entry, modlist) and rv + + def install(self, entry): + ondisk = self._exists(entry, remove=True) + if ondisk: + self.logger.info("POSIX: Hardlink %s cleanup failed" % + entry.get('name')) + try: + os.link(entry.get('to'), entry.get('name')) + rv = True + except OSError: + err = sys.exc_info()[1] + self.logger.error("POSIX: Failed to create hardlink %s to %s: %s" % + (entry.get('name'), entry.get('to'), err)) + rv = False + return POSIXTool.install(self, entry) and rv + diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/Nonexistent.py b/src/lib/Bcfg2/Client/Tools/POSIX/Nonexistent.py new file mode 100644 index 000000000..c870ca0ed --- /dev/null +++ b/src/lib/Bcfg2/Client/Tools/POSIX/Nonexistent.py @@ -0,0 +1,45 @@ +import os +import sys +import shutil +try: + from base import POSIXTool +except ImportError: + # py3k, incompatible syntax with py2.4 + exec("from .base import POSIXTool") + +class POSIXNonexistent(POSIXTool): + __req__ = ['name'] + + def verify(self, entry, _): + if os.path.lexists(entry.get('name')): + self.logger.debug("POSIX: %s exists but should not" % + entry.get("name")) + return False + return True + + def install(self, entry): + ename = entry.get('name') + if entry.get('recursive', '').lower() == 'true': + # ensure that configuration spec is consistent first + for struct in self.config.getchildren(): + for entry in struct.getchildren(): + if (entry.tag == 'Path' and + entry.get('type') != 'nonexistent' and + entry.get('name').startswith(ename)): + self.logger.error('POSIX: Not removing %s. One or ' + 'more files in this directory are ' + 'specified in your configuration.' % + ename) + return False + rm = shutil.rmtree + elif os.path.isdir(ename): + rm = os.rmdir + else: + rm = os.remove + try: + rm(ename) + return True + except OSError: + err = sys.exc_info()[1] + self.logger.error('POSIX: Failed to remove %s: %s' % (ename, err)) + return False diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/Permissions.py b/src/lib/Bcfg2/Client/Tools/POSIX/Permissions.py new file mode 100644 index 000000000..321376b98 --- /dev/null +++ b/src/lib/Bcfg2/Client/Tools/POSIX/Permissions.py @@ -0,0 +1,11 @@ +import os +import sys +try: + from base import POSIXTool +except ImportError: + # py3k, incompatible syntax with py2.4 + exec("from .base import POSIXTool") + +class POSIXPermissions(POSIXTool): + __req__ = ['name', 'perms', 'owner', 'group'] + diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/Symlink.py b/src/lib/Bcfg2/Client/Tools/POSIX/Symlink.py new file mode 100644 index 000000000..fb303bdbe --- /dev/null +++ b/src/lib/Bcfg2/Client/Tools/POSIX/Symlink.py @@ -0,0 +1,46 @@ +import os +import sys +try: + from base import POSIXTool +except ImportError: + # py3k, incompatible syntax with py2.4 + exec("from .base import POSIXTool") + +class POSIXSymlink(POSIXTool): + __req__ = ['name', 'to'] + + def verify(self, entry, modlist): + rv = True + + try: + sloc = os.readlink(entry.get('name')) + if sloc != entry.get('to'): + entry.set('current_to', sloc) + msg = ("Symlink %s points to %s, should be %s" % + (entry.get('name'), sloc, entry.get('to'))) + self.logger.debug("POSIX: " + msg) + entry.set('qtext', "\n".join([entry.get('qtext', ''), msg])) + rv = False + except OSError: + self.logger.debug("POSIX: %s %s does not exist" % + (entry.tag, entry.get("name"))) + entry.set('current_exists', 'false') + return False + + return POSIXTool.verify(self, entry, modlist) and rv + + def install(self, entry): + ondisk = self._exists(entry, remove=True) + if ondisk: + self.logger.info("POSIX: Symlink %s cleanup failed" % + entry.get('name')) + try: + os.symlink(entry.get('to'), entry.get('name')) + rv = True + except OSError: + err = sys.exc_info()[1] + self.logger.error("POSIX: Failed to create symlink %s to %s: %s" % + (entry.get('name'), entry.get('to'), err)) + rv = False + return POSIXTool.install(self, entry) and rv + diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/__init__.py b/src/lib/Bcfg2/Client/Tools/POSIX/__init__.py new file mode 100644 index 000000000..46631eb06 --- /dev/null +++ b/src/lib/Bcfg2/Client/Tools/POSIX/__init__.py @@ -0,0 +1,151 @@ +"""All POSIX Type client support for Bcfg2.""" + +import os +import re +import sys +import shutil +import pkgutil +from datetime import datetime +import Bcfg2.Client.Tools +try: + from base import POSIXTool +except ImportError: + # py3k, incompatible syntax with py2.4 + exec("from .base import POSIXTool") + +class POSIX(Bcfg2.Client.Tools.Tool): + """POSIX File support code.""" + name = 'POSIX' + + def __init__(self, logger, setup, config): + Bcfg2.Client.Tools.Tool.__init__(self, logger, setup, config) + self.ppath = setup['ppath'] + self.max_copies = setup['max_copies'] + self._load_handlers() + self.logger.debug("POSIX: Handlers loaded: %s" % + (", ".join(self._handlers.keys()))) + self.__req__ = dict(Path=dict()) + for etype, hdlr in self._handlers.items(): + self.__req__['Path'][etype] = hdlr.__req__ + self.__handles__.append(('Path', etype)) + # Tool.__init__() sets up the list of handled entries, but we + # need to do it again after __handles__ has been populated. we + # can't populate __handles__ when the class is created because + # _load_handlers() _must_ be called at run-time, not at + # compile-time. + for struct in config: + self.handled = [e for e in struct if self.handlesEntry(e)] + + def _load_handlers(self): + # this must be called at run-time, not at compile-time, or we + # get wierd circular import issues. + self._handlers = dict() + if hasattr(pkgutil, 'walk_packages'): + submodules = pkgutil.walk_packages(path=__path__) + else: + # python 2.4 + import glob + submodules = [] + for path in __path__: + for submodule in glob.glob(os.path.join(path, "*.py")): + mod = os.path.splitext(os.path.basename(submodule))[0] + if mod not in ['__init__']: + submodules.append((None, mod, True)) + + for submodule in submodules: + if submodule[1] == 'base': + continue + module = getattr(__import__("%s.%s" % + (__name__, + submodule[1])).Client.Tools.POSIX, + submodule[1]) + hdlr = getattr(module, "POSIX" + submodule[1]) + if POSIXTool in hdlr.__mro__: + # figure out what entry type this handler handles + etype = hdlr.__name__[5:].lower() + self._handlers[etype] = hdlr(self.logger, + self.setup, + self.config) + + def canVerify(self, entry): + if not Bcfg2.Client.Tools.Tool.canVerify(self, entry): + return False + if not self._handlers[entry.get("type")].fully_specified(entry): + self.logger.error('POSIX: Cannot verify incomplete entry %s. ' + 'Try running bcfg2-lint.' % + entry.get('name')) + return False + return True + + def canInstall(self, entry): + """Check if entry is complete for installation.""" + if not Bcfg2.Client.Tools.Tool.canInstall(self, entry): + return False + if not self._handlers[entry.get("type")].fully_specified(entry): + self.logger.error('POSIX: Cannot install incomplete entry %s. ' + 'Try running bcfg2-lint.' % + entry.get('name')) + return False + return True + + def InstallPath(self, entry): + """Dispatch install to the proper method according to type""" + self.logger.debug("POSIX: Installing entry %s:%s:%s" % + (entry.tag, entry.get("type"), entry.get("name"))) + self._paranoid_backup(entry) + return self._handlers[entry.get("type")].install(entry) + + def VerifyPath(self, entry, modlist): + """Dispatch verify to the proper method according to type""" + self.logger.debug("POSIX: Verifying entry %s:%s:%s" % + (entry.tag, entry.get("type"), entry.get("name"))) + ret = self._handlers[entry.get("type")].verify(entry, modlist) + if self.setup['interactive'] and not ret: + entry.set('qtext', + '%s\nInstall %s %s: (y/N) ' % + (entry.get('qtext', ''), + entry.get('type'), entry.get('name'))) + return ret + + def _prune_old_backups(self, entry): + bkupnam = entry.get('name').replace('/', '_') + bkup_re = re.compile(bkupnam + \ + r'_\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{6}$') + # current list of backups for this file + try: + bkuplist = [f for f in os.listdir(self.ppath) if + bkup_re.match(f)] + except OSError: + err = sys.exc_info()[1] + self.logger.error("POSIX: Failed to create backup list in %s: %s" % + (self.ppath, err)) + return + bkuplist.sort() + while len(bkuplist) >= int(self.max_copies): + # remove the oldest backup available + oldest = bkuplist.pop(0) + self.logger.info("POSIX: Removing old backup %s" % oldest) + try: + os.remove(os.path.join(self.ppath, oldest)) + except OSError: + err = sys.exc_info()[1] + self.logger.error("POSIX: Failed to remove old backup %s: %s" % + (os.path.join(self.ppath, oldest), err)) + + def _paranoid_backup(self, entry): + if (entry.get("paranoid", 'false').lower() == 'true' and + self.setup.get("paranoid", False) and + entry.get('current_exists', 'true') == 'true' and + not os.path.isdir(entry.get("name"))): + self._prune_old_backups(entry) + bkupnam = "%s_%s" % (entry.get('name').replace('/', '_'), + datetime.isoformat(datetime.now())) + bfile = os.path.join(self.ppath, bkupnam) + try: + shutil.copy(entry.get('name'), bfile) + self.logger.info("POSIX: Backup of %s saved to %s" % + (entry.get('name'), bfile)) + except IOError: + err = sys.exc_info()[1] + self.logger.error("POSIX: Failed to create backup file for %s: " + "%s" % (entry.get('name'), err)) diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/base.py b/src/lib/Bcfg2/Client/Tools/POSIX/base.py new file mode 100644 index 000000000..6952d0f7b --- /dev/null +++ b/src/lib/Bcfg2/Client/Tools/POSIX/base.py @@ -0,0 +1,642 @@ +import os +import sys +import pwd +import grp +import stat +import shutil +import Bcfg2.Client.Tools +import Bcfg2.Client.XML + +try: + import selinux + has_selinux = True +except ImportError: + has_selinux = False + +try: + import posix1e + has_acls = True + + # map between permissions characters and numeric ACL constants + acl_map = dict(r=posix1e.ACL_READ, + w=posix1e.ACL_WRITE, + x=posix1e.ACL_EXECUTE) +except ImportError: + has_acls = False + acl_map = dict(r=4, w=2, x=1) + +# map between dev_type attribute and stat constants +device_map = dict(block=stat.S_IFBLK, + char=stat.S_IFCHR, + fifo=stat.S_IFIFO) + + +class POSIXTool(Bcfg2.Client.Tools.Tool): + def fully_specified(self, entry): + # checking is done by __req__ + return True + + def verify(self, entry, modlist): + if not self._verify_metadata(entry): + return False + if entry.get('recursive', 'false').lower() == 'true': + # verify ownership information recursively + for root, dirs, files in os.walk(entry.get('name')): + for p in dirs + files: + if not self._verify_metadata(entry, + path=os.path.join(root, p)): + return False + return True + + def install(self, entry): + plist = [entry.get('name')] + rv = True + rv &= self._set_perms(entry) + if entry.get('recursive', 'false').lower() == 'true': + # set metadata recursively + for root, dirs, files in os.walk(entry.get('name')): + for path in dirs + files: + rv &= self._set_perms(entry, path=os.path.join(root, path)) + return rv + + def _exists(self, entry, remove=False): + try: + # check for existing paths and optionally remove them + ondisk = os.lstat(entry.get('name')) + if remove: + if os.path.isdir(entry.get('name')): + rm = shutil.rmtree + else: + rm = os.unlink + try: + rm(entry.get('name')) + return False + except OSError: + err = sys.exc_info()[1] + self.logger.warning('POSIX: Failed to unlink %s: %s' % + (entry.get('name'), err)) + return ondisk # probably still exists + else: + return ondisk + except OSError: + return False + + def _set_perms(self, entry, path=None): + if path is None: + path = entry.get("name") + + rv = True + if entry.get("owner") and entry.get("group"): + try: + self.logger.debug("POSIX: Setting ownership of %s to %s:%s" % + (path, + self._norm_entry_uid(entry), + self._norm_entry_gid(entry))) + os.chown(path, self._norm_entry_uid(entry), + self._norm_entry_gid(entry)) + except KeyError: + self.logger.error('POSIX: Failed to change ownership of %s' % + path) + rv = False + os.chown(path, 0, 0) + except OSError: + self.logger.error('POSIX: Failed to change ownership of %s' % + path) + rv = False + + if entry.get("perms"): + configPerms = int(entry.get('perms'), 8) + if entry.get('dev_type'): + configPerms |= device_map[entry.get('dev_type')] + try: + self.logger.debug("POSIX: Setting permissions on %s to %s" % + (path, oct(configPerms))) + os.chmod(path, configPerms) + except (OSError, KeyError): + self.logger.error('POSIX: Failed to change permissions on %s' % + path) + rv = False + + if entry.get('mtime'): + try: + os.utime(entry.get('name'), (int(entry.get('mtime')), + int(entry.get('mtime')))) + except OSError: + self.logger.error("POSIX: Failed to set mtime of %s" % path) + rv = False + + rv &= self._set_secontext(entry, path=path) + rv &= self._set_acls(entry, path=path) + return rv + + + def _set_acls(self, entry, path=None): + """ set POSIX ACLs on the file on disk according to the config """ + if not has_acls: + if entry.findall("ACL"): + self.logger.debug("POSIX: ACLs listed for %s but no pylibacl " + "library installed" % entry.get('name')) + return True + + if path is None: + path = entry.get("name") + + try: + acl = posix1e.ACL(file=path) + except IOError: + err = sys.exc_info()[1] + if err.errno == 95: + # fs is mounted noacl + self.logger.error("POSIX: Cannot set ACLs on filesystem " + "mounted without ACL support: %s" % path) + else: + self.logger.error("POSIX: Error getting current ACLS on %s: %s" + % (path, err)) + return False + # clear ACLs out so we start fresh -- way easier than trying + # to add/remove/modify ACLs + for aclentry in acl: + if aclentry.tag_type in [posix1e.ACL_USER, posix1e.ACL_GROUP]: + acl.delete_entry(aclentry) + if os.path.isdir(path): + defacl = posix1e.ACL(filedef=path) + if not defacl.valid(): + # when a default ACL is queried on a directory that + # has no default ACL entries at all, you get an empty + # ACL, which is not valid. in this circumstance, we + # just copy the access ACL to get a base valid ACL + # that we can add things to. + defacl = posix1e.ACL(acl=acl) + else: + for aclentry in defacl: + if aclentry.tag_type in [posix1e.ACL_USER, + posix1e.ACL_GROUP]: + defacl.delete_entry(aclentry) + else: + defacl = None + + for aclkey, perms in self._list_entry_acls(entry).items(): + atype, scope, qualifier = aclkey + if atype == "default": + if defacl is None: + self.logger.warning("POSIX: Cannot set default ACLs on " + "non-directory %s" % path) + continue + entry = posix1e.Entry(defacl) + else: + entry = posix1e.Entry(acl) + for perm in acl_map.values(): + if perm & perms: + entry.permset.add(perm) + entry.tag_type = scope + try: + if scope == posix1e.ACL_USER: + scopename = "user" + entry.qualifier = self._norm_uid(qualifier) + elif scope == posix1e.ACL_GROUP: + scopename = "group" + entry.qualifier = self._norm_gid(qualifier) + except (OSError, KeyError): + err = sys.exc_info()[1] + self.logger.error("POSIX: Could not resolve %s %s: %s" % + (scopename, qualifier, err)) + continue + acl.calc_mask() + + def _apply_acl(acl, path, atype=posix1e.ACL_TYPE_ACCESS): + if atype == posix1e.ACL_TYPE_ACCESS: + atype_str = "access" + else: + atype_str = "default" + if acl.valid(): + self.logger.debug("POSIX: Applying %s ACL to %s:" % (atype_str, + path)) + for line in str(acl).splitlines(): + self.logger.debug(" " + line) + try: + acl.applyto(path, atype) + return True + except: + err = sys.exc_info()[1] + self.logger.error("POSIX: Failed to set ACLs on %s: %s" % + (path, err)) + return False + else: + self.logger.warning("POSIX: %s ACL created for %s was invalid:" + % (atype_str.title(), path)) + for line in str(acl).splitlines(): + self.logger.warning(" " + line) + return False + + rv = _apply_acl(acl, path) + if defacl: + defacl.calc_mask() + rv &= _apply_acl(defacl, path, posix1e.ACL_TYPE_DEFAULT) + return rv + + def _set_secontext(self, entry, path=None): + """ set the SELinux context of the file on disk according to the + config""" + if not has_selinux: + return True + + if path is None: + path = entry.get("name") + context = entry.get("secontext") + if context is None: + # no context listed + return True + + if context == '__default__': + try: + selinux.restorecon(path) + rv = True + except: + err = sys.exc_info()[1] + self.logger.error("POSIX: Failed to restore SELinux context " + "for %s: %s" % (path, err)) + rv = False + else: + try: + rv = selinux.lsetfilecon(path, context) == 0 + except: + err = sys.exc_info()[1] + self.logger.error("POSIX: Failed to restore SELinux context " + "for %s: %s" % (path, err)) + rv = False + return rv + + def _norm_gid(self, gid): + """ This takes a group name or gid and returns the + corresponding gid. """ + try: + return int(gid) + except ValueError: + return int(grp.getgrnam(gid)[2]) + + def _norm_entry_gid(self, entry): + try: + return self._norm_gid(entry.get('group')) + except (OSError, KeyError): + err = sys.exc_info()[1] + self.logger.error('POSIX: GID normalization failed for %s on %s: %s' + % (entry.get('group'), entry.get('name'), err)) + return 0 + + def _norm_uid(self, uid): + """ This takes a username or uid and returns the + corresponding uid. """ + try: + return int(uid) + except ValueError: + return int(pwd.getpwnam(uid)[2]) + + def _norm_entry_uid(self, entry): + try: + return self._norm_uid(entry.get("owner")) + except (OSError, KeyError): + err = sys.exc_info()[1] + self.logger.error('POSIX: UID normalization failed for %s on %s: %s' + % (entry.get('owner'), entry.get('name'), err)) + return 0 + + def _norm_acl_perms(self, perms): + """ takes a representation of an ACL permset and returns a digit + representing the permissions entailed by it. representations can + either be a single octal digit, a string of up to three 'r', + 'w', 'x', or '-' characters, or a posix1e.Permset object""" + if hasattr(perms, 'test'): + # Permset object + return sum([p for p in acl_map.values() + if perms.test(p)]) + + try: + # single octal digit + rv = int(perms) + if rv > 0 and rv < 8: + return rv + else: + self.logger.error("POSIX: Permissions digit out of range in " + "ACL: %s" % perms) + return 0 + except ValueError: + # couldn't be converted to an int; process as a string + if len(perms) > 3: + self.logger.error("POSIX: Permissions string too long in ACL: " + "%s" % perms) + return 0 + rv = 0 + for char in perms: + if char == '-': + continue + elif char not in acl_map: + self.logger.warning("POSIX: Unknown permissions character " + "in ACL: %s" % char) + elif rv & acl_map[char]: + self.logger.warning("POSIX: Duplicate permissions " + "character in ACL: %s" % perms) + else: + rv |= acl_map[char] + return rv + + def _acl2string(self, aclkey, perms): + atype, scope, qualifier = aclkey + acl_str = [] + if atype == 'default': + acl_str.append(atype) + if scope == posix1e.ACL_USER: + acl_str.append("user") + elif scope == posix1e.ACL_GROUP: + acl_str.append("group") + acl_str.append(qualifier) + acl_str.append(self._acl_perm2string(perms)) + return ":".join(acl_str) + + def _acl_perm2string(self, perm): + rv = [] + for char in 'rwx': + if acl_map[char] & perm: + rv.append(char) + else: + rv.append('-') + return ''.join(rv) + + def _gather_data(self, path): + try: + ondisk = os.stat(path) + except OSError: + self.logger.debug("POSIX: %s does not exist" % path) + return (False, None, None, None, None, None) + + try: + owner = str(ondisk[stat.ST_UID]) + except OSError: + err = sys.exc_info()[1] + self.logger.debug("POSIX: Could not get current owner of %s: %s" % + (path, err)) + owner = None + except KeyError: + self.logger.error('POSIX: User resolution failed for %s' % path) + owner = None + + try: + group = str(ondisk[stat.ST_GID]) + except (OSError, KeyError): + err = sys.exc_info()[1] + self.logger.debug("POSIX: Could not get current group of %s: %s" % + (path, err)) + group = None + except KeyError: + self.logger.error('POSIX: Group resolution failed for %s' % path) + group = None + + try: + perms = oct(ondisk[stat.ST_MODE])[-4:] + except (OSError, KeyError, TypeError): + err = sys.exc_info()[1] + self.logger.debug("POSIX: Could not get current permissions of %s: " + "%s" % (path, err)) + perms = None + + if has_selinux: + try: + secontext = selinux.getfilecon(path)[1].split(":")[2] + except (OSError, KeyError): + err = sys.exc_info()[1] + self.logger.debug("POSIX: Could not get current SELinux " + "context of %s: %s" % (path, err)) + secontext = None + else: + secontext = None + + if has_acls: + acls = self._list_file_acls(path) + else: + acls = None + return (ondisk, owner, group, perms, secontext, acls) + + def _verify_metadata(self, entry, path=None): + """ generic method to verify perms, owner, group, secontext, acls, + and mtime """ + # allow setting an alternate path for recursive permissions checking + if path is None: + path = entry.get('name') + attrib = dict() + ondisk, attrib['current_owner'], attrib['current_group'], \ + attrib['current_perms'], attrib['current_secontext'], acls = \ + self._gather_data(path) + + if not ondisk: + entry.set('current_exists', 'false') + return False + + # we conditionally verify every bit of metadata only if it's + # specified on the entry. consequently, canVerify() and + # fully_specified() are preconditions of _verify_metadata(), + # since they will ensure that everything that needs to be + # specified actually is. this lets us gracefully handle + # symlink and hardlink entries, which have SELinux contexts + # but not other permissions, optional secontext and mtime + # attrs, and so on. + configOwner, configGroup, configPerms, mtime = None, None, None, -1 + if entry.get('mtime', '-1') != '-1': + mtime = str(ondisk[stat.ST_MTIME]) + if entry.get("owner"): + configOwner = str(self._norm_entry_uid(entry)) + if entry.get("group"): + configGroup = str(self._norm_entry_gid(entry)) + if entry.get("perms"): + while len(entry.get('perms', '')) < 4: + entry.set('perms', '0' + entry.get('perms', '')) + configPerms = int(entry.get('perms'), 8) + + errors = [] + if configOwner and attrib['current_owner'] != configOwner: + errors.append("Owner for path %s is incorrect. " + "Current owner is %s but should be %s" % + (path, attrib['current_owner'], entry.get('owner'))) + + if configGroup and attrib['current_group'] != configGroup: + errors.append("Group for path %s is incorrect. " + "Current group is %s but should be %s" % + (path, attrib['current_group'], entry.get('group'))) + + if (configPerms and + oct(int(attrib['current_perms'], 8)) != oct(configPerms)): + errors.append("Permissions for path %s are incorrect. " + "Current permissions are %s but should be %s" % + (path, attrib['current_perms'], entry.get('perms'))) + + if entry.get('mtime'): + attrib['current_mtime'] = mtime + if mtime != entry.get('mtime', '-1'): + errors.append("mtime for path %s is incorrect. " + "Current mtime is %s but should be %s" % + (path, mtime, entry.get('mtime'))) + + if has_selinux and entry.get("secontext"): + if entry.get("secontext") == "__default__": + configContext = selinux.matchpathcon(path, 0)[1].split(":")[2] + else: + configContext = entry.get("secontext") + if attrib['current_secontext'] != configContext: + errors.append("SELinux context for path %s is incorrect. " + "Current context is %s but should be %s" % + (path, attrib['current_secontext'], + configContext)) + + if errors: + for error in errors: + self.logger.debug("POSIX: " + error) + entry.set('qtext', "\n".join([entry.get('qtext', '')] + errors)) + if path == entry.get("name"): + for attr, val in attrib.items(): + if val is not None: + entry.set(attr, str(val)) + + aclVerifies = self._verify_acls(entry, path=path) + return aclVerifies and len(errors) == 0 + + def _list_entry_acls(self, entry): + wanted = dict() + for acl in entry.findall("ACL"): + if acl.get("scope") == "user": + scope = posix1e.ACL_USER + elif acl.get("scope") == "group": + scope = posix1e.ACL_GROUP + else: + self.logger.error("POSIX: Unknown ACL scope %s" % + acl.get("scope")) + continue + wanted[(acl.get("type"), scope, acl.get(acl.get("scope")))] = \ + self._norm_acl_perms(acl.get('perms')) + return wanted + + def _list_file_acls(self, path): + def _process_acl(acl, atype): + try: + if acl.tag_type == posix1e.ACL_USER: + qual = pwd.getpwuid(acl.qualifier)[0] + elif acl.tag_type == posix1e.ACL_GROUP: + qual = grp.getgrgid(acl.qualifier)[0] + else: + return + except (OSError, KeyError): + err = sys.exc_info()[1] + self.logger.error("POSIX: Lookup of %s %s failed: %s" % + (scope, acl.qualifier, err)) + qual = acl.qualifier + existing[(atype, acl.tag_type, qual)] = \ + self._norm_acl_perms(acl.permset) + + existing = dict() + try: + for acl in posix1e.ACL(file=path): + _process_acl(acl, "access") + except IOError: + err = sys.exc_info()[1] + if err.errno == 95: + # fs is mounted noacl + self.logger.debug("POSIX: Filesystem mounted without ACL " + "support: %s" % path) + else: + self.logger.error("POSIX: Error getting current ACLS on %s: %s" + % (path, err)) + return existing + + if os.path.isdir(path): + for acl in posix1e.ACL(filedef=path): + _process_acl(acl, "default") + return existing + + def _verify_acls(self, entry, path=None): + if not has_acls: + if entry.findall("ACL"): + self.logger.debug("POSIX: ACLs listed for %s but no pylibacl " + "library installed" % entry.get('name')) + return True + + if path is None: + path = entry.get("name") + + # create lists of normalized representations of the ACLs we want + # and the ACLs we have. this will make them easier to compare + # than trying to mine that data out of the ACL objects and XML + # objects and compare it at the same time. + wanted = self._list_entry_acls(entry) + existing = self._list_file_acls(path) + + missing = [] + extra = [] + wrong = [] + for aclkey, perms in wanted.items(): + if aclkey not in existing: + missing.append(self._acl2string(aclkey, perms)) + elif existing[aclkey] != perms: + wrong.append((self._acl2string(aclkey, perms), + self._acl2string(aclkey, existing[aclkey]))) + if path == entry.get("name"): + atype, scope, qual = aclkey + aclentry = Bcfg2.Client.XML.Element("ACL", type=atype, + perms=str(perms)) + if scope == posix1e.ACL_USER: + aclentry.set("scope", "user") + elif scope == posix1e.ACL_GROUP: + aclentry.set("scope", "group") + else: + self.logger.debug("POSIX: Unknown ACL scope %s on %s" % + (scope, path)) + continue + aclentry.set(aclentry.get("scope"), qual) + entry.append(aclentry) + + for aclkey, perms in existing.items(): + if aclkey not in wanted: + extra.append(self._acl2string(aclkey, perms)) + + msg = [] + if missing: + msg.append("%s ACLs are missing: %s" % (len(missing), + ", ".join(missing))) + if wrong: + msg.append("%s ACLs are wrong: %s" % + (len(wrong), + "; ".join(["%s should be %s" % (e, w) + for w, e in wrong]))) + if extra: + msg.append("%s extra ACLs: %s" % (len(extra), ", ".join(extra))) + + if msg: + msg.insert(0, "POSIX: ACLs for %s are incorrect." % path) + self.logger.debug(msg[0]) + for line in msg[1:]: + self.logger.debug(" " + line) + entry.set('qtext', "\n".join([entry.get("qtext", '')] + msg)) + return False + return True + + def _makedirs(self, entry, path=None): + """ os.makedirs helpfully creates all parent directories for + us, but it sets permissions according to umask, which is + probably wrong. we need to find out which directories were + created and set permissions on those + (http://trac.mcs.anl.gov/projects/bcfg2/ticket/1125) """ + created = [] + if path is None: + path = entry.get("name") + cur = path + while cur != '/': + if not os.path.exists(cur): + created.append(cur) + cur = os.path.dirname(cur) + rv = True + try: + os.makedirs(path) + except OSError: + err = sys.exc_info()[1] + self.logger.error('POSIX: Failed to create directory %s: %s' % + (path, err)) + rv = False + for cpath in created: + rv &= self._set_perms(entry, path=cpath) + return rv diff --git a/src/lib/Bcfg2/Client/Tools/Portage.py b/src/lib/Bcfg2/Client/Tools/Portage.py index 4516f419d..36d48b8d3 100644 --- a/src/lib/Bcfg2/Client/Tools/Portage.py +++ b/src/lib/Bcfg2/Client/Tools/Portage.py @@ -2,8 +2,6 @@ import re import Bcfg2.Client.Tools -from Bcfg2.Bcfg2Py3k import ConfigParser - class Portage(Bcfg2.Client.Tools.PkgTool): """The Gentoo toolset implements package and service operations and @@ -27,30 +25,11 @@ class Portage(Bcfg2.Client.Tools.PkgTool): self._ebuild_pattern = re.compile('(ebuild|binary)') self.cfg = cfg self.installed = {} - self._binpkgonly = True - - # Used to get options from configuration file - parser = ConfigParser.ConfigParser() - parser.read(self.setup.get('setup')) - for opt in ['binpkgonly']: - if parser.has_option(self.name, opt): - setattr(self, ('_%s' % opt), - self._StrToBoolIfBool(parser.get(self.name, opt))) - + self._binpkgonly = self.setup.get('portage_binpkgonly', False) if self._binpkgonly: self.pkgtool = self._binpkgtool self.RefreshPackages() - def _StrToBoolIfBool(self, s): - """Returns a boolean if the string specifies a boolean value. - Returns a string otherwise""" - if s.lower() in ('true', 'yes', 't', 'y', '1'): - return True - elif s.lower() in ('false', 'no', 'f', 'n', '0'): - return False - else: - return s - def RefreshPackages(self): """Refresh memory hashes of packages.""" if not self._initialised: @@ -83,8 +62,8 @@ class Portage(Bcfg2.Client.Tools.PkgTool): entry.set('current_version', version) if not self.setup['quick']: - if ('verify' not in entry.attrib) or \ - self._StrToBoolIfBool(entry.get('verify')): + if ('verify' not in entry.attrib or + entry.get('verify').lower == 'true'): # Check the package if: # - Not running in quick mode diff --git a/src/lib/Bcfg2/Client/Tools/RPMng.py b/src/lib/Bcfg2/Client/Tools/RPMng.py index 00dd00d71..91e2180ae 100644 --- a/src/lib/Bcfg2/Client/Tools/RPMng.py +++ b/src/lib/Bcfg2/Client/Tools/RPMng.py @@ -4,8 +4,6 @@ import os.path import rpm import rpmtools import Bcfg2.Client.Tools -# Compatibility import -from Bcfg2.Bcfg2Py3k import ConfigParser class RPMng(Bcfg2.Client.Tools.PkgTool): """Support for RPM packages.""" @@ -44,82 +42,42 @@ class RPMng(Bcfg2.Client.Tools.PkgTool): self.modlists = {} self.gpg_keyids = self.getinstalledgpg() - # Process thee RPMng section from the config file. - RPMng_CP = ConfigParser.ConfigParser() - RPMng_CP.read(self.setup.get('setup')) - - # installonlypackages - self.installOnlyPkgs = [] - if RPMng_CP.has_option(self.name, 'installonlypackages'): - for i in RPMng_CP.get(self.name, 'installonlypackages').split(','): - self.installOnlyPkgs.append(i.strip()) - if self.installOnlyPkgs == []: - self.installOnlyPkgs = ['kernel', 'kernel-bigmem', 'kernel-enterprise', 'kernel-smp', - 'kernel-modules', 'kernel-debug', 'kernel-unsupported', - 'kernel-source', 'kernel-devel', 'kernel-default', - 'kernel-largesmp-devel', 'kernel-largesmp', 'kernel-xen', - 'gpg-pubkey'] + opt_prefix = self.name.lower() + self.installOnlyPkgs = self.setup["%s_installonly" % opt_prefix] if 'gpg-pubkey' not in self.installOnlyPkgs: self.installOnlyPkgs.append('gpg-pubkey') - self.logger.debug('installOnlyPackages = %s' % self.installOnlyPkgs) - - # erase_flags - self.erase_flags = [] - if RPMng_CP.has_option(self.name, 'erase_flags'): - for i in RPMng_CP.get(self.name, 'erase_flags').split(','): - self.erase_flags.append(i.strip()) - if self.erase_flags == []: - self.erase_flags = ['allmatches'] - self.logger.debug('erase_flags = %s' % self.erase_flags) - - # pkg_checks - if RPMng_CP.has_option(self.name, 'pkg_checks'): - self.pkg_checks = RPMng_CP.get(self.name, 'pkg_checks').lower() - else: - self.pkg_checks = 'true' - self.logger.debug('pkg_checks = %s' % self.pkg_checks) - - # pkg_verify - if RPMng_CP.has_option(self.name, 'pkg_verify'): - self.pkg_verify = RPMng_CP.get(self.name, 'pkg_verify').lower() - else: - self.pkg_verify = 'true' - self.logger.debug('pkg_verify = %s' % self.pkg_verify) - - # installed_action - if RPMng_CP.has_option(self.name, 'installed_action'): - self.installed_action = RPMng_CP.get(self.name, 'installed_action').lower() - else: - self.installed_action = 'install' - self.logger.debug('installed_action = %s' % self.installed_action) - - # version_fail_action - if RPMng_CP.has_option(self.name, 'version_fail_action'): - self.version_fail_action = RPMng_CP.get(self.name, 'version_fail_action').lower() - else: - self.version_fail_action = 'upgrade' - self.logger.debug('version_fail_action = %s' % self.version_fail_action) - - # verify_fail_action - if self.name == "RPMng": - if RPMng_CP.has_option(self.name, 'verify_fail_action'): - self.verify_fail_action = RPMng_CP.get(self.name, 'verify_fail_action').lower() - else: - self.verify_fail_action = 'reinstall' - else: # yum can't reinstall packages. - self.verify_fail_action = 'none' - self.logger.debug('verify_fail_action = %s' % self.verify_fail_action) - - # version_fail_action - if RPMng_CP.has_option(self.name, 'verify_flags'): - self.verify_flags = RPMng_CP.get(self.name, 'verify_flags').lower().split(',') - else: - self.verify_flags = [] + self.erase_flags = self.setup['%s_erase_flags' % opt_prefix] + self.pkg_checks = self.setup['%s_pkg_checks' % opt_prefix] + self.pkg_verify = self.setup['%s_pkg_verify' % opt_prefix] + self.installed_action = self.setup['%s_installed_action' % opt_prefix] + self.version_fail_action = self.setup['%s_version_fail_action' % + opt_prefix] + self.verify_fail_action = self.setup['%s_verify_fail_action' % + opt_prefix] + self.verify_flags = self.setup['%s_verify_flags' % opt_prefix] if '' in self.verify_flags: self.verify_flags.remove('') - self.logger.debug('version_fail_action = %s' % self.version_fail_action) + + self.logger.debug('%s: installOnlyPackages = %s' % + (self.name, self.installOnlyPkgs)) + self.logger.debug('%s: erase_flags = %s' % + (self.name, self.erase_flags)) + self.logger.debug('%s: pkg_checks = %s' % + (self.name, self.pkg_checks)) + self.logger.debug('%s: pkg_verify = %s' % + (self.name, self.pkg_verify)) + self.logger.debug('%s: installed_action = %s' % + (self.name, self.installed_action)) + self.logger.debug('%s: version_fail_action = %s' % + (self.name, self.version_fail_action)) + self.logger.debug('%s: verify_fail_action = %s' % + (self.name, self.verify_fail_action)) + self.logger.debug('%s: verify_flags = %s' % + (self.name, self.verify_flags)) + # Force a re- prelink of all packages if prelink exists. - # Many, if not most package verifies can be caused by out of date prelinking. + # Many, if not most package verifies can be caused by out of + # date prelinking. if os.path.isfile('/usr/sbin/prelink') and not self.setup['dryrun']: cmdrc, output = self.cmd.run('/usr/sbin/prelink -a -mR') if cmdrc == 0: @@ -193,7 +151,8 @@ class RPMng(Bcfg2.Client.Tools.PkgTool): instance = Bcfg2.Client.XML.SubElement(entry, 'Package') for attrib in list(entry.attrib.keys()): instance.attrib[attrib] = entry.attrib[attrib] - if self.pkg_checks == 'true' and entry.get('pkg_checks', 'true') == 'true': + if (self.pkg_checks and + entry.get('pkg_checks', 'true').lower() == 'true'): if 'any' in [entry.get('version'), pinned_version]: version, release = 'any', 'any' elif entry.get('version') == 'auto': @@ -215,7 +174,8 @@ class RPMng(Bcfg2.Client.Tools.PkgTool): if entry.get('name') in self.installed: # There is at least one instance installed. - if self.pkg_checks == 'true' and entry.get('pkg_checks', 'true') == 'true': + if (self.pkg_checks and + entry.get('pkg_checks', 'true').lower() == 'true'): rpmTs = rpm.TransactionSet() rpmHeader = None for h in rpmTs.dbMatch(rpm.RPMTAG_NAME, entry.get('name')): @@ -243,8 +203,8 @@ class RPMng(Bcfg2.Client.Tools.PkgTool): self.logger.debug(" %s" % self.str_evra(inst)) self.instance_status[inst]['installed'] = True - if self.pkg_verify == 'true' and \ - inst.get('pkg_verify', 'true') == 'true': + if (self.pkg_verify and + inst.get('pkg_verify', 'true').lower() == 'true'): flags = inst.get('verify_flags', '').split(',') + self.verify_flags if pkg.get('gpgkeyid', '')[-8:] not in self.gpg_keyids and \ entry.get('name') != 'gpg-pubkey': @@ -302,8 +262,8 @@ class RPMng(Bcfg2.Client.Tools.PkgTool): self.logger.debug(" %s" % self.str_evra(inst)) self.instance_status[inst]['installed'] = True - if self.pkg_verify == 'true' and \ - inst.get('pkg_verify', 'true') == 'true': + if (self.pkg_verify and + inst.get('pkg_verify', 'true').lower() == 'true'): flags = inst.get('verify_flags', '').split(',') + self.verify_flags if pkg.get('gpgkeyid', '')[-8:] not in self.gpg_keyids and \ 'nosignature' not in flags: @@ -520,7 +480,7 @@ class RPMng(Bcfg2.Client.Tools.PkgTool): self.extra = self.FindExtraPackages() def FixInstance(self, instance, inst_status): - """" + """ Control if a reinstall of a package happens or not based on the results from RPMng.VerifyPackage(). @@ -824,8 +784,8 @@ class RPMng(Bcfg2.Client.Tools.PkgTool): return False # We don't want to do any checks so we don't care what the entry has in it. - if self.pkg_checks == 'false' or \ - entry.get('pkg_checks', 'true').lower() == 'false': + if (not self.pkg_checks or + entry.get('pkg_checks', 'true').lower() == 'false'): return True instances = entry.findall('Instance') diff --git a/src/lib/Bcfg2/Client/Tools/RcUpdate.py b/src/lib/Bcfg2/Client/Tools/RcUpdate.py index 1b9a29478..ddf9c1f2d 100644 --- a/src/lib/Bcfg2/Client/Tools/RcUpdate.py +++ b/src/lib/Bcfg2/Client/Tools/RcUpdate.py @@ -23,22 +23,18 @@ class RcUpdate(Bcfg2.Client.Tools.SvcTool): rc = self.cmd.run(cmd % entry.get('name'))[0] is_enabled = (rc == 0) - if entry.get('mode', 'default') == 'supervised': - # check if init script exists - 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 + # check if init script exists + 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 - # check if service is enabled - cmd = '/etc/init.d/%s status | grep started' - rc = self.cmd.run(cmd % entry.attrib['name'])[0] - is_running = (rc == 0) - else: - # we don't care - is_running = is_enabled + # check if service is enabled + cmd = '/etc/init.d/%s status | grep started' + rc = self.cmd.run(cmd % entry.attrib['name'])[0] + is_running = (rc == 0) if entry.get('status') == 'on' and not (is_enabled and is_running): entry.set('current_status', 'off') @@ -53,19 +49,11 @@ class RcUpdate(Bcfg2.Client.Tools.SvcTool): def InstallService(self, entry): """ Install Service entry - In supervised mode we also take care it's (not) running. """ - # don't take any actions for mode='manual' - if entry.get('mode', 'default') == 'manual': - self.logger.info("Service %s mode set to manual. Skipping " - "installation." % (entry.get('name'))) - return False self.logger.info('Installing Service %s' % entry.get('name')) if entry.get('status') == 'on': - # make sure it's running if in supervised mode - if entry.get('mode', 'default') == 'supervised' \ - and entry.get('current_status') == 'off': + if entry.get('current_status') == 'off': self.start_service(entry) # make sure it's enabled cmd = '/sbin/rc-update add %s default' @@ -73,9 +61,7 @@ class RcUpdate(Bcfg2.Client.Tools.SvcTool): return (rc == 0) elif entry.get('status') == 'off': - # make sure it's not running if in supervised mode - if entry.get('mode', 'default') == 'supervised' \ - and entry.get('current_status') == 'on': + if entry.get('current_status') == 'on': self.stop_service(entry) # make sure it's disabled cmd = '/sbin/rc-update del %s default' diff --git a/src/lib/Bcfg2/Client/Tools/SELinux.py b/src/lib/Bcfg2/Client/Tools/SELinux.py new file mode 100644 index 000000000..1c0db904b --- /dev/null +++ b/src/lib/Bcfg2/Client/Tools/SELinux.py @@ -0,0 +1,716 @@ +import os +import re +import sys +import copy +import glob +import struct +import socket +import selinux +import seobject +import Bcfg2.Client.XML +import Bcfg2.Client.Tools +import Bcfg2.Client.Tools.POSIX + +def pack128(int_val): + """ pack a 128-bit integer in big-endian format """ + max_int = 2 ** (128) - 1 + max_word_size = 2 ** 32 - 1 + + if int_val <= max_word_size: + return struct.pack('>L', int_val) + + words = [] + for i in range(4): + word = int_val & max_word_size + words.append(int(word)) + int_val >>= 32 + words.reverse() + return struct.pack('>4I', *words) + +def netmask_itoa(netmask, proto="ipv4"): + """ convert an integer netmask (e.g., /16) to dotted-quad + notation (255.255.0.0) or IPv6 prefix notation (ffff::) """ + if proto == "ipv4": + size = 32 + family = socket.AF_INET + else: # ipv6 + size = 128 + family = socket.AF_INET6 + try: + int(netmask) + except ValueError: + return netmask + + if netmask > size: + raise ValueError("Netmask too large: %s" % netmask) + + res = 0L + for n in range(netmask): + res |= 1 << (size - n - 1) + netmask = socket.inet_ntop(family, pack128(res)) + return netmask + + +class SELinux(Bcfg2.Client.Tools.Tool): + """ SELinux boolean and module support """ + name = 'SELinux' + __handles__ = [('SELinux', 'boolean'), + ('SELinux', 'port'), + ('SELinux', 'fcontext'), + ('SELinux', 'node'), + ('SELinux', 'login'), + ('SELinux', 'user'), + ('SELinux', 'interface'), + ('SELinux', 'permissive'), + ('SELinux', 'module')] + __req__ = dict(SELinux=dict(boolean=['name', 'value'], + module=['name'], + port=['name', 'selinuxtype'], + fcontext=['name', 'selinuxtype'], + node=['name', 'selinuxtype', 'proto'], + login=['name', 'selinuxuser'], + user=['name', 'roles', 'prefix'], + interface=['name', 'selinuxtype'], + permissive=['name'])) + + def __init__(self, logger, setup, config): + Bcfg2.Client.Tools.Tool.__init__(self, logger, setup, config) + self.handlers = {} + for handles in self.__handles__: + etype = handles[1] + self.handlers[etype] = \ + globals()["SELinux%sHandler" % etype.title()](self, logger, + setup, config) + + def BundleUpdated(self, _, states): + for handler in self.handlers.values(): + handler.BundleUpdated(states) + + def FindExtra(self): + extra = [] + for handler in self.handlers.values(): + extra.extend(handler.FindExtra()) + return extra + + def canInstall(self, entry): + return (Bcfg2.Client.Tools.Tool.canInstall(self, entry) and + self.handlers[entry.get('type')].canInstall(entry)) + + def primarykey(self, entry): + """ return a string that should be unique amongst all entries + in the specification """ + return self.handlers[entry.get('type')].primarykey(entry) + + def Install(self, entries, states): + # start a transaction + sr = seobject.semanageRecords("") + if hasattr(sr, "start"): + self.logger.debug("Starting SELinux transaction") + sr.start() + else: + self.logger.debug("SELinux transactions not supported; this may " + "slow things down considerably") + Bcfg2.Client.Tools.Tool.Install(self, entries, states) + if hasattr(sr, "finish"): + self.logger.debug("Committing SELinux transaction") + sr.finish() + + def InstallSELinux(self, entry): + """Dispatch install to the proper method according to type""" + return self.handlers[entry.get('type')].Install(entry) + + def VerifySELinux(self, entry, _): + """Dispatch verify to the proper method according to type""" + rv = self.handlers[entry.get('type')].Verify(entry) + if entry.get('qtext') and self.setup['interactive']: + entry.set('qtext', + '%s\nInstall SELinux %s %s: (y/N) ' % + (entry.get('qtext'), + entry.get('type'), + self.handlers[entry.get('type')].tostring(entry))) + return rv + + def Remove(self, entries): + """Dispatch verify to the proper removal method according to type""" + # sort by type + types = list() + for entry in entries: + if entry.get('type') not in types: + types.append(entry.get('type')) + + for etype in types: + self.handlers[entry.get('type')].Remove([e for e in entries + if e.get('type') == etype]) + + +class SELinuxEntryHandler(object): + etype = None + key_format = ("name",) + value_format = () + str_format = '%(name)s' + custom_re = re.compile(' (?P<name>\S+)$') + custom_format = None + + def __init__(self, tool, logger, setup, config): + self.tool = tool + self.logger = logger + self._records = None + self._all = None + if not self.custom_format: + self.custom_format = self.key_format + + @property + def records(self): + if self._records is None: + self._records = getattr(seobject, "%sRecords" % self.etype)("") + return self._records + + @property + def all_records(self): + if self._all is None: + self._all = self.records.get_all() + return self._all + + @property + def custom_records(self): + if hasattr(self.records, "customized") and self.custom_re: + return dict([(k, self.all_records[k]) for k in self.custom_keys]) + else: + # ValueError is really a pretty dumb exception to raise, + # but that's what the seobject customized() method raises + # if it's defined but not implemented. yeah, i know, wtf. + raise ValueError("custom_records") + + @property + def custom_keys(self): + keys = [] + for cmd in self.records.customized(): + match = self.custom_re.search(cmd) + if match: + if (len(self.custom_format) == 1 and + self.custom_format[0] == "name"): + keys.append(match.group("name")) + else: + keys.append(tuple([match.group(k) + for k in self.custom_format])) + return keys + + def tostring(self, entry): + return self.str_format % entry.attrib + + def keytostring(self, key): + return self.str_format % self._key2attrs(key) + + def _key(self, entry): + if len(self.key_format) == 1 and self.key_format[0] == "name": + return entry.get("name") + else: + rv = [] + for key in self.key_format: + rv.append(entry.get(key)) + return tuple(rv) + + def _key2attrs(self, key): + if isinstance(key, tuple): + rv = dict((self.key_format[i], key[i]) + for i in range(len(self.key_format)) + if self.key_format[i]) + else: + rv = dict(name=key) + if self.value_format: + vals = self.all_records[key] + rv.update(dict((self.value_format[i], vals[i]) + for i in range(len(self.value_format)) + if self.value_format[i])) + return rv + + def key2entry(self, key): + attrs = self._key2attrs(key) + attrs["type"] = self.etype + return Bcfg2.Client.XML.Element("SELinux", **attrs) + + def _args(self, entry, method): + if hasattr(self, "_%sargs" % method): + return getattr(self, "_%sargs" % method)(entry) + elif hasattr(self, "_defaultargs"): + # default args + return self._defaultargs(entry) + else: + raise NotImplementedError + + def _deleteargs(self, entry): + return (self._key(entry)) + + def canInstall(self, entry): + return bool(self._key(entry)) + + def primarykey(self, entry): + return ":".join([entry.tag, entry.get("type"), entry.get("name")]) + + def exists(self, entry): + if self._key(entry) not in self.all_records: + self.logger.debug("SELinux %s %s does not exist" % + (self.etype, self.tostring(entry))) + return False + return True + + def Verify(self, entry): + if not self.exists(entry): + entry.set('current_exists', 'false') + return False + + errors = [] + current_attrs = self._key2attrs(self._key(entry)) + desired_attrs = entry.attrib + for attr in self.value_format: + if not attr: + continue + if current_attrs[attr] != desired_attrs[attr]: + entry.set('current_%s' % attr, current_attrs[attr]) + errors.append("SELinux %s %s has wrong %s: %s, should be %s" % + (self.etype, self.tostring(entry), attr, + current_attrs[attr], desired_attrs[attr])) + + if errors: + for error in errors: + self.logger.debug(error) + entry.set('qtext', "\n".join([entry.get('qtext', '')] + errors)) + return False + else: + return True + + def Install(self, entry, method=None): + if not method: + if self.exists(entry): + method = "modify" + else: + method = "add" + self.logger.debug("%s SELinux %s %s" % + (method.title(), self.etype, self.tostring(entry))) + + try: + getattr(self.records, method)(*self._args(entry, method)) + self._all = None + return True + except ValueError: + err = sys.exc_info()[1] + self.logger.debug("Failed to %s SELinux %s %s: %s" % + (method, self.etype, self.tostring(entry), err)) + return False + + def Remove(self, entries): + for entry in entries: + try: + self.records.delete(*self._args(entry, "delete")) + self._all = None + except ValueError: + err = sys.exc_info()[1] + self.logger.info("Failed to remove SELinux %s %s: %s" % + (self.etype, self.tostring(entry), err)) + + def FindExtra(self): + specified = [self._key(e) + for e in self.tool.getSupportedEntries() + if e.get("type") == self.etype] + try: + records = self.custom_records + except ValueError: + records = self.all_records + return [self.key2entry(key) + for key in records.keys() + if key not in specified] + + def BundleUpdated(self, states): + pass + + +class SELinuxBooleanHandler(SELinuxEntryHandler): + etype = "boolean" + value_format = ("value",) + + @property + def all_records(self): + # older versions of selinux return a single 0/1 value for each + # bool, while newer versions return a list of three 0/1 values + # representing various states. we don't care about the latter + # two values, but it's easier to coerce the older format into + # the newer format as far as interoperation with the rest of + # SELinuxEntryHandler goes + rv = SELinuxEntryHandler.all_records.fget(self) + if rv.values()[0] in [0, 1]: + for key, val in rv.items(): + rv[key] = [val, val, val] + return rv + + def _key2attrs(self, key): + rv = SELinuxEntryHandler._key2attrs(self, key) + status = self.all_records[key][0] + if status: + rv['value'] = "on" + else: + rv['value'] = "off" + return rv + + def _defaultargs(self, entry): + # the only values recognized by both new and old versions of + # selinux are the strings "0" and "1". old selinux accepts + # ints or bools as well, new selinux accepts "on"/"off" + if entry.get("value").lower() == "on": + value = "1" + else: + value = "0" + return (entry.get("name"), value) + + def canInstall(self, entry): + if entry.get("value").lower() not in ["on", "off"]: + self.logger.debug("SELinux %s %s has a bad value: %s" % + (self.etype, self.tostring(entry), + entry.get("value"))) + return False + return (self.exists(entry) and + SELinuxEntryHandler.canInstall(self, entry)) + + +class SELinuxPortHandler(SELinuxEntryHandler): + etype = "port" + value_format = ('selinuxtype', None) + custom_re = re.compile(r'-p (?P<proto>tcp|udp).*? (?P<start>\d+)(?:-(?P<end>\d+))?$') + + @property + def custom_keys(self): + keys = [] + for cmd in self.records.customized(): + match = self.custom_re.search(cmd) + if match: + if match.group('end'): + keys.append((int(match.group('start')), + int(match.group('end')), + match.group('proto'))) + else: + keys.append((int(match.group('start')), + int(match.group('start')), + match.group('proto'))) + return keys + + @property + def all_records(self): + if self._all is None: + # older versions of selinux use (startport, endport) as + # they key for the ports.get_all() dict, and (type, proto, + # level) as the value; this is obviously broken, so newer + # versions use (startport, endport, proto) as the key, and + # (type, level) as the value. abstracting around this + # sucks. + ports = self.records.get_all() + if len(ports.keys()[0]) == 3: + self._all = ports + else: + # uglist list comprehension ever? + self._all = dict([((k[0], k[1], v[1]), (v[0], v[2])) + for k, v in ports.items()]) + return self._all + + def _key(self, entry): + try: + (port, proto) = entry.get("name").split("/") + except ValueError: + self.logger.error("Invalid SELinux node %s: no protocol specified" % + entry.get("name")) + return + if "-" in port: + start, end = port.split("-") + else: + start = port + end = port + return (int(start), int(end), proto) + + def _key2attrs(self, key): + if key[0] == key[1]: + port = str(key[0]) + else: + port = "%s-%s" % (key[0], key[1]) + vals = self.all_records[key] + return dict(name="%s/%s" % (port, key[2]), selinuxtype=vals[0]) + + def _defaultargs(self, entry): + (port, proto) = entry.get("name").split("/") + return (port, proto, '', entry.get("selinuxtype")) + + def _deleteargs(self, entry): + return tuple(entry.get("name").split("/")) + + +class SELinuxFcontextHandler(SELinuxEntryHandler): + etype = "fcontext" + key_format = ("name", "filetype") + value_format = (None, None, "selinuxtype", None) + filetypeargs = dict(all="", + regular="--", + directory="-d", + symlink="-l", + pipe="-p", + socket="-s", + block="-b", + char="-c", + door="-D") + filetypenames = dict(all="all files", + regular="regular file", + directory="directory", + symlink="symbolic link", + pipe="named pipe", + socket="socket", + block="block device", + char="character device", + door="door") + filetypeattrs = dict([v, k] for k, v in filetypenames.iteritems()) + custom_re = re.compile(r'-f \'(?P<filetype>[a-z ]+)\'.*? \'(?P<name>.*)\'') + + @property + def all_records(self): + if self._all is None: + # on older selinux, fcontextRecords.get_all() returns a + # list of tuples of (filespec, filetype, seuser, serole, + # setype, level); on newer selinux, get_all() returns a + # dict of (filespec, filetype) => (seuser, serole, setype, + # level). + fcontexts = self.records.get_all() + if isinstance(fcontexts, dict): + self._all = fcontexts + else: + self._all = dict([(f[0:2], f[2:]) for f in fcontexts]) + return self._all + + def _key(self, entry): + ftype = entry.get("filetype", "all") + return (entry.get("name"), + self.filetypenames.get(ftype, ftype)) + + def _key2attrs(self, key): + rv = dict(name=key[0], filetype=self.filetypeattrs[key[1]]) + vals = self.all_records[key] + # in older versions of selinux, an fcontext with no selinux + # type is the single value None; in newer versions, it's a + # tuple whose 0th (and only) value is None. + if vals and vals[0]: + rv["selinuxtype"] = vals[2] + else: + rv["selinuxtype"] = "<<none>>" + return rv + + def canInstall(self, entry): + return (entry.get("filetype", "all") in self.filetypeargs and + SELinuxEntryHandler.canInstall(self, entry)) + + def _defaultargs(self, entry): + return (entry.get("name"), entry.get("selinuxtype"), + self.filetypeargs[entry.get("filetype", "all")], + '', '') + + def primarykey(self, entry): + return ":".join([entry.tag, entry.get("type"), entry.get("name"), + entry.get("filetype", "all")]) + + +class SELinuxNodeHandler(SELinuxEntryHandler): + etype = "node" + value_format = (None, None, "selinuxtype", None) + str_format = '%(name)s (%(proto)s)' + custom_re = re.compile(r'-M (?P<netmask>\S+).*?-p (?P<proto>ipv\d).*? (?P<addr>\S+)$') + custom_format = ('addr', 'netmask', 'proto') + + def _key(self, entry): + try: + (addr, netmask) = entry.get("name").split("/") + except ValueError: + self.logger.error("Invalid SELinux node %s: no netmask specified" % + entry.get("name")) + return + netmask = netmask_itoa(netmask, proto=entry.get("proto")) + return (addr, netmask, entry.get("proto")) + + def _key2attrs(self, key): + vals = self.all_records[key] + return dict(name="%s/%s" % (key[0], key[1]), proto=key[2], + selinuxtype=vals[2]) + + def _defaultargs(self, entry): + (addr, netmask) = entry.get("name").split("/") + return (addr, netmask, entry.get("proto"), "", entry.get("selinuxtype")) + + +class SELinuxLoginHandler(SELinuxEntryHandler): + etype = "login" + value_format = ("selinuxuser", None) + + def _defaultargs(self, entry): + return (entry.get("name"), entry.get("selinuxuser"), "") + + +class SELinuxUserHandler(SELinuxEntryHandler): + etype = "user" + value_format = ("prefix", None, None, "roles") + + def __init__(self, tool, logger, setup, config): + SELinuxEntryHandler.__init__(self, tool, logger, setup, config) + self.needs_prefix = False + + @property + def records(self): + if self._records is None: + self._records = seobject.seluserRecords() + return self._records + + def Install(self, entry): + # in older versions of selinux, modify() is broken if you + # provide a prefix _at all_, so we try to avoid giving the + # prefix. however, in newer versions, prefix is _required_, + # so we a) try without a prefix; b) catch TypeError, which + # indicates that we had the wrong number of args (ValueError + # is thrown by the bug in older versions of selinux); and c) + # try with prefix. + try: + SELinuxEntryHandler.Install(self, entry) + except TypeError: + self.needs_prefix = True + SELinuxEntryHandler.Install(self, entry) + + def _defaultargs(self, entry): + # in older versions of selinux, modify() is broken if you + # provide a prefix _at all_, so we try to avoid giving the + # prefix. see the comment in Install() above for more + # details. + rv = [entry.get("name"), + entry.get("roles", "").replace(" ", ",").split(",")] + if self.needs_prefix: + rv.extend(['', '', entry.get("prefix")]) + else: + key = self._key(entry) + if key in self.all_records: + attrs = self._key2attrs(key) + if attrs['prefix'] != entry.get("prefix"): + rv.extend(['', '', entry.get("prefix")]) + return tuple(rv) + + +class SELinuxInterfaceHandler(SELinuxEntryHandler): + etype = "interface" + value_format = (None, None, "selinuxtype", None) + + def _defaultargs(self, entry): + return (entry.get("name"), '', entry.get("selinuxtype")) + + +class SELinuxPermissiveHandler(SELinuxEntryHandler): + etype = "permissive" + + @property + def records(self): + try: + return SELinuxEntryHandler.records.fget(self) + except AttributeError: + self.logger.info("Permissive domains not supported by this version " + "of SELinux") + self._records = False + return self._records + + @property + def all_records(self): + if self._all is None: + if self.records == False: + self._all = dict() + else: + # permissiveRecords.get_all() returns a list, so we just + # make it into a dict so that the rest of + # SELinuxEntryHandler works + self._all = dict([(d, d) for d in self.records.get_all()]) + return self._all + + def _defaultargs(self, entry): + return (entry.get("name"),) + + +class SELinuxModuleHandler(SELinuxEntryHandler): + etype = "module" + value_format = (None, "disabled") + + def __init__(self, tool, logger, setup, config): + SELinuxEntryHandler.__init__(self, tool, logger, setup, config) + self.posixtool = Bcfg2.Client.Tools.POSIX.POSIX(logger, setup, config) + try: + self.setype = selinux.selinux_getpolicytype()[1] + except IndexError: + self.logger.error("Unable to determine SELinux policy type") + self.setype = None + + @property + def all_records(self): + if self._all is None: + # we get a list of tuples back; coerce it into a dict + self._all = dict([(m[0], (m[1], m[2])) + for m in self.records.get_all()]) + return self._all + + def _key2attrs(self, key): + rv = SELinuxEntryHandler._key2attrs(self, key) + status = self.all_records[key][1] + if status: + rv['disabled'] = "false" + else: + rv['disabled'] = "true" + return rv + + def _filepath(self, entry): + return os.path.join("/usr/share/selinux", self.setype, + "%s.pp" % entry.get("name")) + + def _pathentry(self, entry): + pathentry = copy.deepcopy(entry) + pathentry.set("name", self._filepath(pathentry)) + pathentry.set("perms", "0644") + pathentry.set("owner", "root") + pathentry.set("group", "root") + pathentry.set("secontext", "__default__") + return pathentry + + def Verify(self, entry): + if not entry.get("disabled"): + entry.set("disabled", "false") + return (SELinuxEntryHandler.Verify(self, entry) and + self.posixtool.Verifyfile(self._pathentry(entry), None)) + + def canInstall(self, entry): + return (entry.text and self.setype and + SELinuxEntryHandler.canInstall(self, entry)) + + def Install(self, entry): + rv = self.posixtool.Installfile(self._pathentry(entry)) + try: + rv = rv and SELinuxEntryHandler.Install(self, entry) + except NameError: + # some versions of selinux have a bug in seobject that + # makes modify() calls fail. add() seems to have the same + # effect as modify, but without the bug + if self.exists(entry): + rv = rv and SELinuxEntryHandler.Install(self, entry, + method="add") + + if entry.get("disabled", "false").lower() == "true": + method = "disable" + else: + method = "enable" + return rv and SELinuxEntryHandler.Install(self, entry, method=method) + + def _addargs(self, entry): + return (self._filepath(entry),) + + def _defaultargs(self, entry): + return (entry.get("name"),) + + def FindExtra(self): + specified = [self._key(e) + for e in self.tool.getSupportedEntries() + if e.get("type") == self.etype] + return [self.key2entry(os.path.basename(f)[:-3]) + for f in glob.glob(os.path.join("/usr/share/selinux", + self.setype, "*.pp")) + if f not in specified] diff --git a/src/lib/Bcfg2/Client/Tools/SMF.py b/src/lib/Bcfg2/Client/Tools/SMF.py index f824410ad..3e0a9da13 100644 --- a/src/lib/Bcfg2/Client/Tools/SMF.py +++ b/src/lib/Bcfg2/Client/Tools/SMF.py @@ -73,11 +73,6 @@ class SMF(Bcfg2.Client.Tools.SvcTool): def InstallService(self, entry): """Install SMF Service entry.""" - # don't take any actions for mode='manual' - if entry.get('mode', 'default') == 'manual': - self.logger.info("Service %s mode set to manual. Skipping " - "installation." % (entry.get('name'))) - return False self.logger.info("Installing Service %s" % (entry.get('name'))) if entry.get('status') == 'off': if entry.get("FMRI").startswith('lrc'): diff --git a/src/lib/Bcfg2/Client/Tools/Systemd.py b/src/lib/Bcfg2/Client/Tools/Systemd.py index e3f6a4169..a295bc608 100644 --- a/src/lib/Bcfg2/Client/Tools/Systemd.py +++ b/src/lib/Bcfg2/Client/Tools/Systemd.py @@ -42,18 +42,11 @@ class Systemd(Bcfg2.Client.Tools.SvcTool): def InstallService(self, entry): """Install Service entry.""" - # don't take any actions for mode = 'manual' - if entry.get('mode', 'default') == 'manual': - self.logger.info("Service %s mode set to manual. Skipping " - "installation." % (entry.get('name'))) - return True - if entry.get('status') == 'on': - pstatus = self.cmd.run(self.get_svc_command(entry, 'enable'))[0] - pstatus = self.cmd.run(self.get_svc_command(entry, 'start'))[0] - + rv = self.cmd.run(self.get_svc_command(entry, 'enable'))[0] == 0 + rv &= self.cmd.run(self.get_svc_command(entry, 'start'))[0] == 0 else: - pstatus = self.cmd.run(self.get_svc_command(entry, 'stop'))[0] - pstatus = self.cmd.run(self.get_svc_command(entry, 'disable'))[0] + rv = self.cmd.run(self.get_svc_command(entry, 'stop'))[0] == 0 + rv &= self.cmd.run(self.get_svc_command(entry, 'disable'))[0] == 0 - return not pstatus + return rv diff --git a/src/lib/Bcfg2/Client/Tools/Upstart.py b/src/lib/Bcfg2/Client/Tools/Upstart.py index 7afc8edd7..aa5a921a6 100644 --- a/src/lib/Bcfg2/Client/Tools/Upstart.py +++ b/src/lib/Bcfg2/Client/Tools/Upstart.py @@ -69,11 +69,6 @@ class Upstart(Bcfg2.Client.Tools.SvcTool): def InstallService(self, entry): """Install Service for entry.""" - # don't take any actions for mode='manual' - if entry.get('mode', 'default') == 'manual': - self.logger.info("Service %s mode set to manual. Skipping " - "installation." % (entry.get('name'))) - return False if entry.get('status') == 'on': pstatus = self.cmd.run(self.get_svc_command(entry, 'start'))[0] elif entry.get('status') == 'off': diff --git a/src/lib/Bcfg2/Client/Tools/YUM24.py b/src/lib/Bcfg2/Client/Tools/YUM24.py index 4e488b9da..2bc821db3 100644 --- a/src/lib/Bcfg2/Client/Tools/YUM24.py +++ b/src/lib/Bcfg2/Client/Tools/YUM24.py @@ -6,20 +6,6 @@ import sys import yum import Bcfg2.Client.XML import Bcfg2.Client.Tools.RPMng -# Compatibility import -from Bcfg2.Bcfg2Py3k import ConfigParser - -YAD = True -CP = ConfigParser.ConfigParser() -try: - if '-C' in sys.argv: - CP.read([sys.argv[sys.argv.index('-C') + 1]]) - else: - CP.read(['/etc/bcfg2.conf']) - if CP.get('YUMng', 'autodep').lower() == 'false': - YAD = False -except: - pass if not hasattr(Bcfg2.Client.Tools.RPMng, 'RPMng'): raise ImportError @@ -79,6 +65,7 @@ class YUM24(Bcfg2.Client.Tools.RPMng.RPMng): (entry.get('name').startswith('/etc/yum.d') \ or entry.get('name').startswith('/etc/yum.repos.d')) \ or entry.get('name') == '/etc/yum.conf'] + self.autodep = setup.get("yum24_autodep") self.yum_avail = dict() self.yum_installed = dict() self.yb = yum.YumBase() @@ -273,7 +260,7 @@ class YUM24(Bcfg2.Client.Tools.RPMng.RPMng): if len(install_pkgs) > 0: self.logger.info("Attempting to install packages") - if YAD: + if self.autodep: pkgtool = "/usr/bin/yum -d0 -y install %s" else: pkgtool = "/usr/bin/yum -d0 install %s" @@ -309,7 +296,7 @@ class YUM24(Bcfg2.Client.Tools.RPMng.RPMng): if len(upgrade_pkgs) > 0: self.logger.info("Attempting to upgrade packages") - if YAD: + if self.autodep: pkgtool = "/usr/bin/yum -d0 -y update %s" else: pkgtool = "/usr/bin/yum -d0 update %s" @@ -359,7 +346,7 @@ class YUM24(Bcfg2.Client.Tools.RPMng.RPMng): """ self.logger.debug('Running YUMng.RemovePackages()') - if YAD: + if self.autodep: pkgtool = "/usr/bin/yum -d0 -y erase %s" else: pkgtool = "/usr/bin/yum -d0 erase %s" diff --git a/src/lib/Bcfg2/Client/Tools/YUMng.py b/src/lib/Bcfg2/Client/Tools/YUMng.py index 244b66cf4..34029b9fe 100644 --- a/src/lib/Bcfg2/Client/Tools/YUMng.py +++ b/src/lib/Bcfg2/Client/Tools/YUMng.py @@ -12,9 +12,6 @@ import yum.misc import rpmUtils.arch import Bcfg2.Client.XML import Bcfg2.Client.Tools -# Compatibility import -from Bcfg2.Bcfg2Py3k import ConfigParser - def build_yname(pkgname, inst): """Build yum appropriate package name.""" @@ -58,20 +55,6 @@ def nevraString(p): return ret -class Parser(ConfigParser.ConfigParser): - - def get(self, section, option, default): - """ - Override ConfigParser.get: If the request option is not in the - config file then return the value of default rather than raise - an exception. We still raise exceptions on missing sections. - """ - try: - return ConfigParser.ConfigParser.get(self, section, option) - except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): - return default - - class RPMDisplay(yum.rpmtrans.RPMBaseCallback): """We subclass the default RPM transaction callback so that we can control Yum's verbosity and pipe it through the right logger.""" @@ -224,38 +207,24 @@ class YUMng(Bcfg2.Client.Tools.PkgTool): def _loadConfig(self): # Process the YUMng section from the config file. - CP = Parser() - CP.read(self.setup.get('setup')) - truth = ['true', 'yes', '1'] - # These are all boolean flags, either we do stuff or we don't - self.pkg_checks = CP.get(self.name, "pkg_checks", "true").lower() \ - in truth - self.pkg_verify = CP.get(self.name, "pkg_verify", "true").lower() \ - in truth - self.doInstall = CP.get(self.name, "installed_action", - "install").lower() == "install" - self.doUpgrade = CP.get(self.name, - "version_fail_action", "upgrade").lower() == "upgrade" - self.doReinst = CP.get(self.name, "verify_fail_action", - "reinstall").lower() == "reinstall" - self.verifyFlags = CP.get(self.name, "verify_flags", - "").lower().replace(' ', ',') + self.pkg_checks = self.setup["yumng_pkg_checks"] + self.pkg_verify = self.setup["yumng_pkg_verify"] + self.doInstall = self.setup["yumng_installed_action"] == "install" + self.doUpgrade = self.setup["yumng_version_fail_action"] == "upgrade" + self.doReinst = self.setup["yumng_verify_fail_action"] == "reinstall" + self.verifyFlags = self.setup["yumng_verify_flags"] self.installOnlyPkgs = self.yb.conf.installonlypkgs if 'gpg-pubkey' not in self.installOnlyPkgs: self.installOnlyPkgs.append('gpg-pubkey') - self.logger.debug("YUMng: Install missing: %s" \ - % self.doInstall) + self.logger.debug("YUMng: Install missing: %s" % self.doInstall) self.logger.debug("YUMng: pkg_checks: %s" % self.pkg_checks) self.logger.debug("YUMng: pkg_verify: %s" % self.pkg_verify) - self.logger.debug("YUMng: Upgrade on version fail: %s" \ - % self.doUpgrade) - self.logger.debug("YUMng: Reinstall on verify fail: %s" \ - % self.doReinst) - self.logger.debug("YUMng: installOnlyPkgs: %s" \ - % str(self.installOnlyPkgs)) + self.logger.debug("YUMng: Upgrade on version fail: %s" % self.doUpgrade) + self.logger.debug("YUMng: Reinstall on verify fail: %s" % self.doReinst) + self.logger.debug("YUMng: installOnlyPkgs: %s" % self.installOnlyPkgs) self.logger.debug("YUMng: verify_flags: %s" % self.verifyFlags) def _fixAutoVersion(self, entry): @@ -425,8 +394,8 @@ class YUMng(Bcfg2.Client.Tools.PkgTool): if entry.get('version', False) == 'auto': self._fixAutoVersion(entry) - self.logger.debug("Verifying package instances for %s" \ - % entry.get('name')) + self.logger.debug("Verifying package instances for %s" % + entry.get('name')) self.verifyCache = {} # Used for checking multilib packages self.modlists[entry] = modlist @@ -450,8 +419,8 @@ class YUMng(Bcfg2.Client.Tools.PkgTool): POs = self.yb.rpmdb.searchProvides(entry.get('name')) if len(POs) > 0: virtPkg = True - self.logger.info("%s appears to be provided by:" \ - % entry.get('name')) + self.logger.info("%s appears to be provided by:" % + entry.get('name')) for p in POs: self.logger.info(" %s" % p) @@ -473,8 +442,13 @@ class YUMng(Bcfg2.Client.Tools.PkgTool): stat['verify_fail'] = False stat['pkg'] = entry stat['modlist'] = modlist - verify_flags = inst.get('verify_flags', self.verifyFlags) - verify_flags = verify_flags.lower().replace(' ', ',').split(',') + if inst.get('verify_flags'): + # this splits on either space or comma + verify_flags = \ + inst.get('verify_flags').lower().replace(' ', + ',').split(',') + else: + verify_flags = self.verifyFlags if 'arch' in nevra: # If arch is specified use it to select the package @@ -483,6 +457,7 @@ class YUMng(Bcfg2.Client.Tools.PkgTool): _POs = POs if len(_POs) == 0: # Package (name, arch) not installed + entry.set('current_exists', 'false') self.logger.debug(" %s is not installed" % nevraString(nevra)) stat['installed'] = False package_fail = True @@ -494,8 +469,23 @@ class YUMng(Bcfg2.Client.Tools.PkgTool): # Check EVR if virtPkg: - self.logger.debug(" Not checking version for virtual package") - _POs = [po for po in POs] # Make a copy + # we need to make sure that the version of the symbol + # provided matches the one required in the + # configuration + vlist = [] + for attr in ["epoch", "version", "release"]: + vlist.append(nevra.get(attr)) + if tuple(vlist) == (None, None, None): + # we just require the package name, no particular + # version, so just make a copy of POs since every + # package that provides this symbol satisfies the + # requirement + _POs = [po for po in POs] + else: + _POs = [po for po in POs + if po.checkPrco('provides', + (nevra["name"], 'EQ', + tuple(vlist)))] elif entry.get('name') == 'gpg-pubkey': if 'version' not in nevra: m = "Skipping verify: gpg-pubkey without an RPM version." @@ -513,10 +503,33 @@ class YUMng(Bcfg2.Client.Tools.PkgTool): package_fail = True stat['version_fail'] = True # Just chose the first pkg for the error message - self.logger.info(" %s: Wrong version installed. " - "Want %s, but have %s" % (entry.get("name"), - nevraString(nevra), - nevraString(POs[0]))) + if virtPkg: + provTuple = \ + [p for p in POs[0].provides + if p[0] == entry.get("name")][0] + entry.set('current_version', "%s:%s-%s" % provTuple[2]) + self.logger.info(" %s: Wrong version installed. " + "Want %s, but %s provides %s" % + (entry.get("name"), + nevraString(nevra), + nevraString(POs[0]), + yum.misc.prco_tuple_to_string(provTuple))) + else: + entry.set('current_version', "%s:%s-%s.%s" % + (POs[0].epoch, + POs[0].version, + POs[0].release, + POs[0].arch)) + self.logger.info(" %s: Wrong version installed. " + "Want %s, but have %s" % + (entry.get("name"), + nevraString(nevra), + nevraString(POs[0]))) + entry.set('version', "%s:%s-%s.%s" % + (nevra.get('epoch', 'any'), + nevra.get('version', 'any'), + nevra.get('release', 'any'), + nevra.get('arch', 'any'))) qtext_versions.append("U(%s)" % str(POs[0])) continue @@ -547,7 +560,7 @@ class YUMng(Bcfg2.Client.Tools.PkgTool): package_fail = True continue - # Now take out the Yum specific objects / modlists / unproblmes + # Now take out the Yum specific objects / modlists / unproblems ignores = [ig.get('name') for ig in entry.findall('Ignore')] + \ [ig.get('name') for ig in inst.findall('Ignore')] + \ self.ignores diff --git a/src/lib/Bcfg2/Client/Tools/__init__.py b/src/lib/Bcfg2/Client/Tools/__init__.py index c6cb6e239..026c7ade0 100644 --- a/src/lib/Bcfg2/Client/Tools/__init__.py +++ b/src/lib/Bcfg2/Client/Tools/__init__.py @@ -1,16 +1,27 @@ """This contains all Bcfg2 Tool modules""" import os -import stat import sys -from subprocess import Popen, PIPE +import stat import time +import pkgutil +from subprocess import Popen, PIPE import Bcfg2.Client.XML - -__all__ = [tool.split('.')[0] \ - for tool in os.listdir(os.path.dirname(__file__)) \ - if tool.endswith(".py") and tool != "__init__.py"] - +from Bcfg2.Bcfg2Py3k import input + +if hasattr(pkgutil, 'walk_packages'): + submodules = pkgutil.walk_packages(path=__path__) +else: + # python 2.4 + import glob + submodules = [] + for path in __path__: + for submodule in glob.glob(os.path.join(path, "*.py")): + mod = os.path.splitext(os.path.basename(submodule))[0] + if mod not in ['__init__']: + submodules.append((None, mod, True)) + +__all__ = [m[1] for m in submodules] drivers = [item for item in __all__ if item not in ['rpmtools']] default = [item for item in drivers if item not in ['RPM', 'Yum']] @@ -36,7 +47,7 @@ class executor: return (p.returncode, output.splitlines()) -class Tool: +class Tool(object): """ All tools subclass this. It defines all interfaces that need to be defined. """ @@ -47,10 +58,6 @@ class Tool: __important__ = [] def __init__(self, logger, setup, config): - self.__important__ = [entry.get('name') \ - for struct in config for entry in struct \ - if entry.tag == 'Path' and \ - entry.get('important') in ['true', 'True']] self.setup = setup self.logger = logger if not hasattr(self, '__ireq__'): @@ -59,8 +66,15 @@ class Tool: self.cmd = executor(logger) self.modified = [] self.extra = [] - self.handled = [entry for struct in self.config for entry in struct \ - if self.handlesEntry(entry)] + self.__important__ = [] + self.handled = [] + for struct in config: + for entry in struct: + if (entry.tag == 'Path' and + entry.get('important', 'false').lower() == 'true'): + self.__important__.append(entry.get('name')) + if self.handlesEntry(entry): + self.handled.append(entry) for filename in self.__execs__: try: mode = stat.S_IMODE(os.stat(filename)[stat.ST_MODE]) @@ -130,12 +144,24 @@ class Tool: '''Build a list of potentially modified POSIX paths for this entry''' return [entry.get('name') for struct in self.config.getchildren() \ for entry in struct.getchildren() \ - if entry.tag in ['Ignore', 'Path']] + if entry.tag == 'Path'] def gatherCurrentData(self, entry): """Default implementation of the information gathering routines.""" pass + def missing_attrs(self, entry): + required = self.__req__[entry.tag] + if isinstance(required, dict): + required = ["type"] + try: + required.extend(self.__req__[entry.tag][entry.get("type")]) + except KeyError: + pass + + return [attr for attr in required + if attr not in entry.attrib or not entry.attrib[attr]] + def canVerify(self, entry): """Test if entry has enough information to be verified.""" if not self.handlesEntry(entry): @@ -148,13 +174,12 @@ class Tool: entry.get('failure'))) return False - missing = [attr for attr in self.__req__[entry.tag] \ - if attr not in entry.attrib] + missing = self.missing_attrs(entry) if missing: - self.logger.error("Incomplete information for entry %s:%s; cannot verify" \ - % (entry.tag, entry.get('name'))) - self.logger.error("\t... due to absence of %s attribute(s)" % \ - (":".join(missing))) + self.logger.error("Cannot verify entry %s:%s due to missing " + "required attribute(s): %s" % + (entry.tag, entry.get('name'), + ", ".join(missing))) try: self.gatherCurrentData(entry) except: @@ -167,6 +192,11 @@ class Tool: """Return a list of extra entries.""" return [] + def primarykey(self, entry): + """ return a string that should be unique amongst all entries + in the specification """ + return "%s:%s" % (entry.tag, entry.get("name")) + def canInstall(self, entry): """Test if entry has enough information to be installed.""" if not self.handlesEntry(entry): @@ -177,13 +207,12 @@ class Tool: (entry.tag, entry.get('name'))) return False - missing = [attr for attr in self.__ireq__[entry.tag] \ - if attr not in entry.attrib or not entry.attrib[attr]] + missing = self.missing_attrs(entry) if missing: - self.logger.error("Incomplete information for entry %s:%s; cannot install" \ - % (entry.tag, entry.get('name'))) - self.logger.error("\t... due to absence of %s attribute" % \ - (":".join(missing))) + self.logger.error("Incomplete information for entry %s:%s; cannot " + "install due to absence of attribute(s): %s" % + (entry.tag, entry.get('name'), + ", ".join(missing))) return False return True @@ -305,8 +334,7 @@ class SvcTool(Tool): return self.cmd.run(self.get_svc_command(service, restart_target))[0] def check_service(self, service): - # not supported for this driver - return 0 + return self.cmd.run(self.get_svc_command(service, 'status'))[0] == 0 def Remove(self, services): """ Dummy implementation of service removal method """ @@ -321,13 +349,12 @@ class SvcTool(Tool): return for entry in [ent for ent in bundle if self.handlesEntry(ent)]: - mode = entry.get('mode', 'default') - if (mode == 'manual' or - (mode == 'interactive_only' and + restart = entry.get("restart", "true") + if (restart.lower() == "false" or + (restart.lower == "interactive" and not self.setup['interactive'])): continue - # need to handle servicemode = (build|default) - # need to handle mode = (default|supervised) + rc = None if entry.get('status') == 'on': if self.setup['servicemode'] == 'build': @@ -336,11 +363,7 @@ class SvcTool(Tool): if self.setup['interactive']: prompt = ('Restart service %s?: (y/N): ' % entry.get('name')) - # py3k compatibility - try: - ans = raw_input(prompt) - except NameError: - ans = input(prompt) + ans = input(prompt) if ans not in ['y', 'Y']: continue rc = self.restart_service(entry) @@ -351,3 +374,19 @@ class SvcTool(Tool): if rc: self.logger.error("Failed to manipulate service %s" % (entry.get('name'))) + + def Install(self, entries, states): + """Install all entries in sublist.""" + for entry in entries: + if entry.get('install', 'true').lower() == 'false': + self.logger.info("Service %s installation is false. Skipping " + "installation." % (entry.get('name'))) + continue + try: + func = getattr(self, "Install%s" % (entry.tag)) + states[entry] = func(entry) + if states[entry]: + self.modified.append(entry) + except: + self.logger.error("Unexpected failure of install method for entry type %s" + % (entry.tag), exc_info=1) diff --git a/src/lib/Bcfg2/Client/Tools/launchd.py b/src/lib/Bcfg2/Client/Tools/launchd.py index c022d32ae..6f08559a2 100644 --- a/src/lib/Bcfg2/Client/Tools/launchd.py +++ b/src/lib/Bcfg2/Client/Tools/launchd.py @@ -88,11 +88,6 @@ class launchd(Bcfg2.Client.Tools.Tool): def InstallService(self, entry): """Enable or disable launchd item.""" - # don't take any actions for mode='manual' - if entry.get('mode', 'default') == 'manual': - self.logger.info("Service %s mode set to manual. Skipping " - "installation." % (entry.get('name'))) - return False name = entry.get('name') if entry.get('status') == 'on': self.logger.error("Installing service %s" % name) diff --git a/src/lib/Bcfg2/Client/Tools/rpmtools.py b/src/lib/Bcfg2/Client/Tools/rpmtools.py index 7441b2c06..32a04262d 100755 --- a/src/lib/Bcfg2/Client/Tools/rpmtools.py +++ b/src/lib/Bcfg2/Client/Tools/rpmtools.py @@ -43,7 +43,6 @@ try: isprelink_imported = True except ImportError: isprelink_imported = False - #print '*********************** isprelink not loaded ***********************' # If the prelink command is installed on the system then we need to do # prelink -y on files. @@ -333,7 +332,6 @@ def prelink_size_check(filename): fsize += len(data) elif whitelist_re.search(filename) and not blacklist_re.search(filename): - # print "***** Warning isprelink extension failed to import ******" plf.close() cmd = '/usr/sbin/prelink -y %s 2> /dev/null' \ % (re.escape(filename)) @@ -601,7 +599,6 @@ def rpm_verify_package(vp_ts, header, verify_options): omitmask |= VERIFY_RDEV omitmask = ((~omitmask & VERIFY_ATTRS) ^ VERIFY_ATTRS) - #print 'omitmask =', omitmask package_results = {} @@ -754,58 +751,41 @@ class Rpmtscallback(object): """ if reason == rpm.RPMCALLBACK_INST_OPEN_FILE: pass - #print 'rpm.RPMCALLBACK_INST_OPEN_FILE' elif reason == rpm.RPMCALLBACK_INST_CLOSE_FILE: pass - #print 'rpm.RPMCALLBACK_INST_CLOSE_FILE' elif reason == rpm.RPMCALLBACK_INST_START: pass - #print 'rpm.RPMCALLBACK_INST_START' elif reason == rpm.RPMCALLBACK_TRANS_PROGRESS or \ reason == rpm.RPMCALLBACK_INST_PROGRESS: pass - #print 'rpm.RPMCALLBACK_TRANS_PROGRESS or \ # rpm.RPMCALLBACK_INST_PROGRESS' elif reason == rpm.RPMCALLBACK_TRANS_START: pass - #print 'rpm.RPMCALLBACK_TRANS_START' elif reason == rpm.RPMCALLBACK_TRANS_STOP: pass - #print 'rpm.RPMCALLBACK_TRANS_STOP' elif reason == rpm.RPMCALLBACK_REPACKAGE_START: pass - #print 'rpm.RPMCALLBACK_REPACKAGE_START' elif reason == rpm.RPMCALLBACK_REPACKAGE_PROGRESS: pass - #print 'rpm.RPMCALLBACK_REPACKAGE_PROGRESS' elif reason == rpm.RPMCALLBACK_REPACKAGE_STOP: pass - #print 'rpm.RPMCALLBACK_REPACKAGE_STOP' elif reason == rpm.RPMCALLBACK_UNINST_PROGRESS: pass - #print 'rpm.RPMCALLBACK_UNINST_PROGRESS' elif reason == rpm.RPMCALLBACK_UNINST_START: pass - #print 'rpm.RPMCALLBACK_UNINST_START' elif reason == rpm.RPMCALLBACK_UNINST_STOP: pass - #print 'rpm.RPMCALLBACK_UNINST_STOP' - #print '***Package ', key, ' deleted ***' # How do we get at this? # RPM.modified += key elif reason == rpm.RPMCALLBACK_UNPACK_ERROR: pass - #print 'rpm.RPMCALLBACK_UNPACK_ERROR' elif reason == rpm.RPMCALLBACK_CPIO_ERROR: pass - #print 'rpm.RPMCALLBACK_CPIO_ERROR' elif reason == rpm.RPMCALLBACK_UNKNOWN: pass - #print 'rpm.RPMCALLBACK_UNKNOWN' else: print('ERROR - Fell through callBack') - #print reason, amount, total, key, client_data def rpm_erase(erase_pkgspecs, erase_flags): """ @@ -836,7 +816,6 @@ def rpm_erase(erase_pkgspecs, erase_flags): erase_ts.addErase(idx) #for te in erase_ts: - # print "%s %s:%s-%s.%s" % (te.N(), te.E(), te.V(), te.R(), te.A()) erase_problems = [] if 'nodeps' not in erase_flags: @@ -847,8 +826,6 @@ def rpm_erase(erase_pkgspecs, erase_flags): erase_callback = Rpmtscallback() erase_ts.run(erase_callback.callback, 'Erase') #else: - # print 'ERROR - Dependency failures on package erase' - # print erase_problems erase_ts.closeDB() del erase_ts |