Skip to content

Commit

Permalink
Post transaction summary is logged for API users (RhBug:1855158)
Browse files Browse the repository at this point in the history
Post transaction summary is always logged into /var/log/dnf.log.
When transaction is called from cli, the summary is also printed to
stdout in columns (as previously).

= changelog =
msg:           Packages installed/removed via DNF API are logged into dnf.log
type:          enhancement
resolves:      https://bugzilla.redhat.com/show_bug.cgi?id=1855158
  • Loading branch information
m-blaha authored and pkratoch committed Dec 16, 2020
1 parent 72aa962 commit 11ce84b
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 146 deletions.
46 changes: 43 additions & 3 deletions dnf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@
import dnf
import libdnf.transaction

from copy import deepcopy
from dnf.comps import CompsQuery
from dnf.i18n import _, P_, ucd
from dnf.util import _parse_specs
from dnf.db.history import SwdbInterface
from dnf.yum import misc
from functools import reduce
try:
from collections.abc import Sequence
except ImportError:
Expand Down Expand Up @@ -611,7 +611,7 @@ def _ts(self):
if self.conf.ignorearch:
self._rpm_probfilter.add(rpm.RPMPROB_FILTER_IGNOREARCH)

probfilter = reduce(operator.or_, self._rpm_probfilter, 0)
probfilter = functools.reduce(operator.or_, self._rpm_probfilter, 0)
self._priv_ts.setProbFilter(probfilter)
return self._priv_ts

Expand Down Expand Up @@ -952,6 +952,15 @@ def do_transaction(self, display=()):
self._plugins.unload_removed_plugins(self.transaction)
self._plugins.run_transaction()

# log post transaction summary
def _pto_callback(action, tsis):
msgs = []
for tsi in tsis:
msgs.append('{}: {}'.format(action, str(tsi)))
return msgs
for msg in dnf.util._post_transaction_output(self, self.transaction, _pto_callback):
logger.debug(msg)

return tid

def _trans_error_summary(self, errstring):
Expand Down Expand Up @@ -1373,7 +1382,7 @@ def _do_package_lists(self, pkgnarrow='all', patterns=None, showdups=None,
if patterns is None or len(patterns) == 0:
return list_fn(None)
yghs = map(list_fn, patterns)
return reduce(lambda a, b: a.merge_lists(b), yghs)
return functools.reduce(lambda a, b: a.merge_lists(b), yghs)

def _list_pattern(self, pkgnarrow, pattern, showdups, ignore_case,
reponame=None):
Expand Down Expand Up @@ -2641,6 +2650,37 @@ def setup_loggers(self):
"""
self._logging._setup_from_dnf_conf(self.conf, file_loggers_only=True)

def _skipped_packages(self, report_problems, transaction):
"""returns set of conflicting packages and set of packages with broken dependency that would
be additionally installed when --best and --allowerasing"""
if self._goal.actions & (hawkey.INSTALL | hawkey.UPGRADE | hawkey.UPGRADE_ALL):
best = True
else:
best = False
ng = deepcopy(self._goal)
params = {"allow_uninstall": self._allow_erasing,
"force_best": best,
"ignore_weak": True}
ret = ng.run(**params)
if not ret and report_problems:
msg = dnf.util._format_resolve_problems(ng.problem_rules())
logger.warning(msg)
problem_conflicts = set(ng.problem_conflicts(available=True))
problem_dependency = set(ng.problem_broken_dependency(available=True)) - problem_conflicts

def _nevra(item):
return hawkey.NEVRA(name=item.name, epoch=item.epoch, version=item.version,
release=item.release, arch=item.arch)

# Sometimes, pkg is not in transaction item, therefore, comparing by nevra
transaction_nevras = [_nevra(tsi) for tsi in transaction]
skipped_conflicts = set(
[pkg for pkg in problem_conflicts if _nevra(pkg) not in transaction_nevras])
skipped_dependency = set(
[pkg for pkg in problem_dependency if _nevra(pkg) not in transaction_nevras])

return skipped_conflicts, skipped_dependency


def _msg_installed(pkg):
name = ucd(pkg)
Expand Down
8 changes: 6 additions & 2 deletions dnf/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,12 @@ def do_transaction(self, display=()):
trans = None

if trans:
msg = self.output.post_transaction_output(trans)
logger.info(msg)
# the post transaction summary is already written to log during
# Base.do_transaction() so here only print the messages to the
# user arranged in columns
print()
print('\n'.join(self.output.post_transaction_output(trans)))
print()
for tsi in trans:
if tsi.state == libdnf.transaction.TransactionItemState_ERROR:
raise dnf.exceptions.Error(_('Transaction failed'))
Expand Down
167 changes: 28 additions & 139 deletions dnf/cli/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@
from __future__ import print_function
from __future__ import unicode_literals

from copy import deepcopy
import fnmatch
import functools
import hawkey
import itertools
import libdnf.transaction
Expand Down Expand Up @@ -53,51 +51,6 @@
logger = logging.getLogger('dnf')


def _make_lists(transaction):
b = dnf.util.Bunch({
'downgraded': [],
'erased': [],
'erased_clean': [],
'erased_dep': [],
'installed': [],
'installed_group': [],
'installed_dep': [],
'installed_weak': [],
'reinstalled': [],
'upgraded': [],
'failed': [],
})

for tsi in transaction:
if tsi.state == libdnf.transaction.TransactionItemState_ERROR:
b.failed.append(tsi)
elif tsi.action == libdnf.transaction.TransactionItemAction_DOWNGRADE:
b.downgraded.append(tsi)
elif tsi.action == libdnf.transaction.TransactionItemAction_INSTALL:
if tsi.reason == libdnf.transaction.TransactionItemReason_GROUP:
b.installed_group.append(tsi)
elif tsi.reason == libdnf.transaction.TransactionItemReason_DEPENDENCY:
b.installed_dep.append(tsi)
elif tsi.reason == libdnf.transaction.TransactionItemReason_WEAK_DEPENDENCY:
b.installed_weak.append(tsi)
else:
# TransactionItemReason_USER
b.installed.append(tsi)
elif tsi.action == libdnf.transaction.TransactionItemAction_REINSTALL:
b.reinstalled.append(tsi)
elif tsi.action == libdnf.transaction.TransactionItemAction_REMOVE:
if tsi.reason == libdnf.transaction.TransactionItemReason_CLEAN:
b.erased_clean.append(tsi)
elif tsi.reason == libdnf.transaction.TransactionItemReason_DEPENDENCY:
b.erased_dep.append(tsi)
else:
b.erased.append(tsi)
elif tsi.action == libdnf.transaction.TransactionItemAction_UPGRADE:
b.upgraded.append(tsi)

return b


def _spread_in_columns(cols_count, label, lst):
left = itertools.chain((label,), itertools.repeat(''))
lst_length = len(lst)
Expand Down Expand Up @@ -1057,37 +1010,6 @@ def list_group_transaction(self, comps, history, diff):
out[0:0] = self._banner(col_data, (_('Group'), _('Packages'), '', ''))
return '\n'.join(out)

def _skipped_packages(self, report_problems, transaction):
"""returns set of conflicting packages and set of packages with broken dependency that would
be additionally installed when --best and --allowerasing"""
if self.base._goal.actions & (hawkey.INSTALL | hawkey.UPGRADE | hawkey.UPGRADE_ALL):
best = True
else:
best = False
ng = deepcopy(self.base._goal)
params = {"allow_uninstall": self.base._allow_erasing,
"force_best": best,
"ignore_weak": True}
ret = ng.run(**params)
if not ret and report_problems:
msg = dnf.util._format_resolve_problems(ng.problem_rules())
logger.warning(msg)
problem_conflicts = set(ng.problem_conflicts(available=True))
problem_dependency = set(ng.problem_broken_dependency(available=True)) - problem_conflicts

def _nevra(item):
return hawkey.NEVRA(name=item.name, epoch=item.epoch, version=item.version,
release=item.release, arch=item.arch)

# Sometimes, pkg is not in transaction item, therefore, comparing by nevra
transaction_nevras = [_nevra(tsi) for tsi in transaction]
skipped_conflicts = set(
[pkg for pkg in problem_conflicts if _nevra(pkg) not in transaction_nevras])
skipped_dependency = set(
[pkg for pkg in problem_dependency if _nevra(pkg) not in transaction_nevras])

return skipped_conflicts, skipped_dependency

def list_transaction(self, transaction, total_width=None):
"""Return a string representation of the transaction in an
easy-to-read format.
Expand All @@ -1102,7 +1024,7 @@ def list_transaction(self, transaction, total_width=None):
# in order to display module changes when RPM transaction is empty
transaction = []

list_bunch = _make_lists(transaction)
list_bunch = dnf.util._make_lists(transaction)
pkglist_lines = []
data = {'n' : {}, 'v' : {}, 'r' : {}}
a_wid = 0 # Arch can't get "that big" ... so always use the max.
Expand Down Expand Up @@ -1271,7 +1193,7 @@ def format_line(group):
# show skipped conflicting packages
if not self.conf.best and self.base._goal.actions & forward_actions:
lines = []
skipped_conflicts, skipped_broken = self._skipped_packages(
skipped_conflicts, skipped_broken = self.base._skipped_packages(
report_problems=True, transaction=transaction)
skipped_broken = dict((str(pkg), pkg) for pkg in skipped_broken)
for pkg in sorted(skipped_conflicts):
Expand Down Expand Up @@ -1436,13 +1358,8 @@ def format_line(group):
max_msg_count, count, msg_pkgs))
return ''.join(out)

def post_transaction_output(self, transaction):
"""Returns a human-readable summary of the results of the
transaction.

:return: a string containing a human-readable summary of the
results of the transaction
"""
def _pto_callback(self, action, tsis):
# Works a bit like calcColumns, but we never overflow a column we just
# have a dynamic number of columns.
def _fits_in_cols(msgs, num):
Expand Down Expand Up @@ -1472,61 +1389,33 @@ def _fits_in_cols(msgs, num):
col_lens[col] *= -1
return col_lens

def _tsi_or_pkg_nevra_cmp(item1, item2):
"""Compares two transaction items or packages by nevra.
Used as a fallback when tsi does not contain package object.
"""
ret = (item1.name > item2.name) - (item1.name < item2.name)
if ret != 0:
return ret
nevra1 = hawkey.NEVRA(name=item1.name, epoch=item1.epoch, version=item1.version,
release=item1.release, arch=item1.arch)
nevra2 = hawkey.NEVRA(name=item2.name, epoch=item2.epoch, version=item2.version,
release=item2.release, arch=item2.arch)
ret = nevra1.evr_cmp(nevra2, self.sack)
if ret != 0:
return ret
return (item1.arch > item2.arch) - (item1.arch < item2.arch)

out = ''
list_bunch = _make_lists(transaction)

skipped_conflicts, skipped_broken = self._skipped_packages(
report_problems=False, transaction=transaction)
skipped = skipped_conflicts.union(skipped_broken)

for (action, tsis) in [(_('Upgraded'), list_bunch.upgraded),
(_('Downgraded'), list_bunch.downgraded),
(_('Installed'), list_bunch.installed +
list_bunch.installed_group +
list_bunch.installed_weak +
list_bunch.installed_dep),
(_('Reinstalled'), list_bunch.reinstalled),
(_('Skipped'), skipped),
(_('Removed'), list_bunch.erased +
list_bunch.erased_dep +
list_bunch.erased_clean),
(_('Failed'), list_bunch.failed)]:
if not tsis:
continue
msgs = []
out += '\n%s:\n' % action
for tsi in sorted(tsis, key=functools.cmp_to_key(_tsi_or_pkg_nevra_cmp)):
msgs.append(str(tsi))
for num in (8, 7, 6, 5, 4, 3, 2):
cols = _fits_in_cols(msgs, num)
if cols:
break
if not cols:
cols = [-(self.term.columns - 2)]
while msgs:
current_msgs = msgs[:len(cols)]
out += ' '
out += self.fmtColumns(zip(current_msgs, cols), end=u'\n')
msgs = msgs[len(cols):]

if not tsis:
return ''
out = []
msgs = []
out.append('{}:'.format(action))
for tsi in tsis:
msgs.append(str(tsi))
for num in (8, 7, 6, 5, 4, 3, 2):
cols = _fits_in_cols(msgs, num)
if cols:
break
if not cols:
cols = [-(self.term.columns - 2)]
while msgs:
current_msgs = msgs[:len(cols)]
out.append(' {}'.format(self.fmtColumns(zip(current_msgs, cols))))
msgs = msgs[len(cols):]
return out


def post_transaction_output(self, transaction):
"""
Return a human-readable summary of the transaction. Packages in sections
are arranged to columns.
"""
return dnf.util._post_transaction_output(self.base, transaction, self._pto_callback)

def setup_progress_callbacks(self):
"""Set up the progress callbacks and various
output bars based on debug level.
Expand Down

0 comments on commit 11ce84b

Please sign in to comment.