From 634758cfc77dff535c5e9e17cc99c6ba19e965b1 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:55:41 +0200 Subject: Adding upstream version 2.16.5. Signed-off-by: Daniel Baumann --- lib/ansible/modules/dnf5.py | 708 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 708 insertions(+) create mode 100644 lib/ansible/modules/dnf5.py (limited to 'lib/ansible/modules/dnf5.py') diff --git a/lib/ansible/modules/dnf5.py b/lib/ansible/modules/dnf5.py new file mode 100644 index 0000000..823d3a7 --- /dev/null +++ b/lib/ansible/modules/dnf5.py @@ -0,0 +1,708 @@ +# -*- 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 M(ansible.builtin.dnf) module + provides are implemented in M(ansible.builtin.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 an 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 O(list) argument as a best practice. + type: str + state: + description: + - Whether to install (V(present), V(latest)), or remove (V(absent)) a package. + - Default is V(None), however in effect the default action is V(present) unless the V(autoremove) option is + enabled for this module, then V(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 O(state) is V(present) or V(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 V(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 O(state) is V(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 O(state) is V(present) or V(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 O(state) is V(latest) + default: "no" + type: bool + security: + description: + - If set to V(true), and O(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 V(true), and O(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 V(all), disables all excludes. + - If set to V(main), disable excludes defined in [main] in dnf.conf. + - If set to V(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 M(ansible.builtin.yum) module. + type: bool + default: "yes" + sslverify: + description: + - Disables SSL validation of the repository server for this transaction. + - This should be set to V(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 M(ansible.builtin.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 O(download_only) is specified. + type: str + allowerasing: + description: + - If V(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: + - 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 libdnf5 python module using {0} ({1}). " + "Please install python3-libdnf5 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 = "all" if self.cacheonly else "none" + if self.download_dir: + conf.destdir = self.download_dir + + 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_event in transaction.get_resolve_logs(): + if log_event.get_problem() == libdnf5.base.GoalProblem_NOT_FOUND and self.state in {"install", "present", "latest"}: + # NOTE dnf module compat + failures.append("No package {} available.".format(log_event.get_spec())) + else: + failures.append(log_event.to_string()) + + 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())) + + 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() + if not self.download_only: + transaction.set_description("ansible dnf5 module") + result = transaction.run() + if result == libdnf5.base.Transaction.TransactionRunResult_ERROR_GPG_CHECK: + self.module.fail_json( + msg="Failed to validate GPG signatures: {}".format(",".join(transaction.get_gpg_signature_problems())), + failures=[], + rc=1, + ) + elif 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() -- cgit v1.2.3