[server] new tool for detecting package updates and upstream removals
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Executable
+413
@@ -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)
|
||||
Reference in New Issue
Block a user