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

dnf history operations that work with comps correctly #1689

Merged
merged 12 commits into from
Jan 7, 2021
Merged
59 changes: 0 additions & 59 deletions dnf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
103 changes: 0 additions & 103 deletions dnf/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,109 +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]

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):
Expand Down
145 changes: 95 additions & 50 deletions dnf/cli/commands/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from __future__ import unicode_literals

import libdnf
import hawkey

from dnf.i18n import _, ucd
from dnf.cli import commands
Expand Down Expand Up @@ -120,6 +121,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.')
Expand Down Expand Up @@ -154,43 +159,94 @@ 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']
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)
return 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)

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
)
self.replay.run()

def _hcmd_userinstalled(self):
"""Execute history userinstalled command."""
Expand Down Expand Up @@ -268,7 +324,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
Expand All @@ -290,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"

Expand All @@ -315,29 +368,21 @@ 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 != "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)
Loading