dnf/dnf5: address issues discovered in 2.16 yum tests (#83659)

Fixes #85554
pull/85613/head
Martin Krizek 4 months ago committed by GitHub
parent dc5209a3fd
commit c0256d6edf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -208,6 +208,8 @@ options:
packages to install (because dependencies between the downgraded packages to install (because dependencies between the downgraded
package and others can cause changes to the packages which were package and others can cause changes to the packages which were
in the earlier transaction). in the earlier transaction).
- Since this feature is not provided by C(dnf) itself but by M(ansible.builtin.dnf) module,
using this in combination with wildcard characters in O(name) may result in an unexpected results.
type: bool type: bool
default: "no" default: "no"
version_added: "2.7" version_added: "2.7"
@ -701,72 +703,56 @@ class DnfModule(YumDnf):
self.module.exit_json(msg="", results=results) self.module.exit_json(msg="", results=results)
def _is_installed(self, pkg): def _is_installed(self, pkg):
installed_query = dnf.subject.Subject(pkg).get_best_query(sack=self.base.sack).installed() return bool(dnf.subject.Subject(pkg).get_best_query(sack=self.base.sack).installed())
if dnf.util.is_glob_pattern(pkg):
available_query = dnf.subject.Subject(pkg).get_best_query(sack=self.base.sack).available()
return not (
{p.name for p in available_query} - {p.name for p in installed_query}
)
else:
return bool(installed_query)
def _is_newer_version_installed(self, pkg_spec): def _is_newer_version_installed(self, pkg_spec):
# expects a versioned package spec
try: try:
if isinstance(pkg_spec, dnf.package.Package): if isinstance(pkg_spec, dnf.package.Package):
installed = sorted(self.base.sack.query().installed().filter(name=pkg_spec.name, arch=pkg_spec.arch))[-1] installed = sorted(self.base.sack.query().installed().filter(name=pkg_spec.name, arch=pkg_spec.arch))[-1]
return installed.evr_gt(pkg_spec) return installed.evr_gt(pkg_spec)
else: else:
available = dnf.subject.Subject(pkg_spec).get_best_query(sack=self.base.sack).available() solution = dnf.subject.Subject(pkg_spec).get_best_solution(self.base.sack)
installed = self.base.sack.query().installed().filter(name=available[0].name) q = solution["query"]
for arch in sorted(set(p.arch for p in installed)): # select only from already-installed arches for this case if not q or not solution['nevra'] or solution['nevra'].has_just_name():
installed_pkg = sorted(installed.filter(arch=arch))[-1] return False
try: installed = self.base.sack.query().installed().filter(name=solution['nevra'].name)
available_pkg = sorted(available.filter(arch=arch))[-1] if not installed:
except IndexError: return False
continue # nothing currently available for this arch; keep going return installed[0].evr_gt(q[0])
if installed_pkg.evr_gt(available_pkg):
return True
return False
except IndexError: except IndexError:
return False return False
def _mark_package_install(self, pkg_spec, upgrade=False): def _mark_package_install(self, pkg_spec, upgrade=False):
"""Mark the package for install.""" """Mark the package for install."""
is_newer_version_installed = self._is_newer_version_installed(pkg_spec)
is_installed = self._is_installed(pkg_spec)
msg = '' msg = ''
try: try:
if is_newer_version_installed: if dnf.util.is_glob_pattern(pkg_spec):
# Special case for package specs that contain glob characters.
# For these we skip `is_installed` and `is_newer_version_installed` tests that allow for the
# allow_downgrade feature and pass the package specs to dnf.
# Since allow_downgrade is not available in dnf and while it is relatively easy to implement it for
# package specs that evaluate to a single package, trying to mimic what would the dnf machinery do
# for glob package specs and then filtering those for allow_downgrade appears to always
# result in naive/inferior solution.
# NOTE this has historically never worked even before https://github.com/ansible/ansible/pull/82725
# where our (buggy) custom code ignored wildcards for the installed checks.
# TODO reasearch how feasible it is to implement the above
if upgrade:
# for upgrade we pass the spec to both upgrade and install, to satisfy both available and installed
# packages evaluated from the glob spec
try:
self.base.upgrade(pkg_spec)
except dnf.exceptions.PackagesNotInstalledError:
pass
self.base.install(pkg_spec, strict=self.base.conf.strict)
elif self._is_newer_version_installed(pkg_spec):
if self.allow_downgrade: if self.allow_downgrade:
# dnf only does allow_downgrade, we have to handle this ourselves self.base.install(pkg_spec, strict=self.base.conf.strict)
# because it allows a possibility for non-idempotent transactions elif self._is_installed(pkg_spec):
# on a system's package set (pending the yum repo has many old if upgrade:
# NVRs indexed)
if upgrade:
if is_installed: # Case 1
# TODO: Is this case reachable?
#
# _is_installed() demands a name (*not* NVR) or else is always False
# (wildcards are treated literally).
#
# Meanwhile, _is_newer_version_installed() demands something versioned
# or else is always false.
#
# I fail to see how they can both be true at the same time for any
# given pkg_spec. -re
self.base.upgrade(pkg_spec)
else: # Case 2
self.base.install(pkg_spec, strict=self.base.conf.strict)
else: # Case 3
self.base.install(pkg_spec, strict=self.base.conf.strict)
else: # Case 4, Nothing to do, report back
pass
elif is_installed: # A potentially older (or same) version is installed
if upgrade: # Case 5
self.base.upgrade(pkg_spec) self.base.upgrade(pkg_spec)
else: # Case 6, Nothing to do, report back else:
pass
else: # Case 7, The package is not installed, simply install it
self.base.install(pkg_spec, strict=self.base.conf.strict) self.base.install(pkg_spec, strict=self.base.conf.strict)
except dnf.exceptions.MarkingError as e: except dnf.exceptions.MarkingError as e:
msg = "No package {0} available.".format(pkg_spec) msg = "No package {0} available.".format(pkg_spec)

@ -178,6 +178,8 @@ options:
packages to install (because dependencies between the downgraded packages to install (because dependencies between the downgraded
package and others can cause changes to the packages which were package and others can cause changes to the packages which were
in the earlier transaction). in the earlier transaction).
- Since this feature is not provided by C(dnf5) itself but by M(ansible.builtin.dnf5) module,
using this in combination with wildcard characters in O(name) may result in an unexpected results.
type: bool type: bool
default: "no" default: "no"
download_only: download_only:
@ -362,7 +364,7 @@ libdnf5 = None
LIBDNF5_ERRORS = RuntimeError LIBDNF5_ERRORS = RuntimeError
def is_installed(base, spec): def get_resolve_spec_settings():
settings = libdnf5.base.ResolveSpecSettings() settings = libdnf5.base.ResolveSpecSettings()
try: try:
settings.set_group_with_name(True) settings.set_group_with_name(True)
@ -388,47 +390,34 @@ def is_installed(base, spec):
settings.group_with_name = True settings.group_with_name = True
settings.with_binaries = False settings.with_binaries = False
settings.with_provides = False settings.with_provides = False
return settings
def is_installed(base, spec):
settings = get_resolve_spec_settings()
installed_query = libdnf5.rpm.PackageQuery(base) installed_query = libdnf5.rpm.PackageQuery(base)
installed_query.filter_installed() installed_query.filter_installed()
match, nevra = installed_query.resolve_pkg_spec(spec, settings, True) match, nevra = installed_query.resolve_pkg_spec(spec, settings, True)
return match
# FIXME use `is_glob_pattern` function when available:
# https://github.com/rpm-software-management/dnf5/issues/1563
glob_patterns = set("*[?")
if any(set(char) & glob_patterns for char in spec):
available_query = libdnf5.rpm.PackageQuery(base)
available_query.filter_available()
available_query.resolve_pkg_spec(spec, settings, True)
return not (
{p.get_name() for p in available_query} - {p.get_name() for p in installed_query}
)
else:
return match
def is_newer_version_installed(base, spec): def is_newer_version_installed(base, spec):
# FIXME investigate whether this function can be replaced by dnf5's allow_downgrade option # expects a versioned package spec
if "/" in spec: if "/" in spec:
spec = spec.split("/")[-1] spec = spec.split("/")[-1]
if spec.endswith(".rpm"): if spec.endswith(".rpm"):
spec = spec[:-4] spec = spec[:-4]
try: settings = get_resolve_spec_settings()
spec_nevra = next(iter(libdnf5.rpm.Nevra.parse(spec))) match, spec_nevra = libdnf5.rpm.PackageQuery(base).resolve_pkg_spec(spec, settings, True)
except LIBDNF5_ERRORS: if not match or spec_nevra.has_just_name():
return False
except StopIteration:
return False
spec_version = spec_nevra.get_version()
if not spec_version:
return False return False
spec_name = spec_nevra.get_name()
installed = libdnf5.rpm.PackageQuery(base) installed = libdnf5.rpm.PackageQuery(base)
installed.filter_installed() installed.filter_installed()
installed.filter_name([spec_nevra.get_name()]) installed.filter_name([spec_name])
installed.filter_latest_evr() installed.filter_latest_evr()
try: try:
installed_package = list(installed)[-1] installed_package = list(installed)[-1]
@ -436,8 +425,8 @@ def is_newer_version_installed(base, spec):
return False return False
target = libdnf5.rpm.PackageQuery(base) target = libdnf5.rpm.PackageQuery(base)
target.filter_name([spec_nevra.get_name()]) target.filter_name([spec_name])
target.filter_version([spec_version]) target.filter_version([spec_nevra.get_version()])
spec_release = spec_nevra.get_release() spec_release = spec_nevra.get_release()
if spec_release: if spec_release:
target.filter_release([spec_release]) target.filter_release([spec_release])
@ -719,8 +708,26 @@ class Dnf5Module(YumDnf):
goal.add_rpm_upgrade(settings) goal.add_rpm_upgrade(settings)
elif self.state in {"installed", "present", "latest"}: elif self.state in {"installed", "present", "latest"}:
upgrade = self.state == "latest" upgrade = self.state == "latest"
# FIXME use `is_glob_pattern` function when available:
# https://github.com/rpm-software-management/dnf5/issues/1563
glob_patterns = set("*[?")
for spec in self.names: for spec in self.names:
if is_newer_version_installed(base, spec): if any(set(char) & glob_patterns for char in spec):
# Special case for package specs that contain glob characters.
# For these we skip `is_installed` and `is_newer_version_installed` tests that allow for the
# allow_downgrade feature and pass the package specs to dnf.
# Since allow_downgrade is not available in dnf and while it is relatively easy to implement it for
# package specs that evaluate to a single package, trying to mimic what would the dnf machinery do
# for glob package specs and then filtering those for allow_downgrade appears to always
# result in naive/inferior solution.
# TODO reasearch how feasible it is to implement the above
if upgrade:
# for upgrade we pass the spec to both upgrade and install, to satisfy both available and installed
# packages evaluated from the glob spec
goal.add_upgrade(spec, settings)
if not self.update_only:
goal.add_install(spec, settings)
elif is_newer_version_installed(base, spec):
if self.allow_downgrade: if self.allow_downgrade:
goal.add_install(spec, settings) goal.add_install(spec, settings)
elif is_installed(base, spec): elif is_installed(base, spec):

@ -630,6 +630,87 @@
- provides-package - provides-package
- provided-package - provided-package
# https://github.com/ansible/ansible/issues/45250
- block:
- name: Install dinginessentail-1.0, dinginessentail-olive-1.0, landsidescalping-1.0
dnf:
name: "dinginessentail-1.0,dinginessentail-olive-1.0,landsidescalping-1.0"
state: present
- name: Upgrade dinginessentail*
dnf:
name: dinginessentail*
state: latest
register: dnf_result
- name: Check dinginessentail with rpm
shell: rpm -q dinginessentail
register: rpm_result
- name: Verify update of dinginessentail
assert:
that:
- "rpm_result.stdout.startswith('dinginessentail-1.1-1')"
- name: Check dinginessentail-olive with rpm
shell: rpm -q dinginessentail-olive
register: rpm_result
- name: Verify update of dinginessentail-olive
assert:
that:
- "rpm_result.stdout.startswith('dinginessentail-olive-1.1-1')"
- name: Check landsidescalping with rpm
shell: rpm -q landsidescalping
register: rpm_result
- name: Verify landsidescalping did NOT get updated
assert:
that:
- "rpm_result.stdout.startswith('landsidescalping-1.0-1')"
- name: Verify yum module outputs
assert:
that:
- "dnf_result is changed"
- "'msg' in dnf_result"
- "'rc' in dnf_result"
- "'results' in dnf_result"
always:
- name: Clean up
dnf:
name: dinginessentail,dinginessentail-olive,landsidescalping
state: absent
- name: test allow_downgrade
block:
- dnf:
name: dinginessentail-1.1
state: present
- dnf:
name: dinginessentail-1.0
state: present
allow_downgrade: true
- dnf:
name: dinginessentail-1.1
state: present
- dnf:
name: dinginessentail-1.0
state: present
allow_downgrade: false
register: r
- assert:
that:
- r is not changed
always:
- dnf:
name: dinginessentail
state: absent
- name: Test failures occured during loading repositories are properly handled - name: Test failures occured during loading repositories are properly handled
vars: vars:
repo_name: test-non-existing-gpgkey-file repo_name: test-non-existing-gpgkey-file
@ -657,3 +738,15 @@
- file: - file:
name: /etc/yum.repos.d/{{ repo_name }}.repo name: /etc/yum.repos.d/{{ repo_name }}.repo
state: absent state: absent
- name: Attempt to install a package with invalid name
dnf:
name: invalid[package_name]
register: r
ignore_errors: true
- assert:
that:
- r is failed
- r.msg is contains("Failed to install some of the specified packages")

Loading…
Cancel
Save