Blame dnf/cli/commands/history.py

Packit Service 21c75c
# Copyright 2006 Duke University
Packit Service 21c75c
# Copyright (C) 2012-2016 Red Hat, Inc.
Packit Service 21c75c
#
Packit Service 21c75c
# This program is free software; you can redistribute it and/or modify
Packit Service 21c75c
# it under the terms of the GNU General Public License as published by
Packit Service 21c75c
# the Free Software Foundation; either version 2 of the License, or
Packit Service 21c75c
# (at your option) any later version.
Packit Service 21c75c
#
Packit Service 21c75c
# This program is distributed in the hope that it will be useful,
Packit Service 21c75c
# but WITHOUT ANY WARRANTY; without even the implied warranty of
Packit Service 21c75c
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
Packit Service 21c75c
# GNU Library General Public License for more details.
Packit Service 21c75c
#
Packit Service 21c75c
# You should have received a copy of the GNU General Public License
Packit Service 21c75c
# along with this program; if not, write to the Free Software
Packit Service 21c75c
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Packit Service 21c75c
Packit Service 21c75c
from __future__ import absolute_import
Packit Service 21c75c
from __future__ import print_function
Packit Service 21c75c
from __future__ import unicode_literals
Packit Service 21c75c
Packit Service 21c75c
import libdnf
Packit Service 2bb387
import hawkey
Packit Service 21c75c
Packit Service 21c75c
from dnf.i18n import _, ucd
Packit Service 21c75c
from dnf.cli import commands
Packit Service 21c75c
from dnf.transaction_sr import TransactionReplay, serialize_transaction
Packit Service 21c75c
Packit Service 21c75c
import dnf.cli
Packit Service 21c75c
import dnf.exceptions
Packit Service 21c75c
import dnf.transaction
Packit Service 21c75c
import dnf.util
Packit Service 21c75c
Packit Service 21c75c
import json
Packit Service 21c75c
import logging
Packit Service 21c75c
import os
Packit Service 21c75c
Packit Service 21c75c
Packit Service 21c75c
logger = logging.getLogger('dnf')
Packit Service 21c75c
Packit Service 21c75c
Packit Service 21c75c
class HistoryCommand(commands.Command):
Packit Service 21c75c
    """A class containing methods needed by the cli to execute the
Packit Service 21c75c
    history command.
Packit Service 21c75c
    """
Packit Service 21c75c
Packit Service 21c75c
    aliases = ('history', 'hist')
Packit Service 21c75c
    summary = _('display, or use, the transaction history')
Packit Service 21c75c
Packit Service 21c75c
    _CMDS = ['list', 'info', 'redo', 'replay', 'rollback', 'store', 'undo', 'userinstalled']
Packit Service 21c75c
Packit Service 21c75c
    def __init__(self, *args, **kw):
Packit Service 21c75c
        super(HistoryCommand, self).__init__(*args, **kw)
Packit Service 21c75c
Packit Service 21c75c
        self._require_one_transaction_id = False
Packit Service 21c75c
Packit Service 21c75c
    @staticmethod
Packit Service 21c75c
    def set_argparser(parser):
Packit Service 21c75c
        parser.add_argument('transactions_action', nargs='?', metavar="COMMAND",
Packit Service 21c75c
                            help="Available commands: {} (default), {}".format(
Packit Service 21c75c
                                HistoryCommand._CMDS[0],
Packit Service 21c75c
                                ", ".join(HistoryCommand._CMDS[1:])))
Packit Service 21c75c
        parser.add_argument('--reverse', action='store_true',
Packit Service 21c75c
                            help="display history list output reversed")
Packit Service 21c75c
        parser.add_argument("-o", "--output", default=None,
Packit Service 21c75c
                            help=_("For the store command, file path to store the transaction to"))
Packit Service 21c75c
        parser.add_argument("--ignore-installed", action="store_true",
Packit Service 21c75c
                            help=_("For the replay command, don't check for installed packages matching "
Packit Service 21c75c
                            "those in transaction"))
Packit Service 21c75c
        parser.add_argument("--ignore-extras", action="store_true",
Packit Service 21c75c
                            help=_("For the replay command, don't check for extra packages pulled "
Packit Service 21c75c
                            "into the transaction"))
Packit Service 21c75c
        parser.add_argument("--skip-unavailable", action="store_true",
Packit Service 21c75c
                            help=_("For the replay command, skip packages that are not available or have "
Packit Service 21c75c
                            "missing dependencies"))
Packit Service 21c75c
        parser.add_argument('transactions', nargs='*', metavar="TRANSACTION",
Packit Service 21c75c
                            help="For commands working with history transactions, "
Packit Service 21c75c
                                 "Transaction ID (<number>, 'last' or 'last-<number>' "
Packit Service 21c75c
                                 "for one transaction, <transaction-id>..<transaction-id> "
Packit Service 21c75c
                                 "for a range)")
Packit Service 21c75c
        parser.add_argument('transaction_filename', nargs='?', metavar="TRANSACTION_FILE",
Packit Service 21c75c
                            help="For the replay command, path to the stored "
Packit Service 21c75c
                                 "transaction file to replay")
Packit Service 21c75c
Packit Service 21c75c
    def configure(self):
Packit Service 21c75c
        if not self.opts.transactions_action:
Packit Service 21c75c
            # no positional argument given
Packit Service 21c75c
            self.opts.transactions_action = self._CMDS[0]
Packit Service 21c75c
        elif self.opts.transactions_action not in self._CMDS:
Packit Service 21c75c
            # first positional argument is not a command
Packit Service 21c75c
            self.opts.transactions.insert(0, self.opts.transactions_action)
Packit Service 21c75c
            self.opts.transactions_action = self._CMDS[0]
Packit Service 21c75c
Packit Service 21c75c
        self._require_one_transaction_id_msg = _("Found more than one transaction ID.\n"
Packit Service 21c75c
                                                 "'{}' requires one transaction ID or package name."
Packit Service 21c75c
                                                 ).format(self.opts.transactions_action)
Packit Service 21c75c
Packit Service 21c75c
        demands = self.cli.demands
Packit Service 21c75c
        if self.opts.transactions_action == 'replay':
Packit Service 21c75c
            if not self.opts.transactions:
Packit Service 21c75c
                raise dnf.cli.CliError(_('No transaction file name given.'))
Packit Service 21c75c
            if len(self.opts.transactions) > 1:
Packit Service 21c75c
                raise dnf.cli.CliError(_('More than one argument given as transaction file name.'))
Packit Service 21c75c
Packit Service 21c75c
            # in case of replay, copy over the file name to it's appropriate variable
Packit Service 21c75c
            # (the arg parser can't distinguish here)
Packit Service 21c75c
            self.opts.transaction_filename = os.path.abspath(self.opts.transactions[0])
Packit Service 21c75c
            self.opts.transactions = []
Packit Service 21c75c
Packit Service 21c75c
            demands.available_repos = True
Packit Service 21c75c
            demands.resolving = True
Packit Service 21c75c
            demands.root_user = True
Packit Service 21c75c
Packit Service 21c75c
            # Override configuration options that affect how the transaction is resolved
Packit Service 21c75c
            self.base.conf.clean_requirements_on_remove = False
Packit Service 21c75c
            self.base.conf.install_weak_deps = False
Packit Service 21c75c
Packit Service 21c75c
            dnf.cli.commands._checkGPGKey(self.base, self.cli)
Packit Service 21c75c
        elif self.opts.transactions_action == 'store':
Packit Service 21c75c
            self._require_one_transaction_id = True
Packit Service 21c75c
            if not self.opts.transactions:
Packit Service 21c75c
                raise dnf.cli.CliError(_('No transaction ID or package name given.'))
Packit Service 21c75c
        elif self.opts.transactions_action in ['redo', 'undo', 'rollback']:
Packit Service 2bb387
            demands.available_repos = True
Packit Service 2bb387
            demands.resolving = True
Packit Service 2bb387
            demands.root_user = True
Packit Service 2bb387
Packit Service 21c75c
            self._require_one_transaction_id = True
Packit Service 21c75c
            if not self.opts.transactions:
Packit Service 21c75c
                msg = _('No transaction ID or package name given.')
Packit Service 21c75c
                logger.critical(msg)
Packit Service 21c75c
                raise dnf.cli.CliError(msg)
Packit Service 21c75c
            elif len(self.opts.transactions) > 1:
Packit Service 21c75c
                logger.critical(self._require_one_transaction_id_msg)
Packit Service 21c75c
                raise dnf.cli.CliError(self._require_one_transaction_id_msg)
Packit Service 21c75c
            demands.available_repos = True
Packit Service 21c75c
            dnf.cli.commands._checkGPGKey(self.base, self.cli)
Packit Service 21c75c
        else:
Packit Service 21c75c
            demands.fresh_metadata = False
Packit Service 21c75c
        demands.sack_activation = True
Packit Service 21c75c
        if self.base.history.path != ":memory:" and not os.access(self.base.history.path, os.R_OK):
Packit Service 21c75c
            msg = _("You don't have access to the history DB: %s" % self.base.history.path)
Packit Service 21c75c
            logger.critical(msg)
Packit Service 21c75c
            raise dnf.cli.CliError(msg)
Packit Service 21c75c
Packit Service 21c75c
    def get_error_output(self, error):
Packit Service 21c75c
        """Get suggestions for resolving the given error."""
Packit Service 21c75c
        if isinstance(error, dnf.exceptions.TransactionCheckError):
Packit Service 21c75c
            if self.opts.transactions_action == 'undo':
Packit Service 21c75c
                id_, = self.opts.transactions
Packit Service 21c75c
                return (_('Cannot undo transaction %s, doing so would result '
Packit Service 21c75c
                          'in an inconsistent package database.') % id_,)
Packit Service 21c75c
            elif self.opts.transactions_action == 'rollback':
Packit Service 21c75c
                id_, = (self.opts.transactions if self.opts.transactions[0] != 'force'
Packit Service 21c75c
                        else self.opts.transactions[1:])
Packit Service 21c75c
                return (_('Cannot rollback transaction %s, doing so would '
Packit Service 21c75c
                          'result in an inconsistent package database.') % id_,)
Packit Service 21c75c
Packit Service 21c75c
        return dnf.cli.commands.Command.get_error_output(self, error)
Packit Service 21c75c
Packit Service 21c75c
    def _hcmd_redo(self, extcmds):
Packit Service 2bb387
        old = self._history_get_transaction(extcmds)
Packit Service 2bb387
        data = serialize_transaction(old)
Packit Service 2bb387
        self.replay = TransactionReplay(
Packit Service 2bb387
            self.base,
Packit Service 2bb387
            data=data,
Packit Service 2bb387
            ignore_installed=True,
Packit Service 2bb387
            ignore_extras=True,
Packit Service 2bb387
            skip_unavailable=self.opts.skip_unavailable
Packit Service 2bb387
        )
Packit Service 2bb387
        self.replay.run()
Packit Service 2bb387
Packit Service 2bb387
    def _history_get_transactions(self, extcmds):
Packit Service 2bb387
        if not extcmds:
Packit Service 2bb387
            raise dnf.cli.CliError(_('No transaction ID given'))
Packit Service 2bb387
Packit Service 2bb387
        old = self.base.history.old(extcmds)
Packit Service 2bb387
        if not old:
Packit Service 2bb387
            raise dnf.cli.CliError(_('Transaction ID "{0}" not found.').format(extcmds[0]))
Packit Service 2bb387
        return old
Packit Service 2bb387
Packit Service 2bb387
    def _history_get_transaction(self, extcmds):
Packit Service 2bb387
        old = self._history_get_transactions(extcmds)
Packit Service 2bb387
        if len(old) > 1:
Packit Service 2bb387
            raise dnf.cli.CliError(_('Found more than one transaction ID!'))
Packit Service 2bb387
        return old[0]
Packit Service 21c75c
Packit Service 21c75c
    def _hcmd_undo(self, extcmds):
Packit Service 2bb387
        old = self._history_get_transaction(extcmds)
Packit Service 2bb387
        self._revert_transaction(old)
Packit Service 21c75c
Packit Service 21c75c
    def _hcmd_rollback(self, extcmds):
Packit Service 2bb387
        old = self._history_get_transaction(extcmds)
Packit Service 2bb387
        last = self.base.history.last()
Packit Service 2bb387
Packit Service 2bb387
        merged_trans = None
Packit Service 2bb387
        if old.tid != last.tid:
Packit Service 2bb387
            # history.old([]) returns all transactions and we don't want that
Packit Service 2bb387
            # so skip merging the transactions when trying to rollback to the last transaction
Packit Service 2bb387
            # which is the current system state and rollback is not applicable
Packit Service 2bb387
            for trans in self.base.history.old(list(range(old.tid + 1, last.tid + 1))):
Packit Service 2bb387
                if trans.altered_lt_rpmdb:
Packit Service 2bb387
                    logger.warning(_('Transaction history is incomplete, before %u.'), trans.tid)
Packit Service 2bb387
                elif trans.altered_gt_rpmdb:
Packit Service 2bb387
                    logger.warning(_('Transaction history is incomplete, after %u.'), trans.tid)
Packit Service 2bb387
Packit Service 2bb387
                if merged_trans is None:
Packit Service 2bb387
                    merged_trans = dnf.db.history.MergedTransactionWrapper(trans)
Packit Service 2bb387
                else:
Packit Service 2bb387
                    merged_trans.merge(trans)
Packit Service 2bb387
Packit Service 2bb387
        self._revert_transaction(merged_trans)
Packit Service 2bb387
Packit Service 2bb387
    def _revert_transaction(self, trans):
Packit Service 2bb387
        action_map = {
Packit Service 2bb387
            "Install": "Removed",
Packit Service 2bb387
            "Removed": "Install",
Packit Service 2bb387
            "Upgrade": "Downgraded",
Packit Service 2bb387
            "Upgraded": "Downgrade",
Packit Service 2bb387
            "Downgrade": "Upgraded",
Packit Service 2bb387
            "Downgraded": "Upgrade",
Packit Service 2bb387
            "Reinstalled": "Reinstall",
Packit Service 2bb387
            "Reinstall": "Reinstalled",
Packit Service 2bb387
            "Obsoleted": "Install",
Packit Service 2bb387
            "Obsolete": "Obsoleted",
Packit Service 2bb387
        }
Packit Service 2bb387
Packit Service 2bb387
        data = serialize_transaction(trans)
Packit Service 2bb387
Packit Service 2bb387
        # revert actions in the serialized transaction data to perform rollback/undo
Packit Service 2bb387
        for content_type in ("rpms", "groups", "environments"):
Packit Service 2bb387
            for ti in data.get(content_type, []):
Packit Service 2bb387
                ti["action"] = action_map[ti["action"]]
Packit Service 2bb387
Packit Service 2bb387
                if ti["action"] == "Install" and ti.get("reason", None) == "clean":
Packit Service 2bb387
                    ti["reason"] = "dependency"
Packit Service 2bb387
Packit Service 2bb387
                if ti.get("repo_id") == hawkey.SYSTEM_REPO_NAME:
Packit Service 2bb387
                    # erase repo_id, because it's not possible to perform forward actions from the @System repo
Packit Service 2bb387
                    ti["repo_id"] = None
Packit Service 2bb387
Packit Service 2bb387
        self.replay = TransactionReplay(
Packit Service 2bb387
            self.base,
Packit Service 2bb387
            data=data,
Packit Service 2bb387
            ignore_installed=True,
Packit Service 2bb387
            ignore_extras=True,
Packit Service 2bb387
            skip_unavailable=self.opts.skip_unavailable
Packit Service 2bb387
        )
Packit Service 2bb387
        self.replay.run()
Packit Service 21c75c
Packit Service 21c75c
    def _hcmd_userinstalled(self):
Packit Service 21c75c
        """Execute history userinstalled command."""
Packit Service 21c75c
        pkgs = tuple(self.base.iter_userinstalled())
Packit Service 2bb387
        n_listed = self.output.listPkgs(pkgs, 'Packages installed by user', 'nevra')
Packit Service 2bb387
        if n_listed == 0:
Packit Service 2bb387
            raise dnf.cli.CliError(_('No packages to list'))
Packit Service 21c75c
Packit Service 21c75c
    def _args2transaction_ids(self):
Packit Service 21c75c
        """Convert commandline arguments to transaction ids"""
Packit Service 21c75c
Packit Service 21c75c
        def str2transaction_id(s):
Packit Service 21c75c
            if s == 'last':
Packit Service 21c75c
                s = '0'
Packit Service 21c75c
            elif s.startswith('last-'):
Packit Service 21c75c
                s = s[4:]
Packit Service 21c75c
            transaction_id = int(s)
Packit Service 21c75c
            if transaction_id <= 0:
Packit Service 21c75c
                transaction_id += self.output.history.last().tid
Packit Service 21c75c
            return transaction_id
Packit Service 21c75c
Packit Service 21c75c
        tids = set()
Packit Service 21c75c
        merged_tids = set()
Packit Service 21c75c
        for t in self.opts.transactions:
Packit Service 21c75c
            if '..' in t:
Packit Service 21c75c
                try:
Packit Service 21c75c
                    begin_transaction_id, end_transaction_id = t.split('..', 2)
Packit Service 21c75c
                except ValueError:
Packit Service 21c75c
                    logger.critical(
Packit Service 21c75c
                        _("Invalid transaction ID range definition '{}'.\n"
Packit Service 21c75c
                          "Use '<transaction-id>..<transaction-id>'."
Packit Service 21c75c
                          ).format(t))
Packit Service 21c75c
                    raise dnf.cli.CliError
Packit Service 21c75c
                cant_convert_msg = _("Can't convert '{}' to transaction ID.\n"
Packit Service 21c75c
                                     "Use '<number>', 'last', 'last-<number>'.")
Packit Service 21c75c
                try:
Packit Service 21c75c
                    begin_transaction_id = str2transaction_id(begin_transaction_id)
Packit Service 21c75c
                except ValueError:
Packit Service 21c75c
                    logger.critical(_(cant_convert_msg).format(begin_transaction_id))
Packit Service 21c75c
                    raise dnf.cli.CliError
Packit Service 21c75c
                try:
Packit Service 21c75c
                    end_transaction_id = str2transaction_id(end_transaction_id)
Packit Service 21c75c
                except ValueError:
Packit Service 21c75c
                    logger.critical(_(cant_convert_msg).format(end_transaction_id))
Packit Service 21c75c
                    raise dnf.cli.CliError
Packit Service 21c75c
                if self._require_one_transaction_id and begin_transaction_id != end_transaction_id:
Packit Service 21c75c
                        logger.critical(self._require_one_transaction_id_msg)
Packit Service 21c75c
                        raise dnf.cli.CliError
Packit Service 21c75c
                if begin_transaction_id > end_transaction_id:
Packit Service 21c75c
                    begin_transaction_id, end_transaction_id = \
Packit Service 21c75c
                        end_transaction_id, begin_transaction_id
Packit Service 21c75c
                merged_tids.add((begin_transaction_id, end_transaction_id))
Packit Service 21c75c
                tids.update(range(begin_transaction_id, end_transaction_id + 1))
Packit Service 21c75c
            else:
Packit Service 21c75c
                try:
Packit Service 21c75c
                    tids.add(str2transaction_id(t))
Packit Service 21c75c
                except ValueError:
Packit Service 21c75c
                    # not a transaction id, assume it's package name
Packit Service 21c75c
                    transact_ids_from_pkgname = self.output.history.search([t])
Packit Service 21c75c
                    if transact_ids_from_pkgname:
Packit Service 21c75c
                        tids.update(transact_ids_from_pkgname)
Packit Service 21c75c
                    else:
Packit Service 21c75c
                        msg = _("No transaction which manipulates package '{}' was found."
Packit Service 21c75c
                                ).format(t)
Packit Service 21c75c
                        if self._require_one_transaction_id:
Packit Service 21c75c
                            logger.critical(msg)
Packit Service 21c75c
                            raise dnf.cli.CliError
Packit Service 21c75c
                        else:
Packit Service 21c75c
                            logger.info(msg)
Packit Service 21c75c
Packit Service 21c75c
        return sorted(tids, reverse=True), merged_tids
Packit Service 21c75c
Packit Service 21c75c
    def run(self):
Packit Service 21c75c
        vcmd = self.opts.transactions_action
Packit Service 21c75c
Packit Service 21c75c
        if vcmd == 'replay':
Packit Service 21c75c
            self.replay = TransactionReplay(
Packit Service 21c75c
                self.base,
Packit Service 2bb387
                filename=self.opts.transaction_filename,
Packit Service 21c75c
                ignore_installed = self.opts.ignore_installed,
Packit Service 21c75c
                ignore_extras = self.opts.ignore_extras,
Packit Service 21c75c
                skip_unavailable = self.opts.skip_unavailable
Packit Service 21c75c
            )
Packit Service 21c75c
            self.replay.run()
Packit Service 21c75c
        else:
Packit Service 21c75c
            tids, merged_tids = self._args2transaction_ids()
Packit Service 21c75c
Packit Service 21c75c
            if vcmd == 'list' and (tids or not self.opts.transactions):
Packit Service 2bb387
                self.output.historyListCmd(tids, reverse=self.opts.reverse)
Packit Service 21c75c
            elif vcmd == 'info' and (tids or not self.opts.transactions):
Packit Service 2bb387
                self.output.historyInfoCmd(tids, self.opts.transactions, merged_tids)
Packit Service 21c75c
            elif vcmd == 'undo':
Packit Service 2bb387
                self._hcmd_undo(tids)
Packit Service 21c75c
            elif vcmd == 'redo':
Packit Service 2bb387
                self._hcmd_redo(tids)
Packit Service 21c75c
            elif vcmd == 'rollback':
Packit Service 2bb387
                self._hcmd_rollback(tids)
Packit Service 21c75c
            elif vcmd == 'userinstalled':
Packit Service 2bb387
                self._hcmd_userinstalled()
Packit Service 21c75c
            elif vcmd == 'store':
Packit Service 2bb387
                tid = self._history_get_transaction(tids)
Packit Service 2bb387
                data = serialize_transaction(tid)
Packit Service 21c75c
                try:
Packit Service 21c75c
                    filename = self.opts.output if self.opts.output is not None else "transaction.json"
Packit Service 21c75c
Packit Service 21c75c
                    # it is absolutely possible for both assumeyes and assumeno to be True, go figure
Packit Service 21c75c
                    if (self.base.conf.assumeno or not self.base.conf.assumeyes) and os.path.isfile(filename):
Packit Service 21c75c
                        msg = _("{} exists, overwrite?").format(filename)
Packit Service 21c75c
                        if self.base.conf.assumeno or not self.base.output.userconfirm(
Packit Service 21c75c
                            msg='\n{} [y/N]: '.format(msg), defaultyes_msg='\n{} [Y/n]: '.format(msg)):
Packit Service 21c75c
                                print(_("Not overwriting {}, exiting.").format(filename))
Packit Service 21c75c
                                return
Packit Service 21c75c
Packit Service 21c75c
                    with open(filename, "w") as f:
Packit Service 21c75c
                        json.dump(data, f, indent=4, sort_keys=True)
Packit Service 21c75c
                        f.write("\n")
Packit Service 21c75c
Packit Service 21c75c
                    print(_("Transaction saved to {}.").format(filename))
Packit Service 21c75c
Packit Service 21c75c
                except OSError as e:
Packit Service 21c75c
                    raise dnf.cli.CliError(_('Error storing transaction: {}').format(str(e)))
Packit Service 21c75c
Packit Service 21c75c
    def run_resolved(self):
Packit Service 2bb387
        if self.opts.transactions_action not in ("replay", "redo", "rollback", "undo"):
Packit Service 21c75c
            return
Packit Service 21c75c
Packit Service 21c75c
        self.replay.post_transaction()
Packit Service 21c75c
Packit Service 21c75c
    def run_transaction(self):
Packit Service 2bb387
        if self.opts.transactions_action not in ("replay", "redo", "rollback", "undo"):
Packit Service 21c75c
            return
Packit Service 21c75c
Packit Service 21c75c
        warnings = self.replay.get_warnings()
Packit Service 21c75c
        if warnings:
Packit Service 21c75c
            logger.log(
Packit Service 21c75c
                dnf.logging.WARNING,
Packit Service 2bb387
                _("Warning, the following problems occurred while running a transaction:")
Packit Service 21c75c
            )
Packit Service 21c75c
            for w in warnings:
Packit Service 21c75c
                logger.log(dnf.logging.WARNING, "  " + w)