Skip to content

Commit

Permalink
Implement storing and replaying a transaction
Browse files Browse the repository at this point in the history
  • Loading branch information
Lukáš Hrázký authored and j-mracek committed Sep 4, 2020
1 parent 4d79599 commit 8e17bcc
Show file tree
Hide file tree
Showing 8 changed files with 974 additions and 21 deletions.
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
144 changes: 124 additions & 20 deletions dnf/cli/commands/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,17 @@

from dnf.i18n import _, ucd
from dnf.cli import commands
from dnf.transaction_sr import TransactionReplay, serialize_transaction

import dnf.cli
import dnf.exceptions
import dnf.transaction
import dnf.util

import json
import logging
import os
import sys


logger = logging.getLogger('dnf')
Expand All @@ -43,7 +47,7 @@ class HistoryCommand(commands.Command):
aliases = ('history', 'hist')
summary = _('display, or use, the transaction history')

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

def __init__(self, *args, **kw):
super(HistoryCommand, self).__init__(*args, **kw)
Expand All @@ -58,10 +62,25 @@ def set_argparser(parser):
", ".join(HistoryCommand._CMDS[1:])))
parser.add_argument('--reverse', action='store_true',
help="display history list output reversed")
parser.add_argument("-o", "--output", default=None,
help=_("For the store command, file path to store the transaction to"))
parser.add_argument("--ignore-installed", action="store_true",
help=_("For the replay command, don't check for installed packages matching "
"those in transaction"))
parser.add_argument("--ignore-extras", action="store_true",
help=_("For the replay command, don't check for extra packages pulled "
"into the transaction"))
parser.add_argument("--skip-unavailable", action="store_true",
help=_("For the replay command, skip packages that are not available or have "
"missing dependencies"))
parser.add_argument('transactions', nargs='*', metavar="TRANSACTION",
help="Transaction ID (<number>, 'last' or 'last-<number>' "
help="For commands working with history transactions, "
"Transaction ID (<number>, 'last' or 'last-<number>' "
"for one transaction, <transaction-id>..<transaction-id> "
"for range)")
"for a range)")
parser.add_argument('transaction_filename', nargs='?', metavar="TRANSACTION_FILE",
help="For the replay command, path to the stored "
"transaction file to replay")

def configure(self):
if not self.opts.transactions_action:
Expand All @@ -72,14 +91,36 @@ def configure(self):
self.opts.transactions.insert(0, self.opts.transactions_action)
self.opts.transactions_action = self._CMDS[0]


self._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']:
if self.opts.transactions_action == 'replay':
if not self.opts.transactions:
raise dnf.cli.CliError(_('No transaction file name given.'))
if len(self.opts.transactions) > 1:
raise dnf.cli.CliError(_('More than one argument given as transaction file name.'))

# in case of replay, copy over the file name to it's appropriate variable
# (the arg parser can't distinguish here)
self.opts.transaction_filename = os.path.abspath(self.opts.transactions[0])
self.opts.transactions = []

demands.available_repos = True
demands.resolving = True
demands.root_user = True

# Override configuration options that affect how the transaction is resolved
self.base.conf.clean_requirements_on_remove = False
self.base.conf.install_weak_deps = False

dnf.cli.commands._checkGPGKey(self.base, self.cli)
elif self.opts.transactions_action == 'store':
self._require_one_transaction_id = True
if not self.opts.transactions:
raise dnf.cli.CliError(_('No transaction ID or package name given.'))
elif self.opts.transactions_action in ['redo', 'undo', 'rollback']:
self._require_one_transaction_id = True
if not self.opts.transactions:
msg = _('No transaction ID or package name given.')
Expand All @@ -89,7 +130,7 @@ def configure(self):
logger.critical(self._require_one_transaction_id_msg)
raise dnf.cli.CliError(self._require_one_transaction_id_msg)
demands.available_repos = True
commands._checkGPGKey(self.base, self.cli)
dnf.cli.commands._checkGPGKey(self.base, self.cli)
else:
demands.fresh_metadata = False
demands.sack_activation = True
Expand Down Expand Up @@ -223,22 +264,66 @@ def str2transaction_id(s):

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

tids, merged_tids = self._args2transaction_ids()
if vcmd == 'replay':
self.base.read_comps(arch_filter=True)

self.replay = TransactionReplay(
self.base,
self.opts.transaction_filename,
ignore_installed = self.opts.ignore_installed,
ignore_extras = self.opts.ignore_extras,
skip_unavailable = self.opts.skip_unavailable
)
self.replay.run()
else:
tids, merged_tids = self._args2transaction_ids()

if vcmd == 'list' and (tids or not self.opts.transactions):
ret = self.output.historyListCmd(tids, reverse=self.opts.reverse)
elif vcmd == 'info' and (tids or not self.opts.transactions):
ret = self.output.historyInfoCmd(tids, self.opts.transactions, merged_tids)
elif vcmd == 'undo':
ret = self._hcmd_undo(tids)
elif vcmd == 'redo':
ret = self._hcmd_redo(tids)
elif vcmd == 'rollback':
ret = self._hcmd_rollback(tids)
elif vcmd == 'userinstalled':
ret = self._hcmd_userinstalled()
elif vcmd == 'store':
print(
"Warning: The stored transaction format is considered unstable and may "
"change at any time. It will work if the same version of dnf is used to "
"store and replay (or between versions as long as it stays the same).",
file=sys.stderr
)

transactions = self.output.history.old(tids)
if not transactions:
raise dnf.cli.CliError(_('Transaction ID "{id}" not found.').format(id=tids[0]))

data = serialize_transaction(transactions[0])
try:
filename = self.opts.output if self.opts.output is not None else "transaction.json"

ret = None
if vcmd == 'list' and (tids or not self.opts.transactions):
ret = self.output.historyListCmd(tids, reverse=self.opts.reverse)
elif vcmd == 'info' and (tids or not self.opts.transactions):
ret = self.output.historyInfoCmd(tids, self.opts.transactions, merged_tids)
elif vcmd == 'undo':
ret = self._hcmd_undo(tids)
elif vcmd == 'redo':
ret = self._hcmd_redo(tids)
elif vcmd == 'rollback':
ret = self._hcmd_rollback(tids)
elif vcmd == 'userinstalled':
ret = self._hcmd_userinstalled()
# it is absolutely possible for both assumeyes and assumeno to be True, go figure
if (self.base.conf.assumeno or not self.base.conf.assumeyes) and os.path.isfile(filename):
msg = _("{} exists, overwrite?").format(filename)
if self.base.conf.assumeno or not self.base.output.userconfirm(
msg='\n{} [y/N]: '.format(msg), defaultyes_msg='\n{} [Y/n]: '.format(msg)):
print(_("Not overwriting {}, exiting.").format(filename))
return

with open(filename, "w") as f:
json.dump(data, f, indent=4, sort_keys=True)
f.write("\n")

print(_("Transaction saved to {}.").format(filename))

except OSError as e:
raise dnf.cli.CliError(_('Error storing transaction: {}').format(str(e)))

if ret is None:
return
Expand All @@ -247,3 +332,22 @@ def run(self):
self.cli.demands.resolving = True
elif code != 0:
raise dnf.exceptions.Error(strs[0])

def run_resolved(self):
if self.opts.transactions_action != "replay":
return

self.replay.post_transaction()

def run_transaction(self):
if self.opts.transactions_action != "replay":
return

warnings = self.replay.get_warnings()
if warnings:
logger.log(
dnf.logging.WARNING,
_("Warning, the following problems occurred while replaying the transaction:")
)
for w in warnings:
logger.log(dnf.logging.WARNING, " " + w)
Loading

0 comments on commit 8e17bcc

Please sign in to comment.