From 1757cef84b267118e9f66b9072873b9588b1b102 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Tue, 24 Nov 2020 10:57:21 +0100 Subject: [PATCH 01/12] transaction_sr: Enable loading transactions from dict --- dnf/cli/commands/history.py | 2 +- dnf/transaction_sr.py | 42 +++++++++++++++++++++++++------------ 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py index 4b2c90bc55..6eb2d9723b 100644 --- a/dnf/cli/commands/history.py +++ b/dnf/cli/commands/history.py @@ -268,7 +268,7 @@ def run(self): if vcmd == 'replay': self.replay = TransactionReplay( self.base, - self.opts.transaction_filename, + filename=self.opts.transaction_filename, ignore_installed = self.opts.ignore_installed, ignore_extras = self.opts.ignore_extras, skip_unavailable = self.opts.skip_unavailable diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py index 45ca2ef7d3..e6b0666540 100644 --- a/dnf/transaction_sr.py +++ b/dnf/transaction_sr.py @@ -187,21 +187,23 @@ class TransactionReplay(object): def __init__( self, base, - fn, + filename="", + data=None, ignore_extras=False, ignore_installed=False, skip_unavailable=False ): """ :param base: the dnf base - :param fn: the filename to load the transaction from + :param filename: the filename to load the transaction from (conflicts with the 'data' argument) + :param data: the dictionary to load the transaction from (conflicts with the 'filename' argument) :param ignore_extras: whether to ignore extra package pulled into the transaction :param ignore_installed: whether to ignore installed versions of packages :param skip_unavailable: whether to skip transaction packages that aren't available """ self._base = base - self._filename = fn + self._filename = filename self._ignore_installed = ignore_installed self._ignore_extras = ignore_extras self._skip_unavailable = skip_unavailable @@ -213,25 +215,39 @@ def __init__( self._nevra_reason_cache = {} self._warnings = [] + if filename and data: + raise ValueError(_("Conflicting TransactionReplay arguments have been specified: filename, data")) + elif filename: + self._load_from_file(filename) + else: + self._load_from_data(data) + + + def _load_from_file(self, fn): + self._filename = fn with open(fn, "r") as f: try: - self._replay_data = json.load(f) + replay_data = json.load(f) except json.decoder.JSONDecodeError as e: raise TransactionFileError(fn, str(e) + ".") try: - self._verify_toplevel_json(self._replay_data) + self._load_from_data(replay_data) + except TransactionError as e: + raise TransactionFileError(fn, e) - self._rpms = self._replay_data.get("rpms", []) - self._assert_type(self._rpms, list, "rpms", "array") + def _load_from_data(self, data): + self._replay_data = data + self._verify_toplevel_json(self._replay_data) - self._groups = self._replay_data.get("groups", []) - self._assert_type(self._groups, list, "groups", "array") + self._rpms = self._replay_data.get("rpms", []) + self._assert_type(self._rpms, list, "rpms", "array") - self._environments = self._replay_data.get("environments", []) - self._assert_type(self._environments, list, "environments", "array") - except TransactionError as e: - raise TransactionFileError(fn, e) + self._groups = self._replay_data.get("groups", []) + self._assert_type(self._groups, list, "groups", "array") + + self._environments = self._replay_data.get("environments", []) + self._assert_type(self._environments, list, "environments", "array") def _raise_or_warn(self, warn_only, msg): if warn_only: From a8e422b291c09e7d429a8080d84c5629466eb589 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Fri, 20 Nov 2020 19:36:49 +0100 Subject: [PATCH 02/12] transaction_sr: Store exception attributes for future use --- dnf/transaction_sr.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py index e6b0666540..36787de455 100644 --- a/dnf/transaction_sr.py +++ b/dnf/transaction_sr.py @@ -55,6 +55,10 @@ def __init__(self, filename, errors): :param errors: a list of error classes or a string with an error description """ + # store args in case someone wants to read them from a caught exception + self.filename = filename + self.errors = errors + if isinstance(errors, (list, tuple)): if len(errors) > 1: msg = _('Errors in "{filename}":').format(filename=filename) From acdd0c44990c98de35376120c3f2c1c8c6b7c916 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Fri, 20 Nov 2020 19:04:59 +0100 Subject: [PATCH 03/12] transaction_sr: Handle serialize_transaction(None) --- dnf/transaction_sr.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py index 36787de455..41ddee1ff2 100644 --- a/dnf/transaction_sr.py +++ b/dnf/transaction_sr.py @@ -120,6 +120,9 @@ def serialize_transaction(transaction): groups = [] environments = [] + if transaction is None: + return data + for tsi in transaction.packages(): if tsi.is_package(): rpms.append({ From a944d76970d375594c198d7dd6ef4ac59ccab22e Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Mon, 23 Nov 2020 16:23:53 +0100 Subject: [PATCH 04/12] transaction_sr: Skip preferred repo lookup if repoid is empty --- dnf/transaction_sr.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py index 41ddee1ff2..9926bebd0a 100644 --- a/dnf/transaction_sr.py +++ b/dnf/transaction_sr.py @@ -314,9 +314,10 @@ def _replay_pkg_action(self, pkg_data): # This can e.g. make a difference in the system-upgrade plugin, in case # the same NEVRA is in two repos, this makes sure the same repo is used # for both download and upgrade steps of the plugin. - query_repo = query.filter(reponame=repo_id) - if query_repo: - query = query_repo.union(query.installed()) + if repo_id: + query_repo = query.filter(reponame=repo_id) + if query_repo: + query = query_repo.union(query.installed()) if not query: self._raise_or_warn(self._skip_unavailable, _('Cannot find rpm nevra "{nevra}".').format(nevra=nevra)) From 191dc7995d7170addaf3e673c8d4da57478eff18 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Fri, 20 Nov 2020 17:44:28 +0100 Subject: [PATCH 05/12] history: Refactor redo code to use transaction store/replay = changelog = msg: Support comps groups in history redo type: enhancement resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1657123 resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1809565 resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1809639 --- dnf/cli/commands/history.py | 40 +++++++++++++++---------------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py index 6eb2d9723b..a283314dda 100644 --- a/dnf/cli/commands/history.py +++ b/dnf/cli/commands/history.py @@ -120,6 +120,10 @@ def configure(self): 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']: + demands.available_repos = True + demands.resolving = True + demands.root_user = True + self._require_one_transaction_id = True if not self.opts.transactions: msg = _('No transaction ID or package name given.') @@ -157,28 +161,16 @@ 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() + + data = serialize_transaction(old) + self.replay = TransactionReplay( + self.base, + data=data, + ignore_installed=True, + ignore_extras=True, + skip_unavailable=self.opts.skip_unavailable + ) + self.replay.run() def _hcmd_undo(self, extcmds): try: @@ -324,13 +316,13 @@ def run(self): raise dnf.exceptions.Error(strs[0]) def run_resolved(self): - if self.opts.transactions_action != "replay": + if self.opts.transactions_action not in ("replay", "redo"): return self.replay.post_transaction() def run_transaction(self): - if self.opts.transactions_action != "replay": + if self.opts.transactions_action not in ("replay", "redo"): return warnings = self.replay.get_warnings() From 4d3a571af7bde8872604c08901a62aff79b9553e Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Fri, 20 Nov 2020 19:07:50 +0100 Subject: [PATCH 06/12] history: Refactor rollback code to use transaction store/replay = changelog = msg: Support comps groups in history rollback type: enhancement resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1657123 resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1809565 resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1809639 --- dnf/cli/cli.py | 56 ----------------------------- dnf/cli/commands/history.py | 72 ++++++++++++++++++++++++++++++++++--- 2 files changed, 67 insertions(+), 61 deletions(-) diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py index 0bc2c119d0..0511f29267 100644 --- a/dnf/cli/cli.py +++ b/dnf/cli/cli.py @@ -624,62 +624,6 @@ def history_get_transaction(self, extcmds): logger.critical(_('Found more than one transaction ID!')) return old[0] - def history_rollback_transaction(self, extcmd): - """Rollback given transaction.""" - old = self.history_get_transaction((extcmd,)) - if old is None: - return 1, ['Failed history rollback, no transaction'] - last = self.history.last() - if last is None: - return 1, ['Failed history rollback, no last?'] - if old.tid == last.tid: - return 0, ['Rollback to current, nothing to do'] - - mobj = None - for trans in self.history.old(list(range(old.tid + 1, last.tid + 1))): - if trans.altered_lt_rpmdb: - logger.warning(_('Transaction history is incomplete, before %u.'), trans.tid) - elif trans.altered_gt_rpmdb: - logger.warning(_('Transaction history is incomplete, after %u.'), trans.tid) - - if mobj is None: - mobj = dnf.db.history.MergedTransactionWrapper(trans) - else: - mobj.merge(trans) - - tm = dnf.util.normalize_time(old.beg_timestamp) - print("Rollback to transaction %u, from %s" % (old.tid, tm)) - print(self.output.fmtKeyValFill(" Undoing the following transactions: ", - ", ".join((str(x) for x in mobj.tids())))) - self.output.historyInfoCmdPkgsAltered(mobj) # :todo - -# history = dnf.history.open_history(self.history) # :todo -# m = libdnf.transaction.MergedTransaction() - -# return - -# operations = dnf.history.NEVRAOperations() -# for id_ in range(old.tid + 1, last.tid + 1): -# operations += history.transaction_nevra_ops(id_) - - try: - self._history_undo_operations(mobj, old.tid + 1, True, strict=self.conf.strict) - except dnf.exceptions.PackagesNotInstalledError as err: - raise - logger.info(_('No package %s installed.'), - self.output.term.bold(ucd(err.pkg_spec))) - return 1, ['A transaction cannot be undone'] - except dnf.exceptions.PackagesNotAvailableError as err: - raise - logger.info(_('No package %s available.'), - self.output.term.bold(ucd(err.pkg_spec))) - return 1, ['A transaction cannot be undone'] - except dnf.exceptions.MarkingError: - raise - assert False - else: - return 2, ["Rollback to transaction %u" % (old.tid,)] - def history_undo_transaction(self, extcmd): """Undo given transaction.""" old = self.history_get_transaction((extcmd,)) diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py index a283314dda..10def47e8c 100644 --- a/dnf/cli/commands/history.py +++ b/dnf/cli/commands/history.py @@ -20,6 +20,7 @@ from __future__ import unicode_literals import libdnf +import hawkey from dnf.i18n import _, ucd from dnf.cli import commands @@ -33,6 +34,7 @@ import json import logging import os +import sys logger = logging.getLogger('dnf') @@ -179,10 +181,70 @@ def _hcmd_undo(self, extcmds): return 1, [str(err)] def _hcmd_rollback(self, extcmds): + old = self.base.history_get_transaction(extcmds) + if old is None: + return 1, ['Failed history rollback'] + last = self.base.history.last() + + merged_trans = None + if old.tid != last.tid: + # history.old([]) returns all transactions and we don't want that + # so skip merging the transactions when trying to rollback to the last transaction + # which is the current system state and rollback is not applicable + for trans in self.base.history.old(list(range(old.tid + 1, last.tid + 1))): + if trans.altered_lt_rpmdb: + logger.warning(_('Transaction history is incomplete, before %u.'), trans.tid) + elif trans.altered_gt_rpmdb: + logger.warning(_('Transaction history is incomplete, after %u.'), trans.tid) + + if merged_trans is None: + merged_trans = dnf.db.history.MergedTransactionWrapper(trans) + else: + merged_trans.merge(trans) + + return self._revert_transaction(merged_trans) + + def _revert_transaction(self, trans): + action_map = { + "Install": "Removed", + "Removed": "Install", + "Upgrade": "Downgraded", + "Upgraded": "Downgrade", + "Downgrade": "Upgraded", + "Downgraded": "Upgrade", + "Reinstalled": "Reinstall", + "Reinstall": "Reinstalled", + "Obsoleted": "Install", + "Obsolete": "Obsoleted", + } + + data = serialize_transaction(trans) + + # revert actions in the serialized transaction data to perform rollback/undo + for content_type in ("rpms", "groups", "environments"): + for ti in data.get(content_type, []): + ti["action"] = action_map[ti["action"]] + + if ti["action"] == "Install" and ti.get("reason", None) == "clean": + ti["reason"] = "dependency" + + if ti.get("repo_id") == hawkey.SYSTEM_REPO_NAME: + # erase repo_id, because it's not possible to perform forward actions from the @System repo + ti["repo_id"] = None + + self.replay = TransactionReplay( + self.base, + data=data, + ignore_installed=True, + ignore_extras=True, + skip_unavailable=self.opts.skip_unavailable + ) try: - return self.base.history_rollback_transaction(extcmds[0]) - except dnf.exceptions.Error as err: - return 1, [str(err)] + self.replay.run() + except dnf.transaction_sr.TransactionFileError as ex: + for error in ex.errors: + print(str(error), file=sys.stderr) + raise dnf.exceptions.PackageNotFoundError(_('no package matched')) def _hcmd_userinstalled(self): """Execute history userinstalled command.""" @@ -316,13 +378,13 @@ def run(self): raise dnf.exceptions.Error(strs[0]) def run_resolved(self): - if self.opts.transactions_action not in ("replay", "redo"): + if self.opts.transactions_action not in ("replay", "redo", "rollback"): return self.replay.post_transaction() def run_transaction(self): - if self.opts.transactions_action not in ("replay", "redo"): + if self.opts.transactions_action not in ("replay", "redo", "rollback"): return warnings = self.replay.get_warnings() From b33fb12e84e8b3855628439ece66ba2c3f83cd70 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Thu, 3 Dec 2020 15:56:52 +0100 Subject: [PATCH 07/12] history: Refactor undo code to use transaction store/replay = changelog = msg: Support comps groups in history undo type: enhancement resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1657123 resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1809565 resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1809639 --- dnf/cli/cli.py | 28 ---------------------------- dnf/cli/commands/history.py | 12 ++++++------ 2 files changed, 6 insertions(+), 34 deletions(-) diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py index 0511f29267..c48f89fc37 100644 --- a/dnf/cli/cli.py +++ b/dnf/cli/cli.py @@ -624,34 +624,6 @@ def history_get_transaction(self, extcmds): logger.critical(_('Found more than one transaction ID!')) return old[0] - def history_undo_transaction(self, extcmd): - """Undo given transaction.""" - old = self.history_get_transaction((extcmd,)) - if old is None: - return 1, ['Failed history undo'] - - tm = dnf.util.normalize_time(old.beg_timestamp) - msg = _("Undoing transaction {}, from {}").format(old.tid, ucd(tm)) - logger.info(msg) - self.output.historyInfoCmdPkgsAltered(old) # :todo - - - mobj = dnf.db.history.MergedTransactionWrapper(old) - - try: - self._history_undo_operations(mobj, old.tid, strict=self.conf.strict) - except dnf.exceptions.PackagesNotInstalledError as err: - logger.info(_('No package %s installed.'), - self.output.term.bold(ucd(err.pkg_spec))) - return 1, ['An operation cannot be undone'] - except dnf.exceptions.PackagesNotAvailableError as err: - logger.info(_('No package %s available.'), - self.output.term.bold(ucd(err.pkg_spec))) - return 1, ['An operation cannot be undone'] - except dnf.exceptions.MarkingError: - raise - else: - return 2, ["Undoing transaction %u" % (old.tid,)] class Cli(object): def __init__(self, base): diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py index 10def47e8c..f621faf803 100644 --- a/dnf/cli/commands/history.py +++ b/dnf/cli/commands/history.py @@ -175,10 +175,10 @@ def _hcmd_redo(self, extcmds): self.replay.run() def _hcmd_undo(self, extcmds): - try: - return self.base.history_undo_transaction(extcmds[0]) - except dnf.exceptions.Error as err: - return 1, [str(err)] + old = self.base.history_get_transaction(extcmds) + if old is None: + return 1, ['Failed history undo'] + return self._revert_transaction(old) def _hcmd_rollback(self, extcmds): old = self.base.history_get_transaction(extcmds) @@ -378,13 +378,13 @@ def run(self): raise dnf.exceptions.Error(strs[0]) def run_resolved(self): - if self.opts.transactions_action not in ("replay", "redo", "rollback"): + if self.opts.transactions_action not in ("replay", "redo", "rollback", "undo"): return self.replay.post_transaction() def run_transaction(self): - if self.opts.transactions_action not in ("replay", "redo", "rollback"): + if self.opts.transactions_action not in ("replay", "redo", "rollback", "undo"): return warnings = self.replay.get_warnings() From a07f6e4c3d456f035d23dda2b69d3c59bb716d55 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Fri, 20 Nov 2020 19:54:54 +0100 Subject: [PATCH 08/12] Remove Base._history_undo_operations() as it was replaced with transaction_sr code --- dnf/base.py | 59 ----------------------------------------------------- 1 file changed, 59 deletions(-) diff --git a/dnf/base.py b/dnf/base.py index 075e74265a..7bf5452003 100644 --- a/dnf/base.py +++ b/dnf/base.py @@ -2211,65 +2211,6 @@ def provides(self, provides_spec): for prefix in ['/bin/', '/sbin/', '/usr/bin/', '/usr/sbin/']] return self.sack.query().filterm(file__glob=binary_provides), binary_provides - def _history_undo_operations(self, operations, first_trans, rollback=False, strict=True): - """Undo the operations on packages by their NEVRAs. - - :param operations: a NEVRAOperations to be undone - :param first_trans: first transaction id being undone - :param rollback: True if transaction is performing a rollback - :param strict: if True, raise an exception on any errors - """ - - # map actions to their opposites - action_map = { - libdnf.transaction.TransactionItemAction_DOWNGRADE: None, - libdnf.transaction.TransactionItemAction_DOWNGRADED: libdnf.transaction.TransactionItemAction_UPGRADE, - libdnf.transaction.TransactionItemAction_INSTALL: libdnf.transaction.TransactionItemAction_REMOVE, - libdnf.transaction.TransactionItemAction_OBSOLETE: None, - libdnf.transaction.TransactionItemAction_OBSOLETED: libdnf.transaction.TransactionItemAction_INSTALL, - libdnf.transaction.TransactionItemAction_REINSTALL: None, - # reinstalls are skipped as they are considered as no-operation from history perspective - libdnf.transaction.TransactionItemAction_REINSTALLED: None, - libdnf.transaction.TransactionItemAction_REMOVE: libdnf.transaction.TransactionItemAction_INSTALL, - libdnf.transaction.TransactionItemAction_UPGRADE: None, - libdnf.transaction.TransactionItemAction_UPGRADED: libdnf.transaction.TransactionItemAction_DOWNGRADE, - libdnf.transaction.TransactionItemAction_REASON_CHANGE: None, - } - - failed = False - for ti in operations.packages(): - try: - action = action_map[ti.action] - except KeyError: - raise RuntimeError(_("Action not handled: {}".format(action))) - - if action is None: - continue - - if action == libdnf.transaction.TransactionItemAction_REMOVE: - query = self.sack.query().installed().filterm(nevra_strict=str(ti)) - if not query: - logger.error(_('No package %s installed.'), ucd(str(ti))) - failed = True - continue - else: - query = self.sack.query().filterm(nevra_strict=str(ti)) - if not query: - logger.error(_('No package %s available.'), ucd(str(ti))) - failed = True - continue - - if action == libdnf.transaction.TransactionItemAction_REMOVE: - for pkg in query: - self._goal.erase(pkg) - else: - selector = dnf.selector.Selector(self.sack) - selector.set(pkg=query) - self._goal.install(select=selector, optional=(not strict)) - - if strict and failed: - raise dnf.exceptions.PackageNotFoundError(_('no package matched')) - def _merge_update_filters(self, q, pkg_spec=None, warning=True): """ Merge Queries in _update_filters and return intersection with q Query From c8c440017a019f75fcd2563ef10498645558b172 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Wed, 2 Dec 2020 08:57:15 +0100 Subject: [PATCH 09/12] history: Move history methods from BaseCli to HistoryCommand --- dnf/cli/cli.py | 19 ------------- dnf/cli/commands/history.py | 53 +++++++++++++++---------------------- 2 files changed, 22 insertions(+), 50 deletions(-) diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py index c48f89fc37..ecd8468c24 100644 --- a/dnf/cli/cli.py +++ b/dnf/cli/cli.py @@ -605,25 +605,6 @@ def _promptWanted(self): return False return True - def _history_get_transactions(self, extcmds): - if not extcmds: - logger.critical(_('No transaction ID given')) - return None - - old = self.history.old(extcmds) - if not old: - logger.critical(_('Not found given transaction ID')) - return None - return old - - def history_get_transaction(self, extcmds): - old = self._history_get_transactions(extcmds) - if old is None: - return None - if len(old) > 1: - logger.critical(_('Found more than one transaction ID!')) - return old[0] - class Cli(object): def __init__(self, base): diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py index f621faf803..08ef3e8262 100644 --- a/dnf/cli/commands/history.py +++ b/dnf/cli/commands/history.py @@ -34,7 +34,6 @@ import json import logging import os -import sys logger = logging.getLogger('dnf') @@ -160,10 +159,7 @@ def get_error_output(self, error): return dnf.cli.commands.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'] - + old = self._history_get_transaction(extcmds) data = serialize_transaction(old) self.replay = TransactionReplay( self.base, @@ -174,16 +170,27 @@ def _hcmd_redo(self, extcmds): ) self.replay.run() + def _history_get_transactions(self, extcmds): + if not extcmds: + raise dnf.cli.CliError(_('No transaction ID given')) + + old = self.base.history.old(extcmds) + if not old: + raise dnf.cli.CliError(_('Transaction ID "{0}" not found.').format(extcmds[0])) + return old + + def _history_get_transaction(self, extcmds): + old = self._history_get_transactions(extcmds) + if len(old) > 1: + raise dnf.cli.CliError(_('Found more than one transaction ID!')) + return old[0] + def _hcmd_undo(self, extcmds): - old = self.base.history_get_transaction(extcmds) - if old is None: - return 1, ['Failed history undo'] + old = self._history_get_transaction(extcmds) return self._revert_transaction(old) def _hcmd_rollback(self, extcmds): - old = self.base.history_get_transaction(extcmds) - if old is None: - return 1, ['Failed history rollback'] + old = self._history_get_transaction(extcmds) last = self.base.history.last() merged_trans = None @@ -239,12 +246,7 @@ def _revert_transaction(self, trans): ignore_extras=True, skip_unavailable=self.opts.skip_unavailable ) - try: - self.replay.run() - except dnf.transaction_sr.TransactionFileError as ex: - for error in ex.errors: - print(str(error), file=sys.stderr) - raise dnf.exceptions.PackageNotFoundError(_('no package matched')) + self.replay.run() def _hcmd_userinstalled(self): """Execute history userinstalled command.""" @@ -344,11 +346,8 @@ def run(self): elif vcmd == 'userinstalled': ret = self._hcmd_userinstalled() elif vcmd == 'store': - 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]) + tid = self._history_get_transaction(tids) + data = serialize_transaction(tid) try: filename = self.opts.output if self.opts.output is not None else "transaction.json" @@ -369,14 +368,6 @@ def run(self): except OSError as e: raise dnf.cli.CliError(_('Error storing transaction: {}').format(str(e))) - 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]) - def run_resolved(self): if self.opts.transactions_action not in ("replay", "redo", "rollback", "undo"): return @@ -391,7 +382,7 @@ def run_transaction(self): if warnings: logger.log( dnf.logging.WARNING, - _("Warning, the following problems occurred while replaying the transaction:") + _("Warning, the following problems occurred while running a transaction:") ) for w in warnings: logger.log(dnf.logging.WARNING, " " + w) From 316e1e6ed75b1594219816776e929ad37cb0dd58 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Thu, 10 Dec 2020 13:36:52 +0100 Subject: [PATCH 10/12] transaction_sr: Simplify error reporting, unify with history --- dnf/transaction_sr.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py index 9926bebd0a..2122aba4c1 100644 --- a/dnf/transaction_sr.py +++ b/dnf/transaction_sr.py @@ -57,21 +57,19 @@ def __init__(self, filename, errors): # store args in case someone wants to read them from a caught exception self.filename = filename - self.errors = errors - if isinstance(errors, (list, tuple)): - if len(errors) > 1: - msg = _('Errors in "{filename}":').format(filename=filename) - for error in errors: - msg += "\n " + str(error) + self.errors = errors + else: + self.errors = [errors] - super(TransactionFileError, self).__init__(msg) - return + if filename: + msg = _('The following problems occurred while replaying the transaction from file "{filename}":').format(filename=filename) + else: + msg = _('The following problems occurred while running a transaction:') - else: - errors = str(errors[0]) + for error in self.errors: + msg += "\n " + str(error) - msg = _('Error in "{filename}": {error}').format(filename=filename, error=errors) super(TransactionFileError, self).__init__(msg) From 8ff41ce2aed73e7870d8114d1d1b5b6aa163a9e3 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Thu, 17 Dec 2020 16:37:01 +0100 Subject: [PATCH 11/12] transaction_sr: TransactionFileError exception to TransactionReplayError --- dnf/transaction_sr.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py index 2122aba4c1..e4974eb9f4 100644 --- a/dnf/transaction_sr.py +++ b/dnf/transaction_sr.py @@ -48,7 +48,7 @@ def __init__(self, msg): super(TransactionError, self).__init__(msg) -class TransactionFileError(dnf.exceptions.Error): +class TransactionReplayError(dnf.exceptions.Error): def __init__(self, filename, errors): """ :param filename: The name of the transaction file being replayed @@ -70,10 +70,10 @@ def __init__(self, filename, errors): for error in self.errors: msg += "\n " + str(error) - super(TransactionFileError, self).__init__(msg) + super(TransactionReplayError, self).__init__(msg) -class IncompatibleTransactionVersionError(TransactionFileError): +class IncompatibleTransactionVersionError(TransactionReplayError): def __init__(self, filename, msg): super(IncompatibleTransactionVersionError, self).__init__(filename, msg) @@ -84,7 +84,7 @@ def _check_version(version, filename): try: major = int(major) except ValueError as e: - raise TransactionFileError( + raise TransactionReplayError( filename, _('Invalid major version "{major}", number expected.').format(major=major) ) @@ -92,7 +92,7 @@ def _check_version(version, filename): try: int(minor) # minor is unused, just check it's a number except ValueError as e: - raise TransactionFileError( + raise TransactionReplayError( filename, _('Invalid minor version "{minor}", number expected.').format(minor=minor) ) @@ -234,12 +234,12 @@ def _load_from_file(self, fn): try: replay_data = json.load(f) except json.decoder.JSONDecodeError as e: - raise TransactionFileError(fn, str(e) + ".") + raise TransactionReplayError(fn, str(e) + ".") try: self._load_from_data(replay_data) except TransactionError as e: - raise TransactionFileError(fn, e) + raise TransactionReplayError(fn, e) def _load_from_data(self, data): self._replay_data = data @@ -268,7 +268,7 @@ def _verify_toplevel_json(self, replay_data): fn = self._filename if "version" not in replay_data: - raise TransactionFileError(fn, _('Missing key "{key}".'.format(key="version"))) + raise TransactionReplayError(fn, _('Missing key "{key}".'.format(key="version"))) self._assert_type(replay_data["version"], str, "version", "string") @@ -580,7 +580,7 @@ def run(self): errors.append(e) if errors: - raise TransactionFileError(fn, errors) + raise TransactionReplayError(fn, errors) def post_transaction(self): """ @@ -635,4 +635,4 @@ def post_transaction(self): pass if errors: - raise TransactionFileError(self._filename, errors) + raise TransactionReplayError(self._filename, errors) From 49d8837c0d8bb32eda2f8efc81e761fd17cdf7fb Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Thu, 17 Dec 2020 16:55:39 +0100 Subject: [PATCH 12/12] transaction_sr: Don't return if there's a mismatch in actions When _ignore_installed == True, then an exception is raised anyway. When _ignore_installed == False, get the requested package to the system regardless the action. --- dnf/transaction_sr.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py index e4974eb9f4..dae8d3004d 100644 --- a/dnf/transaction_sr.py +++ b/dnf/transaction_sr.py @@ -334,7 +334,6 @@ def _replay_pkg_action(self, pkg_data): if action == "Install" and query_na.installed() and not self._base._get_installonly_query(query_na): self._raise_or_warn(self._ignore_installed, _('Package "{na}" is already installed for action "{action}".').format(na=na, action=action)) - return sltr = dnf.selector.Selector(self._base.sack).set(pkg=query) self._base.goal.install(select=sltr, optional=not self._base.conf.strict)