summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Krizek <martin.krizek@gmail.com>2023-03-31 15:51:48 +0200
committerGitHub <noreply@github.com>2023-03-31 15:51:48 +0200
commita81b787a0510098686cf0263dd8b90bd0189e951 (patch)
tree230b7c5990c5074677f94137851a8520cfc744bb
parent666188892ed0833e87803a3e80c58923e4cd6bca (diff)
downloadansible-a81b787a0510098686cf0263dd8b90bd0189e951.tar.gz
Add new dnf5 module (#80272)
-rw-r--r--changelogs/fragments/dnf5-module.yml4
-rw-r--r--lib/ansible/module_utils/facts/system/pkg_mgr.py5
-rw-r--r--lib/ansible/modules/dnf.py8
-rw-r--r--lib/ansible/modules/dnf5.py720
-rw-r--r--lib/ansible/modules/yum.py7
-rw-r--r--lib/ansible/plugins/action/dnf.py83
-rw-r--r--lib/ansible/plugins/action/yum.py8
-rw-r--r--test/integration/targets/dnf/tasks/dnf.yml34
-rw-r--r--test/integration/targets/dnf/tasks/main.yml7
-rw-r--r--test/integration/targets/dnf/tasks/skip_broken_and_nobest.yml3
-rw-r--r--test/integration/targets/dnf/tasks/test_sos_removal.yml4
-rw-r--r--test/integration/targets/dnf5/aliases6
-rw-r--r--test/integration/targets/dnf5/playbook.yml19
-rwxr-xr-xtest/integration/targets/dnf5/runme.sh5
-rw-r--r--test/sanity/ignore.txt1
15 files changed, 892 insertions, 22 deletions
diff --git a/changelogs/fragments/dnf5-module.yml b/changelogs/fragments/dnf5-module.yml
new file mode 100644
index 0000000000..cc5a706bca
--- /dev/null
+++ b/changelogs/fragments/dnf5-module.yml
@@ -0,0 +1,4 @@
+minor_changes:
+ - dnf5 - Add new module for managing packages and other artifacts via the next version of DNF (https://github.com/ansible/ansible/issues/78898)
+known_issues:
+ - "dnf5 - The DNF5 package manager currently does not provide all functionality to ensure feature parity between the existing ``dnf`` and the new ``dnf5`` module. As a result the following ``dnf5`` options are effectively a no-op: ``cacheonly``, ``enable_plugin``, ``disable_plugin`` and ``lock_timeout``."
diff --git a/lib/ansible/module_utils/facts/system/pkg_mgr.py b/lib/ansible/module_utils/facts/system/pkg_mgr.py
index 704ea2014d..bca283a8aa 100644
--- a/lib/ansible/module_utils/facts/system/pkg_mgr.py
+++ b/lib/ansible/module_utils/facts/system/pkg_mgr.py
@@ -77,7 +77,10 @@ class PkgMgrFactCollector(BaseFactCollector):
if int(collected_facts['ansible_distribution_major_version']) < 23:
if self._pkg_mgr_exists('yum'):
pkg_mgr_name = 'yum'
-
+ elif int(collected_facts['ansible_distribution_major_version']) >= 39:
+ # /usr/bin/dnf is planned to be a symlink to /usr/bin/dnf5
+ if self._pkg_mgr_exists('dnf'):
+ pkg_mgr_name = 'dnf5'
else:
if self._pkg_mgr_exists('dnf'):
pkg_mgr_name = 'dnf'
diff --git a/lib/ansible/modules/dnf.py b/lib/ansible/modules/dnf.py
index 4357823981..771d85cde4 100644
--- a/lib/ansible/modules/dnf.py
+++ b/lib/ansible/modules/dnf.py
@@ -18,6 +18,13 @@ short_description: Manages packages with the I(dnf) package manager
description:
- Installs, upgrade, removes, and lists packages and groups with the I(dnf) package manager.
options:
+ use_backend:
+ description:
+ - By default, this module will select the backend based on the C(ansible_pkg_mgr) fact.
+ default: "auto"
+ choices: [ auto, dnf4, dnf5 ]
+ type: str
+ version_added: 2.15
name:
description:
- "A package name or package specifier with version, like C(name-1.0).
@@ -1452,6 +1459,7 @@ def main():
# backported to yum because yum is now in "maintenance mode" upstream
yumdnf_argument_spec['argument_spec']['allowerasing'] = dict(default=False, type='bool')
yumdnf_argument_spec['argument_spec']['nobest'] = dict(default=False, type='bool')
+ yumdnf_argument_spec['argument_spec']['use_backend'] = dict(default='auto', choices=['auto', 'dnf4', 'dnf5'])
module = AnsibleModule(
**yumdnf_argument_spec
diff --git a/lib/ansible/modules/dnf5.py b/lib/ansible/modules/dnf5.py
new file mode 100644
index 0000000000..5e4b3d64ab
--- /dev/null
+++ b/lib/ansible/modules/dnf5.py
@@ -0,0 +1,720 @@
+# -*- coding: utf-8 -*-
+# Copyright 2023 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = """
+module: dnf5
+author: Ansible Core Team
+description:
+ - Installs, upgrade, removes, and lists packages and groups with the I(dnf5) package manager.
+ - "WARNING: The I(dnf5) package manager is still under development and not all features that the existing I(dnf) module
+ provides are implemented in I(dnf5), please consult specific options for more information."
+short_description: Manages packages with the I(dnf5) package manager
+options:
+ name:
+ description:
+ - "A package name or package specifier with version, like C(name-1.0).
+ When using state=latest, this can be '*' which means run: dnf -y update.
+ You can also pass a url or a local path to a rpm file.
+ To operate on several packages this can accept a comma separated string of packages or a list of packages."
+ - Comparison operators for package version are valid here C(>), C(<), C(>=), C(<=). Example - C(name >= 1.0).
+ Spaces around the operator are required.
+ - You can also pass an absolute path for a binary which is provided by the package to install.
+ See examples for more information.
+ aliases:
+ - pkg
+ type: list
+ elements: str
+ default: []
+ list:
+ description:
+ - Various (non-idempotent) commands for usage with C(/usr/bin/ansible) and I(not) playbooks.
+ Use M(ansible.builtin.package_facts) instead of the C(list) argument as a best practice.
+ type: str
+ state:
+ description:
+ - Whether to install (C(present), C(latest)), or remove (C(absent)) a package.
+ - Default is C(None), however in effect the default action is C(present) unless the C(autoremove) option is
+ enabled for this module, then C(absent) is inferred.
+ choices: ['absent', 'present', 'installed', 'removed', 'latest']
+ type: str
+ enablerepo:
+ description:
+ - I(Repoid) of repositories to enable for the install/update operation.
+ These repos will not persist beyond the transaction.
+ When specifying multiple repos, separate them with a ",".
+ type: list
+ elements: str
+ default: []
+ disablerepo:
+ description:
+ - I(Repoid) of repositories to disable for the install/update operation.
+ These repos will not persist beyond the transaction.
+ When specifying multiple repos, separate them with a ",".
+ type: list
+ elements: str
+ default: []
+ conf_file:
+ description:
+ - The remote dnf configuration file to use for the transaction.
+ type: str
+ disable_gpg_check:
+ description:
+ - Whether to disable the GPG checking of signatures of packages being
+ installed. Has an effect only if state is I(present) or I(latest).
+ - This setting affects packages installed from a repository as well as
+ "local" packages installed from the filesystem or a URL.
+ type: bool
+ default: 'no'
+ installroot:
+ description:
+ - Specifies an alternative installroot, relative to which all packages
+ will be installed.
+ default: "/"
+ type: str
+ releasever:
+ description:
+ - Specifies an alternative release from which all packages will be
+ installed.
+ type: str
+ autoremove:
+ description:
+ - If C(true), removes all "leaf" packages from the system that were originally
+ installed as dependencies of user-installed packages but which are no longer
+ required by any such package. Should be used alone or when state is I(absent)
+ type: bool
+ default: "no"
+ exclude:
+ description:
+ - Package name(s) to exclude when state=present, or latest. This can be a
+ list or a comma separated string.
+ type: list
+ elements: str
+ default: []
+ skip_broken:
+ description:
+ - Skip all unavailable packages or packages with broken dependencies
+ without raising an error. Equivalent to passing the --skip-broken option.
+ type: bool
+ default: "no"
+ update_cache:
+ description:
+ - Force dnf to check if cache is out of date and redownload if needed.
+ Has an effect only if state is I(present) or I(latest).
+ type: bool
+ default: "no"
+ aliases: [ expire-cache ]
+ update_only:
+ description:
+ - When using latest, only update installed packages. Do not install packages.
+ - Has an effect only if state is I(latest)
+ default: "no"
+ type: bool
+ security:
+ description:
+ - If set to C(true), and C(state=latest) then only installs updates that have been marked security related.
+ - Note that, similar to C(dnf upgrade-minimal), this filter applies to dependencies as well.
+ type: bool
+ default: "no"
+ bugfix:
+ description:
+ - If set to C(true), and C(state=latest) then only installs updates that have been marked bugfix related.
+ - Note that, similar to C(dnf upgrade-minimal), this filter applies to dependencies as well.
+ default: "no"
+ type: bool
+ enable_plugin:
+ description:
+ - This is currently a no-op as dnf5 itself does not implement this feature.
+ - I(Plugin) name to enable for the install/update operation.
+ The enabled plugin will not persist beyond the transaction.
+ type: list
+ elements: str
+ default: []
+ disable_plugin:
+ description:
+ - This is currently a no-op as dnf5 itself does not implement this feature.
+ - I(Plugin) name to disable for the install/update operation.
+ The disabled plugins will not persist beyond the transaction.
+ type: list
+ default: []
+ elements: str
+ disable_excludes:
+ description:
+ - Disable the excludes defined in DNF config files.
+ - If set to C(all), disables all excludes.
+ - If set to C(main), disable excludes defined in [main] in dnf.conf.
+ - If set to C(repoid), disable excludes defined for given repo id.
+ type: str
+ validate_certs:
+ description:
+ - This is effectively a no-op in the dnf5 module as dnf5 itself handles downloading a https url as the source of the rpm,
+ but is an accepted parameter for feature parity/compatibility with the I(yum) module.
+ type: bool
+ default: "yes"
+ sslverify:
+ description:
+ - Disables SSL validation of the repository server for this transaction.
+ - This should be set to C(false) if one of the configured repositories is using an untrusted or self-signed certificate.
+ type: bool
+ default: "yes"
+ allow_downgrade:
+ description:
+ - Specify if the named package and version is allowed to downgrade
+ a maybe already installed higher version of that package.
+ Note that setting allow_downgrade=True can make this module
+ behave in a non-idempotent way. The task could end up with a set
+ of packages that does not match the complete list of specified
+ packages to install (because dependencies between the downgraded
+ package and others can cause changes to the packages which were
+ in the earlier transaction).
+ type: bool
+ default: "no"
+ install_repoquery:
+ description:
+ - This is effectively a no-op in DNF as it is not needed with DNF, but is an accepted parameter for feature
+ parity/compatibility with the I(yum) module.
+ type: bool
+ default: "yes"
+ download_only:
+ description:
+ - Only download the packages, do not install them.
+ default: "no"
+ type: bool
+ lock_timeout:
+ description:
+ - This is currently a no-op as dnf5 does not provide an option to configure it.
+ - Amount of time to wait for the dnf lockfile to be freed.
+ required: false
+ default: 30
+ type: int
+ install_weak_deps:
+ description:
+ - Will also install all packages linked by a weak dependency relation.
+ type: bool
+ default: "yes"
+ download_dir:
+ description:
+ - Specifies an alternate directory to store packages.
+ - Has an effect only if I(download_only) is specified.
+ type: str
+ allowerasing:
+ description:
+ - If C(true) it allows erasing of installed packages to resolve dependencies.
+ required: false
+ type: bool
+ default: "no"
+ nobest:
+ description:
+ - Set best option to False, so that transactions are not limited to best candidates only.
+ required: false
+ type: bool
+ default: "no"
+ cacheonly:
+ description:
+ - This is currently no-op as dnf5 does not implement the feature.
+ - Tells dnf to run entirely from system cache; does not download or update metadata.
+ type: bool
+ default: "no"
+extends_documentation_fragment:
+- action_common_attributes
+- action_common_attributes.flow
+attributes:
+ action:
+ details: In the case of dnf, it has 2 action plugins that use it under the hood, M(ansible.builtin.yum) and M(ansible.builtin.package).
+ support: partial
+ async:
+ support: none
+ bypass_host_loop:
+ support: none
+ check_mode:
+ support: full
+ diff_mode:
+ support: full
+ platform:
+ platforms: rhel
+requirements:
+ - "python3"
+ - "python3-libdnf5"
+version_added: 2.15
+"""
+
+EXAMPLES = """
+- name: Install the latest version of Apache
+ ansible.builtin.dnf5:
+ name: httpd
+ state: latest
+
+- name: Install Apache >= 2.4
+ ansible.builtin.dnf5:
+ name: httpd >= 2.4
+ state: present
+
+- name: Install the latest version of Apache and MariaDB
+ ansible.builtin.dnf5:
+ name:
+ - httpd
+ - mariadb-server
+ state: latest
+
+- name: Remove the Apache package
+ ansible.builtin.dnf5:
+ name: httpd
+ state: absent
+
+- name: Install the latest version of Apache from the testing repo
+ ansible.builtin.dnf5:
+ name: httpd
+ enablerepo: testing
+ state: present
+
+- name: Upgrade all packages
+ ansible.builtin.dnf5:
+ name: "*"
+ state: latest
+
+- name: Update the webserver, depending on which is installed on the system. Do not install the other one
+ ansible.builtin.dnf5:
+ name:
+ - httpd
+ - nginx
+ state: latest
+ update_only: yes
+
+- name: Install the nginx rpm from a remote repo
+ ansible.builtin.dnf5:
+ name: 'http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm'
+ state: present
+
+- name: Install nginx rpm from a local file
+ ansible.builtin.dnf5:
+ name: /usr/local/src/nginx-release-centos-6-0.el6.ngx.noarch.rpm
+ state: present
+
+- name: Install Package based upon the file it provides
+ ansible.builtin.dnf5:
+ name: /usr/bin/cowsay
+ state: present
+
+- name: Install the 'Development tools' package group
+ ansible.builtin.dnf5:
+ name: '@Development tools'
+ state: present
+
+- name: Autoremove unneeded packages installed as dependencies
+ ansible.builtin.dnf5:
+ autoremove: yes
+
+- name: Uninstall httpd but keep its dependencies
+ ansible.builtin.dnf5:
+ name: httpd
+ state: absent
+ autoremove: no
+"""
+
+RETURN = """
+msg:
+ description: Additional information about the result
+ returned: always
+ type: str
+ sample: "Nothing to do"
+results:
+ description: A list of the dnf transaction results
+ returned: success
+ type: list
+ sample: ["Installed: lsof-4.94.0-4.fc37.x86_64"]
+failures:
+ description: A list of the dnf transaction failures
+ returned: failure
+ type: list
+ sample: ["Argument 'lsof' matches only excluded packages."]
+rc:
+ description: For compatibility, 0 for success, 1 for failure
+ returned: always
+ type: int
+ sample: 0
+"""
+
+import os
+import sys
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.locale import get_best_parsable_locale
+from ansible.module_utils.common.respawn import has_respawned, probe_interpreters_for_module, respawn_module
+from ansible.module_utils.yumdnf import YumDnf, yumdnf_argument_spec
+
+libdnf5 = None
+
+
+def is_installed(base, spec):
+ settings = libdnf5.base.ResolveSpecSettings()
+ query = libdnf5.rpm.PackageQuery(base)
+ query.filter_installed()
+ match, nevra = query.resolve_pkg_spec(spec, settings, True)
+ return match
+
+
+def is_newer_version_installed(base, spec):
+ try:
+ spec_nevra = next(iter(libdnf5.rpm.Nevra.parse(spec)))
+ except RuntimeError:
+ return False
+ spec_name = spec_nevra.get_name()
+ v = spec_nevra.get_version()
+ r = spec_nevra.get_release()
+ if not v or not r:
+ return False
+ spec_evr = "{}:{}-{}".format(spec_nevra.get_epoch() or "0", v, r)
+
+ query = libdnf5.rpm.PackageQuery(base)
+ query.filter_installed()
+ query.filter_name([spec_name])
+ query.filter_evr([spec_evr], libdnf5.common.QueryCmp_GT)
+
+ return query.size() > 0
+
+
+def package_to_dict(package):
+ return {
+ "nevra": package.get_nevra(),
+ "envra": package.get_nevra(), # dnf module compat
+ "name": package.get_name(),
+ "arch": package.get_arch(),
+ "epoch": str(package.get_epoch()),
+ "release": package.get_release(),
+ "version": package.get_version(),
+ "repo": package.get_repo_id(),
+ "yumstate": "installed" if package.is_installed() else "available",
+ }
+
+
+def get_unneeded_pkgs(base):
+ query = libdnf5.rpm.PackageQuery(base)
+ query.filter_installed()
+ query.filter_unneeded()
+ for pkg in query:
+ yield pkg
+
+
+class Dnf5Module(YumDnf):
+ def __init__(self, module):
+ super(Dnf5Module, self).__init__(module)
+ self._ensure_dnf()
+
+ # FIXME https://github.com/rpm-software-management/dnf5/issues/402
+ self.lockfile = ""
+ self.pkg_mgr_name = "dnf5"
+
+ # DNF specific args that are not part of YumDnf
+ self.allowerasing = self.module.params["allowerasing"]
+ self.nobest = self.module.params["nobest"]
+
+ def _ensure_dnf(self):
+ locale = get_best_parsable_locale(self.module)
+ os.environ["LC_ALL"] = os.environ["LC_MESSAGES"] = locale
+ os.environ["LANGUAGE"] = os.environ["LANG"] = locale
+
+ global libdnf5
+ has_dnf = True
+ try:
+ import libdnf5 # type: ignore[import]
+ except ImportError:
+ has_dnf = False
+
+ if has_dnf:
+ return
+
+ system_interpreters = [
+ "/usr/libexec/platform-python",
+ "/usr/bin/python3",
+ "/usr/bin/python2",
+ "/usr/bin/python",
+ ]
+
+ if not has_respawned():
+ # probe well-known system Python locations for accessible bindings, favoring py3
+ interpreter = probe_interpreters_for_module(system_interpreters, "libdnf5")
+
+ if interpreter:
+ # respawn under the interpreter where the bindings should be found
+ respawn_module(interpreter)
+ # end of the line for this module, the process will exit here once the respawned module completes
+
+ # done all we can do, something is just broken (auto-install isn't useful anymore with respawn, so it was removed)
+ self.module.fail_json(
+ msg="Could not import the dnf python module using {0} ({1}). "
+ "Please install `python3-dnf` or `python2-dnf` package or ensure you have specified the "
+ "correct ansible_python_interpreter. (attempted {2})".format(
+ sys.executable, sys.version.replace("\n", ""), system_interpreters
+ ),
+ failures=[],
+ )
+
+ def is_lockfile_pid_valid(self):
+ # FIXME https://github.com/rpm-software-management/dnf5/issues/402
+ return True
+
+ def run(self):
+ if sys.version_info.major < 3:
+ self.module.fail_json(
+ msg="The dnf5 module requires Python 3.",
+ failures=[],
+ rc=1,
+ )
+ if not self.list and not self.download_only and os.geteuid() != 0:
+ self.module.fail_json(
+ msg="This command has to be run under the root user.",
+ failures=[],
+ rc=1,
+ )
+
+ if self.enable_plugin or self.disable_plugin:
+ self.module.fail_json(
+ msg="enable_plugin and disable_plugin options are not yet implemented in DNF5",
+ failures=[],
+ rc=1,
+ )
+
+ base = libdnf5.base.Base()
+ conf = base.get_config()
+
+ if self.conf_file:
+ conf.config_file_path = self.conf_file
+
+ try:
+ base.load_config_from_file()
+ except RuntimeError as e:
+ self.module.fail_json(
+ msg=str(e),
+ conf_file=self.conf_file,
+ failures=[],
+ rc=1,
+ )
+
+ if self.releasever is not None:
+ variables = base.get_vars()
+ variables.set("releasever", self.releasever)
+ if self.exclude:
+ conf.excludepkgs = self.exclude
+ if self.disable_excludes:
+ if self.disable_excludes == "all":
+ self.disable_excludes = "*"
+ conf.disable_excludes = self.disable_excludes
+ conf.skip_broken = self.skip_broken
+ conf.best = not self.nobest
+ conf.install_weak_deps = self.install_weak_deps
+ conf.gpgcheck = not self.disable_gpg_check
+ conf.localpkg_gpgcheck = not self.disable_gpg_check
+ conf.sslverify = self.sslverify
+ conf.clean_requirements_on_remove = self.autoremove
+ conf.installroot = self.installroot
+ conf.use_host_config = True # needed for installroot
+ conf.cacheonly = self.cacheonly
+
+ base.setup()
+
+ log_router = base.get_logger()
+ global_logger = libdnf5.logger.GlobalLogger()
+ global_logger.set(log_router.get(), libdnf5.logger.Logger.Level_DEBUG)
+ logger = libdnf5.logger.create_file_logger(base)
+ log_router.add_logger(logger)
+
+ if self.update_cache:
+ repo_query = libdnf5.repo.RepoQuery(base)
+ repo_query.filter_type(libdnf5.repo.Repo.Type_AVAILABLE)
+ for repo in repo_query:
+ repo_dir = repo.get_cachedir()
+ if os.path.exists(repo_dir):
+ repo_cache = libdnf5.repo.RepoCache(base, repo_dir)
+ repo_cache.write_attribute(libdnf5.repo.RepoCache.ATTRIBUTE_EXPIRED)
+
+ sack = base.get_repo_sack()
+ sack.create_repos_from_system_configuration()
+
+ repo_query = libdnf5.repo.RepoQuery(base)
+ if self.disablerepo:
+ repo_query.filter_id(self.disablerepo, libdnf5.common.QueryCmp_IGLOB)
+ for repo in repo_query:
+ repo.disable()
+ if self.enablerepo:
+ repo_query.filter_id(self.enablerepo, libdnf5.common.QueryCmp_IGLOB)
+ for repo in repo_query:
+ repo.enable()
+
+ sack.update_and_load_enabled_repos(True)
+
+ if self.update_cache and not self.names and not self.list:
+ self.module.exit_json(
+ msg="Cache updated",
+ changed=False,
+ results=[],
+ rc=0
+ )
+
+ if self.list:
+ command = self.list
+ if command == "updates":
+ command = "upgrades"
+
+ if command in {"installed", "upgrades", "available"}:
+ query = libdnf5.rpm.PackageQuery(base)
+ getattr(query, "filter_{}".format(command))()
+ results = [package_to_dict(package) for package in query]
+ elif command in {"repos", "repositories"}:
+ query = libdnf5.repo.RepoQuery(base)
+ query.filter_enabled(True)
+ results = [{"repoid": repo.get_id(), "state": "enabled"} for repo in query]
+ else:
+ resolve_spec_settings = libdnf5.base.ResolveSpecSettings()
+ query = libdnf5.rpm.PackageQuery(base)
+ query.resolve_pkg_spec(command, resolve_spec_settings, True)
+ results = [package_to_dict(package) for package in query]
+
+ self.module.exit_json(msg="", results=results, rc=0)
+
+ settings = libdnf5.base.GoalJobSettings()
+ settings.group_with_name = True
+ if self.bugfix or self.security:
+ advisory_query = libdnf5.advisory.AdvisoryQuery(base)
+ types = []
+ if self.bugfix:
+ types.append("bugfix")
+ if self.security:
+ types.append("security")
+ advisory_query.filter_type(types)
+ settings.set_advisory_filter(advisory_query)
+
+ goal = libdnf5.base.Goal(base)
+ results = []
+ if self.names == ["*"] and self.state == "latest":
+ goal.add_rpm_upgrade(settings)
+ elif self.state in {"install", "present", "latest"}:
+ upgrade = self.state == "latest"
+ for spec in self.names:
+ if is_newer_version_installed(base, spec):
+ if self.allow_downgrade:
+ if upgrade:
+ if is_installed(base, spec):
+ goal.add_upgrade(spec, settings)
+ else:
+ goal.add_install(spec, settings)
+ else:
+ goal.add_install(spec, settings)
+ elif is_installed(base, spec):
+ if upgrade:
+ goal.add_upgrade(spec, settings)
+ else:
+ if self.update_only:
+ results.append("Packages providing {} not installed due to update_only specified".format(spec))
+ else:
+ goal.add_install(spec, settings)
+ elif self.state in {"absent", "removed"}:
+ for spec in self.names:
+ try:
+ goal.add_remove(spec, settings)
+ except RuntimeError as e:
+ self.module.fail_json(msg=str(e), failures=[], rc=1)
+ if self.autoremove:
+ for pkg in get_unneeded_pkgs(base):
+ goal.add_rpm_remove(pkg, settings)
+
+ goal.set_allow_erasing(self.allowerasing)
+ try:
+ transaction = goal.resolve()
+ except RuntimeError as e:
+ self.module.fail_json(msg=str(e), failures=[], rc=1)
+
+ if transaction.get_problems():
+ failures = []
+ for log in transaction.get_resolve_logs_as_strings():
+ if log.startswith("No match for argument") and self.state in {"install", "present", "latest"}:
+ failures.append("No package {} available.".format(log.rsplit(' ', 1)[-1]))
+ else:
+ failures.append(log)
+
+ if transaction.get_problems() & libdnf5.base.GoalProblem_SOLVER_ERROR != 0:
+ msg = "Depsolve Error occurred"
+ else:
+ msg = "Failed to install some of the specified packages"
+ self.module.fail_json(
+ msg=msg,
+ failures=failures,
+ rc=1,
+ )
+
+ # NOTE dnf module compat
+ actions_compat_map = {
+ "Install": "Installed",
+ "Remove": "Removed",
+ "Replace": "Installed",
+ "Upgrade": "Installed",
+ "Replaced": "Removed",
+ }
+ changed = bool(transaction.get_transaction_packages())
+ for pkg in transaction.get_transaction_packages():
+ if self.download_only:
+ action = "Downloaded"
+ else:
+ action = libdnf5.base.transaction.transaction_item_action_to_string(pkg.get_action())
+ results.append("{}: {}".format(actions_compat_map.get(action, action), pkg.get_package().get_nevra()))
+
+ result_to_str = {
+ libdnf5.rpm.RpmSignature.CheckResult_FAILED_NOT_SIGNED: "package is not signed",
+ }
+ msg = ""
+ if self.module.check_mode:
+ if results:
+ msg = "Check mode: No changes made, but would have if not in check mode"
+ else:
+ transaction.download(self.download_dir or "")
+ if not self.download_only:
+ for pkg in transaction.get_transaction_packages():
+ if not self.disable_gpg_check:
+ result = libdnf5.rpm.RpmSignature(base).check_package_signature(pkg.get_package())
+ if result == libdnf5.rpm.RpmSignature.CheckResult_FAILED_NOT_SIGNED:
+ self.module.fail_json(
+ msg="Failed to validate GPG signature for {}: {}".format(pkg.get_package().get_nevra(), result_to_str.get(result, result)),
+ failures=[],
+ rc=1,
+ )
+ if result in {
+ libdnf5.rpm.RpmSignature.CheckResult_FAILED_KEY_MISSING,
+ libdnf5.rpm.RpmSignature.CheckResult_FAILED_NOT_TRUSTED,
+ libdnf5.rpm.RpmSignature.CheckResult_FAILED
+ }:
+ # FIXME https://github.com/rpm-software-management/dnf5/issues/386
+ pass
+
+ transaction.set_description("ansible dnf5 module")
+ result = transaction.run()
+ if result != libdnf5.base.Transaction.TransactionRunResult_SUCCESS:
+ self.module.fail_json(
+ msg="Failed to install some of the specified packages",
+ failures=["{}: {}".format(transaction.transaction_result_to_string(result), log) for log in transaction.get_transaction_problems()],
+ rc=1,
+ )
+
+ if not msg and not results:
+ msg = "Nothing to do"
+
+ self.module.exit_json(
+ results=results,
+ changed=changed,
+ msg=msg,
+ rc=0,
+ )
+
+
+def main():
+ # Extend yumdnf_argument_spec with dnf-specific features that will never be
+ # backported to yum because yum is now in "maintenance mode" upstream
+ yumdnf_argument_spec["argument_spec"]["allowerasing"] = dict(default=False, type="bool")
+ yumdnf_argument_spec["argument_spec"]["nobest"] = dict(default=False, type="bool")
+ Dnf5Module(AnsibleModule(**yumdnf_argument_spec)).run()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/lib/ansible/modules/yum.py b/lib/ansible/modules/yum.py
index dddc47cc76..0f8982b47a 100644
--- a/lib/ansible/modules/yum.py
+++ b/lib/ansible/modules/yum.py
@@ -23,10 +23,11 @@ options:
description:
- This module supports C(yum) (as it always has), this is known as C(yum3)/C(YUM3)/C(yum-deprecated) by
upstream yum developers. As of Ansible 2.7+, this module also supports C(YUM4), which is the
- "new yum" and it has an C(dnf) backend.
+ "new yum" and it has an C(dnf) backend. As of ansible-core 2.15+, C(dnf) will auto select the backend
+ based on the C(ansible_pkg_mgr) fact.
- By default, this module will select the backend based on the C(ansible_pkg_mgr) fact.
default: "auto"
- choices: [ auto, yum, yum4, dnf ]
+ choices: [ auto, yum, yum4, dnf, dnf4, dnf5 ]
type: str
version_added: "2.7"
name:
@@ -1806,7 +1807,7 @@ def main():
# list=repos
# list=pkgspec
- yumdnf_argument_spec['argument_spec']['use_backend'] = dict(default='auto', choices=['auto', 'yum', 'yum4', 'dnf'])
+ yumdnf_argument_spec['argument_spec']['use_backend'] = dict(default='auto', choices=['auto', 'yum', 'yum4', 'dnf', 'dnf4', 'dnf5'])
module = AnsibleModule(
**yumdnf_argument_spec
diff --git a/lib/ansible/plugins/action/dnf.py b/lib/ansible/plugins/action/dnf.py
new file mode 100644
index 0000000000..bf8ac3f46a
--- /dev/null
+++ b/lib/ansible/plugins/action/dnf.py
@@ -0,0 +1,83 @@
+# Copyright: (c) 2023, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from ansible.errors import AnsibleActionFail
+from ansible.plugins.action import ActionBase
+from ansible.utils.display import Display
+
+display = Display()
+
+VALID_BACKENDS = frozenset(("dnf", "dnf4", "dnf5"))
+
+
+# FIXME mostly duplicate of the yum action plugin
+class ActionModule(ActionBase):
+
+ TRANSFERS_FILES = False
+
+ def run(self, tmp=None, task_vars=None):
+ self._supports_check_mode = True
+ self._supports_async = True
+
+ result = super(ActionModule, self).run(tmp, task_vars)
+ del tmp # tmp no longer has any effect
+
+ # Carry-over concept from the package action plugin
+ if 'use' in self._task.args and 'use_backend' in self._task.args:
+ raise AnsibleActionFail("parameters are mutually exclusive: ('use', 'use_backend')")
+
+ module = self._task.args.get('use', self._task.args.get('use_backend', 'auto'))
+
+ if module == 'auto':
+ try:
+ if self._task.delegate_to: # if we delegate, we should use delegated host's facts
+ module = self._templar.template("{{hostvars['%s']['ansible_facts']['pkg_mgr']}}" % self._task.delegate_to)
+ else:
+ module = self._templar.template("{{ansible_facts.pkg_mgr}}")
+ except Exception:
+ pass # could not get it from template!
+
+ if module not in VALID_BACKENDS:
+ facts = self._execute_module(
+ module_name="ansible.legacy.setup", module_args=dict(filter="ansible_pkg_mgr", gather_subset="!all"),
+ task_vars=task_vars)
+ display.debug("Facts %s" % facts)
+ module = facts.get("ansible_facts", {}).get("ansible_pkg_mgr", "auto")
+ if (not self._task.delegate_to or self._task.delegate_facts) and module != 'auto':
+ result['ansible_facts'] = {'pkg_mgr': module}
+
+ if module not in VALID_BACKENDS:
+ result.update(
+ {
+ 'failed': True,
+ 'msg': ("Could not detect which major revision of dnf is in use, which is required to determine module backend.",
+ "You should manually specify use_backend to tell the module whether to use the dnf4 or dnf5 backend})"),
+ }
+ )
+
+ else:
+ if module == "dnf4":
+ module = "dnf"
+
+ # eliminate collisions with collections search while still allowing local override
+ module = 'ansible.legacy.' + module
+
+ if not self._shared_loader_obj.module_loader.has_plugin(module):
+ result.update({'failed': True, 'msg': "Could not find a dnf module backend for %s." % module})
+ else:
+ new_module_args = self._task.args.copy()
+ if 'use_backend' in new_module_args:
+ del new_module_args['use_backend']
+ if 'use' in new_module_args:
+ del new_module_args['use']
+
+ display.vvvv("Running %s as the backend for the dnf action plugin" % module)
+ result.update(self._execute_module(
+ module_name=module, module_args=new_module_args, task_vars=task_vars, wrap_async=self._task.async_val))
+
+ # Cleanup
+ if not self._task.async_val:
+ # remove a temporary path we created
+ self._remove_tmp_path(self._connection._shell.tmpdir)
+
+ return result
diff --git a/lib/ansible/plugins/action/yum.py b/lib/ansible/plugins/action/yum.py
index d90a9e00cf..9121e8129d 100644
--- a/lib/ansible/plugins/action/yum.py
+++ b/lib/ansible/plugins/action/yum.py
@@ -23,7 +23,7 @@ from ansible.utils.display import Display
display = Display()
-VALID_BACKENDS = frozenset(('yum', 'yum4', 'dnf'))
+VALID_BACKENDS = frozenset(('yum', 'yum4', 'dnf', 'dnf4', 'dnf5'))
class ActionModule(ActionBase):
@@ -53,6 +53,9 @@ class ActionModule(ActionBase):
module = self._task.args.get('use', self._task.args.get('use_backend', 'auto'))
+ if module == 'dnf':
+ module = 'auto'
+
if module == 'auto':
try:
if self._task.delegate_to: # if we delegate, we should use delegated host's facts
@@ -81,7 +84,7 @@ class ActionModule(ActionBase):
)
else:
- if module == "yum4":
+ if module in {"yum4", "dnf4"}:
module = "dnf"
# eliminate collisions with collections search while still allowing local override
@@ -90,7 +93,6 @@ class ActionModule(ActionBase):
if not self._shared_loader_obj.module_loader.has_plugin(module):
result.update({'failed': True, 'msg': "Could not find a yum module backend for %s." % module})
else:
- # run either the yum (yum3) or dnf (yum4) backend module
new_module_args = self._task.args.copy()
if 'use_backend' in new_module_args:
del new_module_args['use_backend']
diff --git a/test/integration/targets/dnf/tasks/dnf.yml b/test/integration/targets/dnf/tasks/dnf.yml
index ec1c36f860..e6343e98f0 100644
--- a/test/integration/targets/dnf/tasks/dnf.yml
+++ b/test/integration/targets/dnf/tasks/dnf.yml
@@ -224,7 +224,7 @@
- assert:
that:
- dnf_result is success
- - dnf_result.results|length == 2
+ - dnf_result.results|length >= 2
- "dnf_result.results[0].startswith('Removed: ')"
- "dnf_result.results[1].startswith('Removed: ')"
@@ -427,6 +427,10 @@
- shell: 'dnf -y group install "Custom Group" && dnf -y group remove "Custom Group"'
register: shell_dnf_result
+- dnf:
+ name: "@Custom Group"
+ state: absent
+
# GROUP UPGRADE - this will go to the same method as group install
# but through group_update - it is its invocation we're testing here
# see commit 119c9e5d6eb572c4a4800fbe8136095f9063c37b
@@ -446,6 +450,10 @@
# cleanup until https://github.com/ansible/ansible/issues/27377 is resolved
- shell: dnf -y group install "Custom Group" && dnf -y group remove "Custom Group"
+- dnf:
+ name: "@Custom Group"
+ state: absent
+
- name: try to install non existing group
dnf:
name: "@non-existing-group"
@@ -522,6 +530,7 @@
- dnf_result is changed
- "'results' in dnf_result"
- landsidescalping_result.rc == 0
+ when: not dnf5|default('false')
# Fedora 28 (DNF 2) does not support this, just remove the package itself
- name: remove landsidescalping package on Fedora 28
@@ -551,30 +560,35 @@
- "'No package non-existent-rpm available' in dnf_result['failures'][0]"
- "'Failed to install some of the specified packages' in dnf_result['msg']"
-- name: use latest to install httpd
+- name: ensure sos isn't installed
dnf:
- name: httpd
+ name: sos
+ state: absent
+
+- name: use latest to install sos
+ dnf:
+ name: sos
state: latest
register: dnf_result
-- name: verify httpd was installed
+- name: verify sos was installed
assert:
that:
- - "'changed' in dnf_result"
+ - dnf_result is changed
-- name: uninstall httpd
+- name: uninstall sos
dnf:
- name: httpd
+ name: sos
state: removed
-- name: update httpd only if it exists
+- name: update sos only if it exists
dnf:
- name: httpd
+ name: sos
state: latest
update_only: yes
register: dnf_result
-- name: verify httpd not installed
+- name: verify sos not installed
assert:
that:
- "not dnf_result is changed"
diff --git a/test/integration/targets/dnf/tasks/main.yml b/test/integration/targets/dnf/tasks/main.yml
index 66a171ac94..b84e3389bc 100644
--- a/test/integration/targets/dnf/tasks/main.yml
+++ b/test/integration/targets/dnf/tasks/main.yml
@@ -62,6 +62,7 @@
- astream_name is defined
- (ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('29', '>=')) or
(ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>='))
+ - not dnf5|default('false')
tags:
- dnf_modularity
@@ -70,5 +71,7 @@
(ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>='))
- include_tasks: cacheonly.yml
- when: (ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('23', '>=')) or
- (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>='))
+ when:
+ - (ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('23', '>=')) or
+ (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>='))
+ - not dnf5|default('false')
diff --git a/test/integration/targets/dnf/tasks/skip_broken_and_nobest.yml b/test/integration/targets/dnf/tasks/skip_broken_and_nobest.yml
index 503cb4c3e6..f54c0a83d9 100644
--- a/test/integration/targets/dnf/tasks/skip_broken_and_nobest.yml
+++ b/test/integration/targets/dnf/tasks/skip_broken_and_nobest.yml
@@ -240,7 +240,8 @@
- name: Do an "upgrade" to an older version of broken-a, allow_downgrade=false
dnf:
name:
- - broken-a-1.2.3-1*
+ #- broken-a-1.2.3-1*
+ - broken-a-1.2.3-1.el7.x86_64
state: latest
allow_downgrade: false
check_mode: true
diff --git a/test/integration/targets/dnf/tasks/test_sos_removal.yml b/test/integration/targets/dnf/tasks/test_sos_removal.yml
index 40ceb62bf4..5e161dbb2e 100644
--- a/test/integration/targets/dnf/tasks/test_sos_removal.yml
+++ b/test/integration/targets/dnf/tasks/test_sos_removal.yml
@@ -15,5 +15,5 @@
that:
- sos_rm is successful
- sos_rm is changed
- - "'Removed: sos-{{ sos_version }}-{{ sos_release }}' in sos_rm.results[0]"
- - sos_rm.results|length == 1
+ - sos_rm.results|select("contains", "Removed: sos-{{ sos_version }}-{{ sos_release }}")|length > 0
+ - sos_rm.results|length > 0
diff --git a/test/integration/targets/dnf5/aliases b/test/integration/targets/dnf5/aliases
new file mode 100644
index 0000000000..4baf6e62c6
--- /dev/null
+++ b/test/integration/targets/dnf5/aliases
@@ -0,0 +1,6 @@
+destructive
+shippable/posix/group1
+skip/freebsd
+skip/macos
+context/target
+needs/target/dnf
diff --git a/test/integration/targets/dnf5/playbook.yml b/test/integration/targets/dnf5/playbook.yml
new file mode 100644
index 0000000000..16dfd22e36
--- /dev/null
+++ b/test/integration/targets/dnf5/playbook.yml
@@ -0,0 +1,19 @@
+- hosts: localhost
+ tasks:
+ - block:
+ - command: "dnf install -y 'dnf-command(copr)'"
+ - command: dnf copr enable -y rpmsoftwaremanagement/dnf5-unstable
+ - command: dnf install -y python3-libdnf5
+
+ - include_role:
+ name: dnf
+ vars:
+ dnf5: true
+ dnf_log_files:
+ - /var/log/dnf5.log
+ when:
+ - ansible_distribution == 'Fedora'
+ - ansible_distribution_major_version is version('37', '>=')
+ module_defaults:
+ dnf:
+ use_backend: dnf5
diff --git a/test/integration/targets/dnf5/runme.sh b/test/integration/targets/dnf5/runme.sh
new file mode 100755
index 0000000000..51a6bf4524
--- /dev/null
+++ b/test/integration/targets/dnf5/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -ux
+export ANSIBLE_ROLES_PATH=../
+ansible-playbook playbook.yml "$@"
diff --git a/test/sanity/ignore.txt b/test/sanity/ignore.txt
index 2f0d9b87c6..cc49e463d4 100644
--- a/test/sanity/ignore.txt
+++ b/test/sanity/ignore.txt
@@ -40,6 +40,7 @@ lib/ansible/modules/copy.py validate-modules:doc-default-does-not-match-spec
lib/ansible/modules/copy.py validate-modules:nonexistent-parameter-documented
lib/ansible/modules/copy.py validate-modules:undocumented-parameter
lib/ansible/modules/dnf.py validate-modules:parameter-invalid
+lib/ansible/modules/dnf5.py validate-modules:parameter-invalid
lib/ansible/modules/file.py validate-modules:undocumented-parameter
lib/ansible/modules/find.py use-argspec-type-path # fix needed
lib/ansible/modules/git.py pylint:disallowed-name