From 8fc971c867cd8b3dca0a45da2f42849d3929f5ef Mon Sep 17 00:00:00 2001 From: Alexander Sulfrian Date: Wed, 23 Apr 2014 03:07:14 +0200 Subject: Client/Tools/Pkgng: add client tool for pkgng --- src/lib/Bcfg2/Client/Tools/Pkgng.py | 222 ++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 src/lib/Bcfg2/Client/Tools/Pkgng.py diff --git a/src/lib/Bcfg2/Client/Tools/Pkgng.py b/src/lib/Bcfg2/Client/Tools/Pkgng.py new file mode 100644 index 000000000..81ed3e5c1 --- /dev/null +++ b/src/lib/Bcfg2/Client/Tools/Pkgng.py @@ -0,0 +1,222 @@ +"""This is the Bcfg2 support for pkg.""" + +import os +import Bcfg2.Options +import Bcfg2.Client.Tools + + +class Pkgng(Bcfg2.Client.Tools.Tool): + """Support for pkgng packages on FreeBSD.""" + + options = Bcfg2.Client.Tools.Tool.options + [ + Bcfg2.Options.PathOption( + cf=('Pkgng', 'path'), + default='/usr/sbin/pkg', dest='pkg_path', + help='Pkgng tool path')] + + name = 'Pkgng' + __execs__ = [] + __handles__ = [('Package', 'pkgng'), ('Path', 'ignore')] + __req__ = {'Package': ['name', 'version'], 'Path': ['type']} + + def __init__(self, config): + Bcfg2.Client.Tools.Tool.__init__(self, config) + + self.pkg = Bcfg2.Options.setup.pkg_path + self.__execs__ = [self.pkg] + + self.pkgcmd = self.pkg + ' install -fy' + if not Bcfg2.Options.setup.debug: + self.pkgcmd += ' -q' + self.pkgcmd += ' %s' + + self.ignores = [entry.get('name') for struct in config + for entry in struct + if entry.tag == 'Path' and + entry.get('type') == 'ignore'] + + self.__important__ = self.__important__ + \ + [entry.get('name') for struct in config + for entry in struct + if (entry.tag == 'Path' and + entry.get('name').startswith('/etc/pkg/'))] + self.nonexistent = [entry.get('name') for struct in config + for entry in struct if entry.tag == 'Path' + and entry.get('type') == 'nonexistent'] + self.actions = {} + self.pkg_cache = {} + self._load_pkg_cache() + + def _load_pkg_cache(self): + """Cache the version of all currently installed packages.""" + self.pkg_cache = {} + output = self.cmd.run([self.pkg, 'query', '-a', '%n %v']).stdout + for line in output.splitlines(): + parts = line.split(' ') + name = ' '.join(parts[:-1]) + self.pkg_cache[name] = parts[-1] + + def FindExtra(self): + """Find extra packages.""" + packages = [entry.get('name') for entry in self.getSupportedEntries()] + extras = [(name, value) for (name, value) in self.pkg_cache.items() + if name not in packages] + return [Bcfg2.Client.XML.Element('Package', name=name, + type='pkgng', version=version) + for (name, version) in extras] + + def VerifyChecksums(self, entry, modlist): + """Verify the checksum of the files, owned by a package.""" + output = self.cmd.run([self.pkg, 'check', '-s', + entry.get('name')]).stdout.splitlines() + files = [] + for item in output: + if "checksum mismatch" in item: + files.append(item.split()[-1]) + elif "No such file or directory" in item: + continue + else: + self.logger.error("Got Unsupported pattern %s " + "from pkg check" % item) + + files = list(set(files) - set(self.ignores)) + # We check if there is file in the checksum to do + if files: + # if files are found there we try to be sure our modlist is sane + # with erroneous symlinks + modlist = [os.path.realpath(filename) for filename in modlist] + bad = [filename for filename in files if filename not in modlist] + if bad: + self.logger.debug("It is suggested that you either manage " + "these files, revert the changes, or ignore " + "false failures:") + self.logger.info("Package %s failed validation. Bad files " + "are:" % entry.get('name')) + self.logger.info(bad) + entry.set('qtext', + "Reinstall Package %s-%s to fix failing files? " + "(y/N) " % (entry.get('name'), entry.get('version'))) + return False + return True + + def _get_candidate_versions(self, name): + """ + Get versions of the specified package name available for + installation from the configured remote repositories. + """ + output = self.cmd.run([self.pkg, 'search', '-Qversion', '-q', + '-Sname', '-e', name]).stdout.splitlines() + versions = [] + for line in output: + versions.append(line) + + if len(versions) == 0: + return None + + return sorted(versions) + + def VerifyPackage(self, entry, modlist, checksums=True): + """Verify package for entry.""" + if 'version' not in entry.attrib: + self.logger.info("Cannot verify unversioned package %s" % + (entry.attrib['name'])) + return False + + pkgname = entry.get('name') + if pkgname not in self.pkg_cache: + self.logger.info("Package %s not installed" % (entry.get('name'))) + entry.set('current_exists', 'false') + return False + + installed_version = self.pkg_cache[pkgname] + candidate_versions = self._get_candidate_versions(pkgname) + if candidate_versions is not None: + candidate_version = candidate_versions[0] + else: + self.logger.error("Package %s is installed but no candidate" + + "version was found." % (entry.get('name'))) + return False + + if entry.get('version').startswith('auto'): + desired_version = candidate_version + entry.set('version', "auto: %s" % desired_version) + elif entry.get('version').startswith('any'): + desired_version = installed_version + entry.set('version', "any: %s" % desired_version) + else: + desired_version = entry.get('version') + + if desired_version != installed_version: + entry.set('current_version', installed_version) + entry.set('qtext', "Modify Package %s (%s -> %s)? (y/N) " % + (entry.get('name'), entry.get('current_version'), + desired_version)) + return False + else: + # version matches + if (not Bcfg2.Options.setup.quick and + entry.get('verify', 'true') == 'true' + and checksums): + pkgsums = self.VerifyChecksums(entry, modlist) + return pkgsums + return True + + def Remove(self, packages): + """Deal with extra configuration detected.""" + pkgnames = " ".join([pkg.get('name') for pkg in packages]) + if len(packages) > 0: + self.logger.info('Removing packages:') + self.logger.info(pkgnames) + self.cmd.run([self.pkg, 'delete', '-y', pkgnames]) + self._load_pkg_cache() + self.modified += packages + self.extra = self.FindExtra() + + def Install(self, packages): + ipkgs = [] + bad_pkgs = [] + for pkg in packages: + versions = self._get_candidate_versions(pkg.get('name')) + if versions is None: + self.logger.error("pkg has no information about package %s" % + (pkg.get('name'))) + continue + + if pkg.get('version').startswith('auto') or \ + pkg.get('version').startswith('any'): + ipkgs.append("%s-%s" % (pkg.get('name'), versions[0])) + continue + + if pkg.get('version') in versions: + ipkgs.append("%s-%s" % (pkg.get('name'), pkg.get('version'))) + continue + else: + self.logger.error("Package %s: desired version %s not in %s" % + (pkg.get('name'), pkg.get('version'), + versions)) + bad_pkgs.append(pkg.get('name')) + + if bad_pkgs: + self.logger.error("Cannot find correct versions of packages:") + self.logger.error(bad_pkgs) + if not ipkgs: + return + if not self.cmd.run(self.pkgcmd % (" ".join(ipkgs))): + self.logger.error("pkg command failed") + self._load_pkg_cache() + self.extra = self.FindExtra() + mark = [] + states = dict() + for package in packages: + states[package] = self.VerifyPackage(package, [], checksums=False) + if states[package]: + self.modified.append(package) + if package.get('origin') == 'Packages': + mark.append(package.get('name')) + if mark: + self.cmd.run([self.pkg, 'set', '-A1', '-y'] + mark) + return states + + def VerifyPath(self, _entry, _): + """Do nothing here since we only verify Path type=ignore.""" + return True -- cgit v1.2.3-1-g7c22