Blame plugins/versionlock.py

Packit 3a9065
#
Packit 3a9065
# Copyright (C) 2015  Red Hat, Inc.
Packit 3a9065
#
Packit 3a9065
# This copyrighted material is made available to anyone wishing to use,
Packit 3a9065
# modify, copy, or redistribute it subject to the terms and conditions of
Packit 3a9065
# the GNU General Public License v.2, or (at your option) any later version.
Packit 3a9065
# This program is distributed in the hope that it will be useful, but WITHOUT
Packit 3a9065
# ANY WARRANTY expressed or implied, including the implied warranties of
Packit 3a9065
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
Packit 3a9065
# Public License for more details.  You should have received a copy of the
Packit 3a9065
# GNU General Public License along with this program; if not, write to the
Packit 3a9065
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
Packit 3a9065
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
Packit 3a9065
# source code or documentation are not subject to the GNU General Public
Packit 3a9065
# License and may only be used or replicated with the express permission of
Packit 3a9065
# Red Hat, Inc.
Packit 3a9065
#
Packit 3a9065
Packit 3a9065
from __future__ import absolute_import
Packit 3a9065
from __future__ import unicode_literals
Packit 3a9065
from dnfpluginscore import _, logger
Packit 3a9065
Packit 3a9065
import dnf
Packit 3a9065
import dnf.cli
Packit 3a9065
import dnf.exceptions
Packit 3a9065
import fnmatch
Packit 3a9065
import hawkey
Packit 3a9065
import os
Packit 3a9065
import tempfile
Packit 3a9065
import time
Packit 3a9065
Packit 3a9065
NOT_READABLE = _('Unable to read version lock configuration: %s')
Packit 3a9065
NO_LOCKLIST = _('Locklist not set')
Packit 3a9065
ADDING_SPEC = _('Adding versionlock on:')
Packit 3a9065
EXCLUDING_SPEC = _('Adding exclude on:')
Packit 3a9065
EXISTING_SPEC = _('Package already locked in equivalent form:')
Packit 3a9065
ALREADY_LOCKED = _('Package {} is already locked')
Packit 3a9065
ALREADY_EXCLUDED = _('Package {} is already excluded')
Packit 3a9065
DELETING_SPEC = _('Deleting versionlock for:')
Packit 3a9065
NOTFOUND_SPEC = _('No package found for:')
Packit 3a9065
NO_VERSIONLOCK = _('Excludes from versionlock plugin were not applied')
Packit 3a9065
APPLY_LOCK = _('Versionlock plugin: number of lock rules from file "{}" applied: {}')
Packit 3a9065
APPLY_EXCLUDE = _('Versionlock plugin: number of exclude rules from file "{}" applied: {}')
Packit 3a9065
NEVRA_ERROR = _('Versionlock plugin: could not parse pattern:')
Packit 3a9065
Packit 3a9065
locklist_fn = None
Packit 3a9065
Packit 3a9065
Packit 3a9065
class VersionLock(dnf.Plugin):
Packit 3a9065
Packit 3a9065
    name = 'versionlock'
Packit 3a9065
Packit 3a9065
    def __init__(self, base, cli):
Packit 3a9065
        super(VersionLock, self).__init__(base, cli)
Packit 3a9065
        self.base = base
Packit 3a9065
        self.cli = cli
Packit 3a9065
        if self.cli is not None:
Packit 3a9065
            self.cli.register_command(VersionLockCommand)
Packit 3a9065
Packit 3a9065
    def config(self):
Packit 3a9065
        global locklist_fn
Packit 3a9065
        cp = self.read_config(self.base.conf)
Packit 3a9065
        locklist_fn = (cp.has_section('main') and cp.has_option('main', 'locklist')
Packit 3a9065
                       and cp.get('main', 'locklist'))
Packit 3a9065
Packit 3a9065
    def locking_enabled(self):
Packit 3a9065
        if self.cli is None:
Packit 3a9065
            enabled = True  # loaded via the api, not called by cli
Packit 3a9065
        else:
Packit 3a9065
            enabled = self.cli.demands.plugin_filtering_enabled
Packit 3a9065
            if enabled is None:
Packit 3a9065
                enabled = self.cli.demands.resolving
Packit 3a9065
        return enabled
Packit 3a9065
Packit 3a9065
    def sack(self):
Packit 3a9065
        if not self.locking_enabled():
Packit 3a9065
            logger.debug(NO_VERSIONLOCK)
Packit 3a9065
            return
Packit 3a9065
Packit 3a9065
        excludes_query = self.base.sack.query().filter(empty=True)
Packit 3a9065
        locked_query = self.base.sack.query().filter(empty=True)
Packit 3a9065
        locked_names = set()
Packit 3a9065
        # counter of applied rules [locked_count, excluded_count]
Packit 3a9065
        count = [0, 0]
Packit 3a9065
        for pat in _read_locklist():
Packit 3a9065
            excl = 0
Packit 3a9065
            if pat and pat[0] == '!':
Packit 3a9065
                pat = pat[1:]
Packit 3a9065
                excl = 1
Packit 3a9065
Packit 3a9065
            possible_nevras = dnf.subject.Subject(pat).get_nevra_possibilities()
Packit 3a9065
            if possible_nevras:
Packit 3a9065
                count[excl] += 1
Packit 3a9065
            else:
Packit 3a9065
                logger.error("%s %s", NEVRA_ERROR, pat)
Packit 3a9065
                continue
Packit 3a9065
            for nevra in possible_nevras:
Packit 3a9065
                pat_query = nevra.to_query(self.base.sack)
Packit 3a9065
                if excl:
Packit 3a9065
                    excludes_query = excludes_query.union(pat_query)
Packit 3a9065
                else:
Packit 3a9065
                    locked_names.add(nevra.name)
Packit 3a9065
                    locked_query = locked_query.union(pat_query)
Packit 3a9065
Packit 3a9065
        if count[1]:
Packit 3a9065
            logger.debug(APPLY_EXCLUDE.format(locklist_fn, count[1]))
Packit 3a9065
        if count[0]:
Packit 3a9065
            logger.debug(APPLY_LOCK.format(locklist_fn, count[0]))
Packit 3a9065
Packit 3a9065
        if locked_names:
Packit 3a9065
            all_versions = self.base.sack.query().filter(name__glob=list(locked_names))
Packit 3a9065
            other_versions = all_versions.difference(locked_query)
Packit 3a9065
            excludes_query = excludes_query.union(other_versions)
Packit 3a9065
            # exclude also anything that obsoletes the locked versions of packages
Packit 3a9065
            excludes_query = excludes_query.union(
Packit 3a9065
                self.base.sack.query().filterm(obsoletes=locked_query))
Packit 3a9065
Packit 3a9065
        excludes_query.filterm(reponame__neq=hawkey.SYSTEM_REPO_NAME)
Packit 3a9065
        if excludes_query:
Packit 3a9065
            self.base.sack.add_excludes(excludes_query)
Packit 3a9065
Packit 3a9065
EXC_CMDS = ['exclude', 'add-!', 'add!', 'blacklist']
Packit 3a9065
DEL_CMDS = ['delete', 'del']
Packit 3a9065
ALL_CMDS = ['add', 'clear', 'list'] + EXC_CMDS + DEL_CMDS
Packit 3a9065
Packit 3a9065
Packit 3a9065
class VersionLockCommand(dnf.cli.Command):
Packit 3a9065
Packit 3a9065
    aliases = ("versionlock",)
Packit 3a9065
    summary = _("control package version locks")
Packit 3a9065
    usage = "[add|exclude|list|delete|clear] [<package-nevr-spec>]"
Packit 3a9065
Packit 3a9065
    @staticmethod
Packit 3a9065
    def set_argparser(parser):
Packit 3a9065
        parser.add_argument("--raw", default=False, action='store_true',
Packit 3a9065
                            help=_("Use package specifications as they are, do not "
Packit 3a9065
                                   "try to parse them"))
Packit 3a9065
        parser.add_argument("subcommand", nargs='?',
Packit 3a9065
                            metavar="[add|exclude|list|delete|clear]")
Packit 3a9065
        parser.add_argument("package", nargs='*',
Packit 3a9065
                            metavar="[<package-nevr-spec>]")
Packit 3a9065
Packit 3a9065
    def configure(self):
Packit 3a9065
        self.cli.demands.sack_activation = True
Packit 3a9065
        self.cli.demands.available_repos = True
Packit 3a9065
Packit 3a9065
    def run(self):
Packit 3a9065
        cmd = 'list'
Packit 3a9065
        if self.opts.subcommand:
Packit 3a9065
            if self.opts.subcommand not in ALL_CMDS:
Packit 3a9065
                cmd = 'add'
Packit 3a9065
                self.opts.package.insert(0, self.opts.subcommand)
Packit 3a9065
            elif self.opts.subcommand in EXC_CMDS:
Packit 3a9065
                cmd = 'exclude'
Packit 3a9065
            elif self.opts.subcommand in DEL_CMDS:
Packit 3a9065
                cmd = 'delete'
Packit 3a9065
            else:
Packit 3a9065
                cmd = self.opts.subcommand
Packit 3a9065
Packit 3a9065
        if cmd == 'add':
Packit 3a9065
            (entry, entry_cmd) = _search_locklist(self.opts.package)
Packit 3a9065
            if entry == '':
Packit 3a9065
                _write_locklist(self.base, self.opts.package, self.opts.raw, True,
Packit 3a9065
                                "\n# Added lock on %s\n" % time.ctime(),
Packit 3a9065
                                ADDING_SPEC, '')
Packit 3a9065
            elif cmd != entry_cmd:
Packit 3a9065
                raise dnf.exceptions.Error(ALREADY_EXCLUDED.format(entry))
Packit 3a9065
            else:
Packit 3a9065
                logger.info("%s %s", EXISTING_SPEC, entry)
Packit 3a9065
        elif cmd == 'exclude':
Packit 3a9065
            (entry, entry_cmd) = _search_locklist(self.opts.package)
Packit 3a9065
            if entry == '':
Packit 3a9065
                _write_locklist(self.base, self.opts.package, self.opts.raw, False,
Packit 3a9065
                                "\n# Added exclude on %s\n" % time.ctime(),
Packit 3a9065
                                EXCLUDING_SPEC, '!')
Packit 3a9065
            elif cmd != entry_cmd:
Packit 3a9065
                raise dnf.exceptions.Error(ALREADY_LOCKED.format(entry))
Packit 3a9065
            else:
Packit 3a9065
                logger.info("%s %s", EXISTING_SPEC, entry)
Packit 3a9065
        elif cmd == 'list':
Packit 3a9065
            for pat in _read_locklist():
Packit 3a9065
                print(pat)
Packit 3a9065
        elif cmd == 'clear':
Packit 3a9065
            if not locklist_fn:
Packit 3a9065
                raise dnf.exceptions.Error(NO_LOCKLIST)
Packit 3a9065
            with open(locklist_fn, 'w') as f:
Packit 3a9065
                # open in write mode truncates file
Packit 3a9065
                pass
Packit 3a9065
        elif cmd == 'delete':
Packit 3a9065
            if not locklist_fn:
Packit 3a9065
                raise dnf.exceptions.Error(NO_LOCKLIST)
Packit 3a9065
            dirname = os.path.dirname(locklist_fn)
Packit 3a9065
            (out, tmpfilename) = tempfile.mkstemp(dir=dirname, suffix='.tmp')
Packit 3a9065
            locked_specs = _read_locklist()
Packit 3a9065
            count = 0
Packit 3a9065
            with os.fdopen(out, 'w', -1) as out:
Packit 3a9065
                for ent in locked_specs:
Packit 3a9065
                    if _match(ent, self.opts.package):
Packit 3a9065
                        print("%s %s" % (DELETING_SPEC, ent))
Packit 3a9065
                        count += 1
Packit 3a9065
                        continue
Packit 3a9065
                    out.write(ent)
Packit 3a9065
                    out.write('\n')
Packit 3a9065
            if not count:
Packit 3a9065
                os.unlink(tmpfilename)
Packit 3a9065
            else:
Packit 3a9065
                os.chmod(tmpfilename, 0o644)
Packit 3a9065
                os.rename(tmpfilename, locklist_fn)
Packit 3a9065
Packit 3a9065
Packit 3a9065
def _read_locklist():
Packit 3a9065
    locklist = []
Packit 3a9065
    try:
Packit 3a9065
        if not locklist_fn:
Packit 3a9065
            raise dnf.exceptions.Error(NO_LOCKLIST)
Packit 3a9065
        with open(locklist_fn) as llfile:
Packit 3a9065
            for line in llfile.readlines():
Packit 3a9065
                if line.startswith('#') or line.strip() == '':
Packit 3a9065
                    continue
Packit 3a9065
                locklist.append(line.strip())
Packit 3a9065
    except IOError as e:
Packit 3a9065
        raise dnf.exceptions.Error(NOT_READABLE % e)
Packit 3a9065
    return locklist
Packit 3a9065
Packit 3a9065
Packit 3a9065
def _search_locklist(package):
Packit 3a9065
    found = action = ''
Packit 3a9065
    locked_specs = _read_locklist()
Packit 3a9065
    for ent in locked_specs:
Packit 3a9065
        if _match(ent, package):
Packit 3a9065
            found = ent
Packit 3a9065
            action = 'exclude' if ent.startswith('!') else 'add'
Packit 3a9065
            break
Packit 3a9065
    return (found, action)
Packit 3a9065
Packit 3a9065
Packit 3a9065
def _write_locklist(base, args, raw, try_installed, comment, info, prefix):
Packit 3a9065
    specs = set()
Packit 3a9065
    for pat in args:
Packit 3a9065
        if raw:
Packit 3a9065
            specs.add(pat)
Packit 3a9065
            continue
Packit 3a9065
        subj = dnf.subject.Subject(pat)
Packit 3a9065
        pkgs = None
Packit 3a9065
        if try_installed:
Packit 3a9065
            pkgs = subj.get_best_query(dnf.sack._rpmdb_sack(base), with_nevra=True,
Packit 3a9065
                                       with_provides=False, with_filenames=False)
Packit 3a9065
        if not pkgs:
Packit 3a9065
            pkgs = subj.get_best_query(base.sack, with_nevra=True, with_provides=False,
Packit 3a9065
                                       with_filenames=False)
Packit 3a9065
        if not pkgs:
Packit 3a9065
            print("%s %s" % (NOTFOUND_SPEC, pat))
Packit 3a9065
Packit 3a9065
        for pkg in pkgs:
Packit 3a9065
            specs.add(pkgtup2spec(*pkg.pkgtup))
Packit 3a9065
Packit 3a9065
    if specs:
Packit 3a9065
        try:
Packit 3a9065
            if not locklist_fn:
Packit 3a9065
                raise dnf.exceptions.Error(NO_LOCKLIST)
Packit 3a9065
            with open(locklist_fn, 'a') as f:
Packit 3a9065
                f.write(comment)
Packit 3a9065
                for spec in specs:
Packit 3a9065
                    print("%s %s" % (info, spec))
Packit 3a9065
                    f.write("%s%s\n" % (prefix, spec))
Packit 3a9065
        except IOError as e:
Packit 3a9065
            raise dnf.exceptions.Error(NOT_READABLE % e)
Packit 3a9065
Packit 3a9065
def _match(ent, patterns):
Packit 3a9065
    ent = ent.lstrip('!')
Packit 3a9065
    for pat in patterns:
Packit 3a9065
        if ent == pat:
Packit 3a9065
            return True
Packit 3a9065
    try:
Packit 3a9065
        n = hawkey.split_nevra(ent)
Packit 3a9065
    except hawkey.ValueException:
Packit 3a9065
        return False
Packit 3a9065
    for name in (
Packit 3a9065
        '%s' % n.name,
Packit 3a9065
        '%s.%s' % (n.name, n.arch),
Packit 3a9065
        '%s-%s' % (n.name, n.version),
Packit 3a9065
        '%s-%s-%s' % (n.name, n.version, n.release),
Packit 3a9065
        '%s-%s:%s' % (n.name, n.epoch, n.version),
Packit 3a9065
        '%s-%s-%s.%s' % (n.name, n.version, n.release, n.arch),
Packit 3a9065
        '%s-%s:%s-%s' % (n.name, n.epoch, n.version, n.release),
Packit 3a9065
        '%s:%s-%s-%s.%s' % (n.epoch, n.name, n.version, n.release, n.arch),
Packit 3a9065
        '%s-%s:%s-%s.%s' % (n.name, n.epoch, n.version, n.release, n.arch),
Packit 3a9065
    ):
Packit 3a9065
        for pat in patterns:
Packit 3a9065
            if fnmatch.fnmatch(name, pat):
Packit 3a9065
                return True
Packit 3a9065
    return False
Packit 3a9065
Packit 3a9065
Packit 3a9065
def pkgtup2spec(name, arch, epoch, version, release):
Packit 3a9065
    # we ignore arch
Packit 3a9065
    e = "" if epoch in (None, "") else "%s:" % epoch
Packit 3a9065
    return "%s-%s%s-%s.*" % (name, e, version, release)