Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transaction Store/Replay #1630

Merged
merged 10 commits into from
Sep 4, 2020
3 changes: 2 additions & 1 deletion dnf.spec
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# default dependencies
%global hawkey_version 0.52.0
%global hawkey_version 0.53.0
%global libcomps_version 0.1.8
%global libmodulemd_version 1.4.0
%global rpm_version 4.14.0
Expand Down Expand Up @@ -405,6 +405,7 @@ ln -sr %{buildroot}%{confdir}/vars %{buildroot}%{_sysconfdir}/yum/vars
%{_mandir}/man8/%{name}.8*
%{_mandir}/man8/yum2dnf.8*
%{_mandir}/man7/dnf.modularity.7*
%{_mandir}/man5/dnf-transaction-json.5*
%{_unitdir}/%{name}-makecache.service
%{_unitdir}/%{name}-makecache.timer
%{_var}/cache/%{name}/
Expand Down
33 changes: 16 additions & 17 deletions dnf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1565,9 +1565,17 @@ def reason_fn(pkgname):

def environment_install(self, env_id, types, exclude=None, strict=True, exclude_groups=None):
# :api
"""Installs packages of environment group identified by env_id.
:param types: Types of packages to install. Either an integer as a
logical conjunction of CompsPackageType ids or a list of string
package type ids (conditional, default, mandatory, optional).
"""
assert dnf.util.is_string_type(env_id)
solver = self._build_comps_solver()
types = self._translate_comps_pkg_types(types)

if not isinstance(types, int):
types = libdnf.transaction.listToCompsPackageType(types)

trans = dnf.comps.install_or_skip(solver._environment_install,
env_id, types, exclude or set(),
strict, exclude_groups)
Expand All @@ -1582,24 +1590,12 @@ def environment_remove(self, env_id):
trans = solver._environment_remove(env_id)
return self._add_comps_trans(trans)

_COMPS_TRANSLATION = {
'default': dnf.comps.DEFAULT,
'mandatory': dnf.comps.MANDATORY,
'optional': dnf.comps.OPTIONAL,
'conditional': dnf.comps.CONDITIONAL
}

@staticmethod
def _translate_comps_pkg_types(pkg_types):
ret = 0
for (name, enum) in Base._COMPS_TRANSLATION.items():
if name in pkg_types:
ret |= enum
return ret

def group_install(self, grp_id, pkg_types, exclude=None, strict=True):
# :api
"""Installs packages of selected group
:param pkg_types: Types of packages to install. Either an integer as a
logical conjunction of CompsPackageType ids or a list of string
package type ids (conditional, default, mandatory, optional).
:param exclude: list of package name glob patterns
that will be excluded from install set
:param strict: boolean indicating whether group packages that
Expand All @@ -1621,7 +1617,10 @@ def _pattern_to_pkgname(pattern):
exclude_pkgnames = itertools.chain.from_iterable(nested_excludes)

solver = self._build_comps_solver()
pkg_types = self._translate_comps_pkg_types(pkg_types)

if not isinstance(pkg_types, int):
pkg_types = libdnf.transaction.listToCompsPackageType(pkg_types)

trans = dnf.comps.install_or_skip(solver._group_install,
grp_id, pkg_types, exclude_pkgnames,
strict)
Expand Down
3 changes: 2 additions & 1 deletion dnf/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import dnf.cli.commands.distrosync
import dnf.cli.commands.downgrade
import dnf.cli.commands.group
import dnf.cli.commands.history
import dnf.cli.commands.install
import dnf.cli.commands.makecache
import dnf.cli.commands.mark
Expand Down Expand Up @@ -722,6 +723,7 @@ def __init__(self, base):
self.register_command(dnf.cli.commands.deplist.DeplistCommand)
self.register_command(dnf.cli.commands.downgrade.DowngradeCommand)
self.register_command(dnf.cli.commands.group.GroupCommand)
self.register_command(dnf.cli.commands.history.HistoryCommand)
self.register_command(dnf.cli.commands.install.InstallCommand)
self.register_command(dnf.cli.commands.makecache.MakeCacheCommand)
self.register_command(dnf.cli.commands.mark.MarkCommand)
Expand All @@ -742,7 +744,6 @@ def __init__(self, base):
self.register_command(dnf.cli.commands.CheckUpdateCommand)
self.register_command(dnf.cli.commands.RepoPkgsCommand)
self.register_command(dnf.cli.commands.HelpCommand)
self.register_command(dnf.cli.commands.HistoryCommand)

def _configure_repos(self, opts):
self.base.read_all_repos(opts)
Expand Down
224 changes: 1 addition & 223 deletions dnf/cli/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,14 @@
from __future__ import print_function
from __future__ import unicode_literals

import libdnf

from dnf.cli.option_parser import OptionParser
from dnf.i18n import _, ucd
from dnf.i18n import _

import argparse
import dnf.cli
import dnf.cli.demand
import dnf.const
import dnf.exceptions
import dnf.i18n
import dnf.pycomp
import dnf.transaction
import dnf.util
import functools
import logging
import operator
import os

logger = logging.getLogger('dnf')
Expand Down Expand Up @@ -821,216 +812,3 @@ def run(self):
else:
command = self.cli.cli_commands[self.opts.cmd]
self.cli.optparser.print_help(command(self))

class HistoryCommand(Command):
"""A class containing methods needed by the cli to execute the
history command.
"""

aliases = ('history', 'hist')
summary = _('display, or use, the transaction history')

_CMDS = ['list', 'info', 'redo', 'undo', 'rollback', 'userinstalled']

transaction_ids = set()
merged_transaction_ids = set()

@staticmethod
def set_argparser(parser):
parser.add_argument('transactions_action', nargs='?', metavar="COMMAND",
help="Available commands: {} (default), {}".format(
HistoryCommand._CMDS[0],
", ".join(HistoryCommand._CMDS[1:])))
parser.add_argument('--reverse', action='store_true',
help="display history list output reversed")
parser.add_argument('transactions', nargs='*', metavar="TRANSACTION",
help="Transaction ID (<number>, 'last' or 'last-<number>' "
"for one transaction, <transaction-id>..<transaction-id> "
"for range)")

def configure(self):
if not self.opts.transactions_action:
# no positional argument given
self.opts.transactions_action = self._CMDS[0]
elif self.opts.transactions_action not in self._CMDS:
# first positional argument is not a command
self.opts.transactions.insert(0, self.opts.transactions_action)
self.opts.transactions_action = self._CMDS[0]

require_one_transaction_id = False
require_one_transaction_id_msg = _("Found more than one transaction ID.\n"
"'{}' requires one transaction ID or package name."
).format(self.opts.transactions_action)
demands = self.cli.demands
if self.opts.transactions_action in ['redo', 'undo', 'rollback']:
demands.root_user = True
require_one_transaction_id = True
if not self.opts.transactions:
msg = _('No transaction ID or package name given.')
logger.critical(msg)
raise dnf.cli.CliError(msg)
elif len(self.opts.transactions) > 1:
logger.critical(require_one_transaction_id_msg)
raise dnf.cli.CliError(require_one_transaction_id_msg)
demands.available_repos = True
_checkGPGKey(self.base, self.cli)
else:
demands.fresh_metadata = False
demands.sack_activation = True
if self.base.history.path != ":memory:" and not os.access(self.base.history.path, os.R_OK):
msg = _("You don't have access to the history DB: %s" % self.base.history.path)
logger.critical(msg)
raise dnf.cli.CliError(msg)
self.transaction_ids = self._args2transaction_ids(self.merged_transaction_ids,
require_one_transaction_id,
require_one_transaction_id_msg)

def get_error_output(self, error):
"""Get suggestions for resolving the given error."""
if isinstance(error, dnf.exceptions.TransactionCheckError):
if self.opts.transactions_action == 'undo':
id_, = self.opts.transactions
return (_('Cannot undo transaction %s, doing so would result '
'in an inconsistent package database.') % id_,)
elif self.opts.transactions_action == 'rollback':
id_, = (self.opts.transactions if self.opts.transactions[0] != 'force'
else self.opts.transactions[1:])
return (_('Cannot rollback transaction %s, doing so would '
'result in an inconsistent package database.') % id_,)

return Command.get_error_output(self, error)

def _hcmd_redo(self, extcmds):
old = self.base.history_get_transaction(extcmds)
if old is None:
return 1, ['Failed history redo']
tm = dnf.util.normalize_time(old.beg_timestamp)
print('Repeating transaction %u, from %s' % (old.tid, tm))
self.output.historyInfoCmdPkgsAltered(old)

for i in old.packages():
pkgs = list(self.base.sack.query().filter(nevra=str(i), reponame=i.from_repo))
if i.action in dnf.transaction.FORWARD_ACTIONS:
if not pkgs:
logger.info(_('No package %s available.'),
self.output.term.bold(ucd(str(i))))
return 1, ['An operation cannot be redone']
pkg = pkgs[0]
self.base.install(str(pkg))
elif i.action == libdnf.transaction.TransactionItemAction_REMOVE:
if not pkgs:
# package was removed already, we can skip removing it again
continue
pkg = pkgs[0]
self.base.remove(str(pkg))

self.base.resolve()
self.base.do_transaction()

def _hcmd_undo(self, extcmds):
try:
return self.base.history_undo_transaction(extcmds[0])
except dnf.exceptions.Error as err:
return 1, [str(err)]

def _hcmd_rollback(self, extcmds):
try:
return self.base.history_rollback_transaction(extcmds[0])
except dnf.exceptions.Error as err:
return 1, [str(err)]

def _hcmd_userinstalled(self):
"""Execute history userinstalled command."""
pkgs = tuple(self.base.iter_userinstalled())
return self.output.listPkgs(pkgs, 'Packages installed by user', 'nevra')

def _args2transaction_ids(self, merged_ids=set(),
require_one_trans_id=False, require_one_trans_id_msg=''):
"""Convert commandline arguments to transaction ids"""

def str2transaction_id(s):
if s == 'last':
s = '0'
elif s.startswith('last-'):
s = s[4:]
transaction_id = int(s)
if transaction_id <= 0:
transaction_id += self.output.history.last().tid
return transaction_id

transaction_ids = set()
for t in self.opts.transactions:
if '..' in t:
try:
begin_transaction_id, end_transaction_id = t.split('..', 2)
except ValueError:
logger.critical(
_("Invalid transaction ID range definition '{}'.\n"
"Use '<transaction-id>..<transaction-id>'."
).format(t))
raise dnf.cli.CliError
cant_convert_msg = _("Can't convert '{}' to transaction ID.\n"
"Use '<number>', 'last', 'last-<number>'.")
try:
begin_transaction_id = str2transaction_id(begin_transaction_id)
except ValueError:
logger.critical(_(cant_convert_msg).format(begin_transaction_id))
raise dnf.cli.CliError
try:
end_transaction_id = str2transaction_id(end_transaction_id)
except ValueError:
logger.critical(_(cant_convert_msg).format(end_transaction_id))
raise dnf.cli.CliError
if require_one_trans_id and begin_transaction_id != end_transaction_id:
logger.critical(require_one_trans_id_msg)
raise dnf.cli.CliError
if begin_transaction_id > end_transaction_id:
begin_transaction_id, end_transaction_id = \
end_transaction_id, begin_transaction_id
merged_ids.add((begin_transaction_id, end_transaction_id))
transaction_ids.update(range(begin_transaction_id, end_transaction_id + 1))
else:
try:
transaction_ids.add(str2transaction_id(t))
except ValueError:
# not a transaction id, assume it's package name
transact_ids_from_pkgname = self.output.history.search([t])
if transact_ids_from_pkgname:
transaction_ids.update(transact_ids_from_pkgname)
else:
msg = _("No transaction which manipulates package '{}' was found."
).format(t)
if require_one_trans_id:
logger.critical(msg)
raise dnf.cli.CliError
else:
logger.info(msg)

return sorted(transaction_ids, reverse=True)

def run(self):
vcmd = self.opts.transactions_action

ret = None
if vcmd == 'list' and (self.transaction_ids or not self.opts.transactions):
ret = self.output.historyListCmd(self.transaction_ids,
reverse=self.opts.reverse)
elif vcmd == 'info' and (self.transaction_ids or not self.opts.transactions):
ret = self.output.historyInfoCmd(self.transaction_ids, self.opts.transactions,
self.merged_transaction_ids)
elif vcmd == 'undo':
ret = self._hcmd_undo(self.transaction_ids)
elif vcmd == 'redo':
ret = self._hcmd_redo(self.transaction_ids)
elif vcmd == 'rollback':
ret = self._hcmd_rollback(self.transaction_ids)
elif vcmd == 'userinstalled':
ret = self._hcmd_userinstalled()

if ret is None:
return
(code, strs) = ret
if code == 2:
self.cli.demands.resolving = True
elif code != 0:
raise dnf.exceptions.Error(strs[0])
4 changes: 3 additions & 1 deletion dnf/cli/commands/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
from dnf.cli import commands
from dnf.i18n import _, ucd

import libdnf.transaction

import dnf.cli
import dnf.exceptions
import dnf.util
Expand Down Expand Up @@ -243,7 +245,7 @@ def _mark_install(self, patterns):
types = tuple(self.base.conf.group_package_types + ['optional'])
else:
types = tuple(self.base.conf.group_package_types)
pkg_types = self.base._translate_comps_pkg_types(types)
pkg_types = libdnf.transaction.listToCompsPackageType(types)
for env_id in res.environments:
dnf.comps.install_or_skip(solver._environment_install, env_id, pkg_types)
for group_id in res.groups:
Expand Down
Loading