diff --git a/server/Makefile b/server/Makefile index aceccef57..10eb74ebc 100644 --- a/server/Makefile +++ b/server/Makefile @@ -19,6 +19,7 @@ install: cp eit $(DESTDIR)$(LIBDIR)/entropy/server/ -Ra install -m 755 eit.py $(DESTDIR)$(BINDIR)/eit install -m 755 matter $(DESTDIR)$(PREFIX)/sbin + install -m 755 antimatter $(DESTDIR)$(PREFIX)/sbin for d in $(SUBDIRS); do $(MAKE) -C $$d install; done diff --git a/server/antimatter b/server/antimatter new file mode 100755 index 000000000..513ba8c38 --- /dev/null +++ b/server/antimatter @@ -0,0 +1,413 @@ +#!/usr/bin/python + +import argparse +import functools +import sys +import os +import re +import time + +os.environ['ETP_GETTEXT_DOMAIN'] = "entropy-server" + +# Entropy imports +sys.path.insert(0, '/usr/lib/entropy/lib') +sys.path.insert(0, '/usr/lib/entropy/server') +sys.path.insert(0, '/usr/lib/entropy/client') +sys.path.insert(0, '../lib') +sys.path.insert(0, '../server') +sys.path.insert(0, '../client') + + +# Entropy imports +from entropy.output import print_info, print_error, print_warning, \ + print_generic + +# Portage imports +os.environ['ACCEPT_PROPERTIES'] = "* -interactive" +os.environ['FEATURES'] = "split-log" +os.environ['CMAKE_NO_COLOR'] = "yes" + +from _emerge.actions import load_emerge_config +import portage +import portage.dep +import portage.versions + + +class AntiMatterPackage(object): + """ + This object describes a potentially available update and + exposes an API for easy retrieval of the update information. + """ + + def __init__(self, vardb, portdb, installed, available, status): + """ + Constructor. + + @param vardb: Portage vardb object. + @type vardb: opaque + @param portdb: Portage portdb object. + @type portdb: opaque + @param installed: Atom object of the installed package. + @type installed: portage.dep.Atom + @param available: Atom object of the available package. + @type available: portage.dep.Atom + @param status: 1 for upgrade, -1 for downgrade, 0 for rebuild + @type status: int + """ + self._vardb = vardb + self._portdb = portdb + self._installed = installed + self._available = available + self._status = status + + def slot(self): + """ + Return the package slot. + """ + return self._installed.slot + + def keyslot(self): + """ + Return the package key:slot string. + """ + target = self.target() + return "%s:%s" % (target.cp, target.slot) + + def installed(self): + """ + Return the installed package Atom object. + """ + return self._installed + + def available(self): + """ + Return the available package Atom object or None. + """ + return self._available + + def target(self): + """ + Automatically return the right object between available() + and installed(). In particular, if available() is None, + installed() is returned. + """ + if self._available: + return self._available + return self._installed + + def dropped_upstream(self): + """ + Return whether the package has been dropped upstream + and no more updated versions are available for update. + """ + return self._available is None + + def upgrade(self): + """ + Return whether the package would be upgraded. + """ + return self._status == 1 + + def downgrade(self): + """ + Return whether the package would be downgraded. + """ + return self._status == -1 + + +class BaseAntiMatterResult(object): + """ + Base class for implementing AntiMatter result + objects. Subclasses must implement the notify() + method. + """ + + def __init__(self, result, quiet, verbose): + """ + Constructor. + + @param result: list of AntiMatterPackage objects + @type result: list + @param quiet: quiet output enabled (print one item per line) + @type quiet: bool + @param verbose: verbose output enabled (print the full version) + @type verbose: bool + """ + self._result = result + self._quiet = quiet + self._verbose = verbose + + def notify(self): + """ + Notify the result of an AntiMatter run using + the list of AntiMatterPackage objects passed + to the constructor. + """ + raise NotImplementedError() + + +class StdoutAntiMatterResult(BaseAntiMatterResult): + """ + BaseAntiMatterResult subclass that prints the results + to stdout. + """ + + def __init__(self, result, quiet, verbose): + """ + Constructor. + + @param result: list of AntiMatterPackage objects + @type result: list + @param quiet: quiet output enabled + @type quiet: bool + @param verbose: verbose output enabled + @type verbose: bool + """ + super(StdoutAntiMatterResult, self).__init__( + result, quiet, verbose) + + def notify(self): + """ + Overridden from BaseAntiMatterResult + """ + for package in self._result: + if self._verbose: + target = package.target() + else: + target = package.keyslot() + + if self._quiet: + print_generic(target) + else: + print_info(target) + + +class AntiMatter(object): + """ + AntiMatter is a package update scanner that uses + Portage and Entropy to determine + """ + + def __init__(self, nsargs): + """ + Constructor. + + @param nsargs: argparse's parsed arguments. + @param entropy_obj: an Entropy instance or None + """ + self._nsargs = nsargs + + def _get_dbs(self): + """ + Return a tuple containing (vardb, portdb) + """ + emerge_config = load_emerge_config() + emerge_settings, emerge_trees, _mtimedb = emerge_config + settings = portage.config(clone=emerge_settings) + + portdb = emerge_trees[settings["ROOT"]]["porttree"].dbapi + if not portdb.frozen: + portdb.freeze() + vardb = emerge_trees[settings["ROOT"]]["vartree"].dbapi + + return vardb, portdb + + def _new_scan(self): + """ + Internal scan method, executes the actual scan and retuns + a raw list of AntiMatterPackage objects. + """ + vardb, portdb = self._get_dbs() + new_days_old_secs = self._nsargs.new_days_old * 3600 * 24 + result = [] + + cp_all = portdb.cp_all() + cp_all.sort() + root = portdb.porttree_root + for count, package in enumerate(cp_all): + + count_str = "[%s of %s]" % ( + count, len(cp_all),) + + if self._nsargs.verbose: + print_warning("%s :: %s" % (count_str, package), + back=True) + + cp_dir = os.path.join(root, package) + try: + mtime = os.path.getmtime(cp_dir) + except (OSError, IOError): + mtime = 0.0 + + if abs(time.time() - mtime) >= new_days_old_secs: + # not new enough + continue + + best_installed = portage.best(vardb.match(package)) + if best_installed: + # package key is already installed, ignore + continue + + best_visible = portage.best(portdb.match(package)) + if not best_visible: + # wtf? package masked? + continue + + try: + slot, repo = portdb.aux_get( + best_visible, ["SLOT", "repository"]) + except KeyError: + # portage is scrappy + continue + + atom = portage.dep.Atom( + "=%s:%s::%s" % (best_visible, slot, repo), + allow_wildcard=True, + allow_repo=True) + + pkg = AntiMatterPackage( + vardb, portdb, None, atom, 1) + result.append(pkg) + + if cp_all and self._nsargs.verbose: + print_generic("") + + return result + + def _scan(self): + """ + Internal scan method, executes the actual scan and retuns + a raw list of AntiMatterPackage objects. + """ + vardb, portdb = self._get_dbs() + result = [] + + cpv_all = vardb.cpv_all() + cpv_all.sort() + for count, package in enumerate(cpv_all): + + count_str = "[%s of %s]" % ( + count, len(cpv_all),) + + slot, repo = vardb.aux_get(package, ["SLOT", "repository"]) + + atom = portage.dep.Atom( + "=%s:%s::%s" % (package, slot, repo), + allow_wildcard=True, + allow_repo=True) + + if self._nsargs.verbose: + print_warning("%s :: %s" % (count_str, atom), + back=True) + + key_slot = "%s:%s" % (atom.cp, atom.slot) + + best_visible = portage.best(vardb.match(key_slot)) + + if not best_visible: + # dropped upstream + pkg = AntiMatterPackage( + vardb, portdb, atom, None, -1) + result.append(pkg) + if self._nsargs.verbose: + print_error( + " %s no longer upstream or masked" % (key_slot,)) + continue + + cmp_res = portage.versions.pkgcmp( + portage.versions.pkgsplit(best_visible), + portage.versions.pkgsplit(package)) + + pkg = AntiMatterPackage( + vardb, portdb, atom, + best_visible, cmp_res) + result.append(pkg) + + if cpv_all and self._nsargs.verbose: + print_generic("") + + return result + + def scan(self): + """ + Execute a scan of the system and return a BaseAntiMatterResult + object. + """ + if self._nsargs.new: + result = self._new_scan() + else: + result = self._scan() + # apply filtering basing on arguments + if self._nsargs.extinguished: + result = [x for x in result if x.dropped_upstream()] + elif self._nsargs.upgrade: + result = [x for x in result if x.upgrade()] + elif self._nsargs.downgrade: + result = [x for x in result if x.downgrade()] + + def _regex_filter(regex, x): + target = x.target() + operator = target.operator + target_str = target[len(operator):] + return regex.match(target_str) + + for regex in self._nsargs.filters: + result = list(filter( + functools.partial(_regex_filter, regex), + result)) + + return StdoutAntiMatterResult( + result, self._nsargs.quiet, self._nsargs.verbose) + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser( + description='Automated package updates scanner', + formatter_class=argparse.RawDescriptionHelpFormatter) + + parser.add_argument("--verbose", "-v", action="store_true", + default=False, help="verbose output") + parser.add_argument("--quiet", "-q", action="store_true", + default=False, help="quiet output") + + parser.add_argument("--filter", "-f", dest="filters", + action="append", default=[], + help="filter package atoms using the given " + "regular expressions. They will be applied in " + "the given order, against the full package atom " + "string (which includes version, slot, repo: " + " app-foo/bar-1.2.3:slot::repo).") + + mg_group = parser.add_mutually_exclusive_group(required=True) + mg_group.add_argument("--extinguished", "-e", action="store_true", + default=False, + help="list dead packages only (those which " + "upstream dropped)") + mg_group.add_argument("--upgrade", "-u", action="store_true", + default=True, + help="list packages that would be upgraded " + "(default action)") + mg_group.add_argument("--downgrade", "-d", action="store_true", + default=False, + help="list packages that would be downgraded") + mg_group.add_argument("--new", "-n", action="store_true", + default=False, + help="list packages that have been recently " + "added") + + nsargs = None + try: + nsargs = parser.parse_args(sys.argv[1:]) + nsargs.filters = [re.compile(x) for x in nsargs.filters] + # still hardcoded, but candidate for argparse argument + # used in the calculation of new packages. + nsargs.new_days_old = 14 + except re.error as err: + print_error("Error, invalid regexp: %s" % (err,)) + raise SystemExit(1) + + antimatter = AntiMatter(nsargs) + antimatter.scan().notify() + + raise SystemExit(0)