diff --git a/dnf/base.py b/dnf/base.py index ec41ab0..39c21c3 100644 --- a/dnf/base.py +++ b/dnf/base.py @@ -242,6 +242,8 @@ class Base(object): @property def comps(self): # :api + if self._comps is None: + self.read_comps(arch_filter=True) return self._comps @property @@ -1881,7 +1883,6 @@ class Base(object): no_match_module_specs = install_specs.grp_specs if no_match_module_specs: - self.read_comps(arch_filter=True) exclude_specs.grp_specs = self._expand_groups(exclude_specs.grp_specs) self._install_groups(no_match_module_specs, exclude_specs, no_match_group_specs, strict) @@ -2084,7 +2085,6 @@ class Base(object): msg = _('Not a valid form: %s') logger.warning(msg, grp_spec) elif grp_specs: - self.read_comps(arch_filter=True) if self.env_group_remove(grp_specs): done = True @@ -2218,65 +2218,6 @@ class Base(object): 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 diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py index cd720a9..be737ed 100644 --- a/dnf/cli/cli.py +++ b/dnf/cli/cli.py @@ -505,7 +505,7 @@ class BaseCli(dnf.Base): # XXX put this into the ListCommand at some point if len(ypl.obsoletes) > 0 and basecmd == 'list': # if we've looked up obsolete lists and it's a list request - rop = [0, ''] + rop = len(ypl.obsoletes) print(_('Obsoleting Packages')) for obtup in sorted(ypl.obsoletesTuples, key=operator.itemgetter(0)): @@ -517,8 +517,7 @@ class BaseCli(dnf.Base): rrap = self.output.listPkgs(ypl.recent, _('Recently Added Packages'), basecmd, columns=columns) if len(patterns) and \ - rrap[0] and rop[0] and rup[0] and rep[0] and rap[0] and \ - raep[0] and rip[0]: + rrap == 0 and rop == 0 and rup == 0 and rep == 0 and rap == 0 and raep == 0 and rip == 0: raise dnf.exceptions.Error(_('No matching Packages to list')) def returnPkgLists(self, pkgnarrow='all', patterns=None, @@ -608,109 +607,6 @@ class BaseCli(dnf.Base): 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] - - 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,)) - 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/group.py b/dnf/cli/commands/group.py index bd17f80..cf54279 100644 --- a/dnf/cli/commands/group.py +++ b/dnf/cli/commands/group.py @@ -110,9 +110,6 @@ class GroupCommand(commands.Command): return installed, available - def _grp_setup(self): - self.base.read_comps(arch_filter=True) - def _info(self, userlist): for strng in userlist: group_matched = False @@ -370,8 +367,6 @@ class GroupCommand(commands.Command): cmd = self.opts.subcmd extcmds = self.opts.args - self._grp_setup() - if cmd == 'summary': return self._summary(extcmds) if cmd == 'list': diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py index e381f90..293d93f 100644 --- a/dnf/cli/commands/history.py +++ b/dnf/cli/commands/history.py @@ -20,6 +20,7 @@ from __future__ import print_function from __future__ import unicode_literals import libdnf +import hawkey from dnf.i18n import _, ucd from dnf.cli import commands @@ -120,6 +121,10 @@ class HistoryCommand(commands.Command): 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.') @@ -154,48 +159,101 @@ class HistoryCommand(commands.Command): 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'] - 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() + old = self._history_get_transaction(extcmds) + 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 _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): - try: - return self.base.history_undo_transaction(extcmds[0]) - except dnf.exceptions.Error as err: - return 1, [str(err)] + old = self._history_get_transaction(extcmds) + self._revert_transaction(old) def _hcmd_rollback(self, extcmds): - try: - return self.base.history_rollback_transaction(extcmds[0]) - except dnf.exceptions.Error as err: - return 1, [str(err)] + old = self._history_get_transaction(extcmds) + 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) + + 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 + ) + self.replay.run() def _hcmd_userinstalled(self): """Execute history userinstalled command.""" pkgs = tuple(self.base.iter_userinstalled()) - return self.output.listPkgs(pkgs, 'Packages installed by user', 'nevra') + n_listed = self.output.listPkgs(pkgs, 'Packages installed by user', 'nevra') + if n_listed == 0: + raise dnf.cli.CliError(_('No packages to list')) def _args2transaction_ids(self): """Convert commandline arguments to transaction ids""" @@ -263,14 +321,11 @@ class HistoryCommand(commands.Command): def run(self): vcmd = self.opts.transactions_action - ret = None if vcmd == 'replay': - self.base.read_comps(arch_filter=True) - 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 @@ -280,23 +335,20 @@ class HistoryCommand(commands.Command): 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) + 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) + self.output.historyInfoCmd(tids, self.opts.transactions, merged_tids) elif vcmd == 'undo': - ret = self._hcmd_undo(tids) + self._hcmd_undo(tids) elif vcmd == 'redo': - ret = self._hcmd_redo(tids) + self._hcmd_redo(tids) elif vcmd == 'rollback': - ret = self._hcmd_rollback(tids) + self._hcmd_rollback(tids) elif vcmd == 'userinstalled': - ret = self._hcmd_userinstalled() + 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" @@ -317,29 +369,21 @@ class HistoryCommand(commands.Command): 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 != "replay": + 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 != "replay": + if self.opts.transactions_action not in ("replay", "redo", "rollback", "undo"): return warnings = self.replay.get_warnings() 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) diff --git a/dnf/cli/commands/install.py b/dnf/cli/commands/install.py index 38a90b6..b637af0 100644 --- a/dnf/cli/commands/install.py +++ b/dnf/cli/commands/install.py @@ -151,7 +151,6 @@ class InstallCommand(commands.Command): return err_pkgs def _install_groups(self, grp_specs): - self.base.read_comps(arch_filter=True) try: self.base.env_group_install(grp_specs, tuple(self.base.conf.group_package_types), diff --git a/dnf/cli/commands/remove.py b/dnf/cli/commands/remove.py index f50dbd9..e455ba6 100644 --- a/dnf/cli/commands/remove.py +++ b/dnf/cli/commands/remove.py @@ -142,7 +142,6 @@ class RemoveCommand(commands.Command): skipped_grps = self.opts.grp_specs if skipped_grps: - self.base.read_comps(arch_filter=True) for group in skipped_grps: try: if self.base.env_group_remove([group]): diff --git a/dnf/cli/commands/repoquery.py b/dnf/cli/commands/repoquery.py index 099a931..b0d06a9 100644 --- a/dnf/cli/commands/repoquery.py +++ b/dnf/cli/commands/repoquery.py @@ -632,7 +632,6 @@ class RepoQueryCommand(commands.Command): print("\n".join(sorted(pkgs))) def _group_member_report(self, query): - self.base.read_comps(arch_filter=True) package_conf_dict = {} for group in self.base.comps.groups: package_conf_dict[group.id] = set([pkg.name for pkg in group.packages_iter()]) diff --git a/dnf/cli/commands/shell.py b/dnf/cli/commands/shell.py index 431fe50..18c886f 100644 --- a/dnf/cli/commands/shell.py +++ b/dnf/cli/commands/shell.py @@ -239,6 +239,9 @@ exit (or quit) exit the shell""") if fill_sack: self.base.fill_sack() + # reset base._comps, as it has changed due to changing the repos + self.base._comps = None + else: self._help('repo') diff --git a/dnf/cli/commands/upgrade.py b/dnf/cli/commands/upgrade.py index 44789c9..f62cfcc 100644 --- a/dnf/cli/commands/upgrade.py +++ b/dnf/cli/commands/upgrade.py @@ -124,7 +124,6 @@ class UpgradeCommand(commands.Command): def _update_groups(self): if self.skipped_grp_specs: - self.base.read_comps(arch_filter=True) self.base.env_group_upgrade(self.skipped_grp_specs) return True return False diff --git a/dnf/cli/output.py b/dnf/cli/output.py index 6d729b6..6cfc9e2 100644 --- a/dnf/cli/output.py +++ b/dnf/cli/output.py @@ -597,18 +597,10 @@ class Output(object): number '>' - highlighting used when the package has a higher version number - :return: (exit_code, [errors]) - - exit_code is:: - - 0 = we're done, exit - 1 = we've errored, exit with error string - + :return: number of packages listed """ if outputType in ['list', 'info', 'name', 'nevra']: - thingslisted = 0 if len(lst) > 0: - thingslisted = 1 print('%s' % description) info_set = set() if outputType == 'list': @@ -645,9 +637,7 @@ class Output(object): if info_set: print("\n".join(sorted(info_set))) - if thingslisted == 0: - return 1, [_('No packages to list')] - return 0, [] + return len(lst) def userconfirm(self, msg=None, defaultyes_msg=None): """Get a yes or no from the user, and default to No diff --git a/dnf/db/history.py b/dnf/db/history.py index 4d355f9..994cdb0 100644 --- a/dnf/db/history.py +++ b/dnf/db/history.py @@ -381,6 +381,9 @@ class SwdbInterface(object): prev_trans.altered_gt_rpmdb = True return result[::-1] + def get_current(self): + return TransactionWrapper(self.swdb.getCurrent()) + def set_reason(self, pkg, reason): """Set reason for package""" rpm_item = self.rpm._pkg_to_swdb_rpm_item(pkg) diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py index 9b9b074..dae8d30 100644 --- a/dnf/transaction_sr.py +++ b/dnf/transaction_sr.py @@ -48,30 +48,32 @@ class TransactionError(dnf.exceptions.Error): 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 :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 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) + super(TransactionReplayError, self).__init__(msg) -class IncompatibleTransactionVersionError(TransactionFileError): +class IncompatibleTransactionVersionError(TransactionReplayError): def __init__(self, filename, msg): super(IncompatibleTransactionVersionError, self).__init__(filename, msg) @@ -82,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) ) @@ -90,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) ) @@ -116,6 +118,9 @@ def serialize_transaction(transaction): groups = [] environments = [] + if transaction is None: + return data + for tsi in transaction.packages(): if tsi.is_package(): rpms.append({ @@ -187,21 +192,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 +220,39 @@ class TransactionReplay(object): 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) + ".") + raise TransactionReplayError(fn, str(e) + ".") try: - self._verify_toplevel_json(self._replay_data) + self._load_from_data(replay_data) + except TransactionError as e: + raise TransactionReplayError(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: @@ -247,7 +268,7 @@ class TransactionReplay(object): 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") @@ -257,6 +278,7 @@ class TransactionReplay(object): try: action = pkg_data["action"] nevra = pkg_data["nevra"] + repo_id = pkg_data["repo_id"] reason = libdnf.transaction.StringToTransactionItemReason(pkg_data["reason"]) except KeyError as e: raise TransactionError( @@ -282,6 +304,19 @@ class TransactionReplay(object): epoch = parsed_nevra.epoch if parsed_nevra.epoch is not None else 0 query = query_na.filter(epoch=epoch, version=parsed_nevra.version, release=parsed_nevra.release) + # In case the package is found in the same repo as in the original + # transaction, limit the query to that plus installed packages. IOW + # remove packages with the same NEVRA in case they are found in + # multiple repos and the repo the package came from originally is one + # of them. + # 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. + 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)) return @@ -299,7 +334,6 @@ class TransactionReplay(object): 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) @@ -545,7 +579,7 @@ class TransactionReplay(object): errors.append(e) if errors: - raise TransactionFileError(fn, errors) + raise TransactionReplayError(fn, errors) def post_transaction(self): """ @@ -600,4 +634,4 @@ class TransactionReplay(object): pass if errors: - raise TransactionFileError(self._filename, errors) + raise TransactionReplayError(self._filename, errors) diff --git a/tests/api/test_dnf_base.py b/tests/api/test_dnf_base.py index ca71b75..656bd22 100644 --- a/tests/api/test_dnf_base.py +++ b/tests/api/test_dnf_base.py @@ -34,9 +34,7 @@ class DnfBaseApiTest(TestCase): def test_comps(self): # Base.comps self.assertHasAttr(self.base, "comps") - - # blank initially - self.assertEqual(self.base.comps, None) + self.assertHasType(self.base.comps, dnf.comps.Comps) self.base.read_comps() self.assertHasType(self.base.comps, dnf.comps.Comps)