dnf5,apt: add auto_install_module_deps option (#84292)

* dnf5,apt: add auto_install_module_deps option

Fixes #84206
devel
Martin Krizek 12 hours ago committed by GitHub
parent 95e3af3e0f
commit 2a53b851fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,2 @@
minor_changes:
- dnf5, apt - add ``auto_install_module_deps`` option (https://github.com/ansible/ansible/issues/84206)

@ -17,6 +17,12 @@ description:
- Manages I(apt) packages (such as for Debian/Ubuntu).
version_added: "0.0.2"
options:
auto_install_module_deps:
description:
- Automatically install dependencies required to run this module.
type: bool
default: yes
version_added: 2.19
name:
description:
- A list of package names, like V(foo), or package specifier with version, like V(foo=1.0) or V(foo>=1.0).
@ -191,8 +197,7 @@ options:
default: 60
version_added: "2.12"
requirements:
- python-apt (python 2)
- python3-apt (python 3)
- python3-apt
- aptitude (before 2.4)
author: "Matthew Williams (@mgwilliams)"
extends_documentation_fragment: action_common_attributes
@ -214,8 +219,8 @@ notes:
- When used with a C(loop:) each package will be processed individually, it is much more efficient to pass the list directly to the O(name) option.
- When O(default_release) is used, an implicit priority of 990 is used. This is the same behavior as C(apt-get -t).
- When an exact version is specified, an implicit priority of 1001 is used.
- If the interpreter can't import C(python-apt)/C(python3-apt) the module will check for it in system-owned interpreters as well.
If the dependency can't be found, the module will attempt to install it.
- If the interpreter can't import C(python3-apt) the module will check for it in system-owned interpreters as well.
If the dependency can't be found, depending on the value of O(auto_install_module_deps) the module will attempt to install it.
If the dependency is found or installed, the module will be respawned under the correct interpreter.
"""
@ -1233,6 +1238,7 @@ def main():
allow_downgrade=dict(type='bool', default=False, aliases=['allow-downgrade', 'allow_downgrades', 'allow-downgrades']),
allow_change_held_packages=dict(type='bool', default=False),
lock_timeout=dict(type='int', default=60),
auto_install_module_deps=dict(type='bool', default=True),
),
mutually_exclusive=[['deb', 'package', 'upgrade']],
required_one_of=[['autoremove', 'deb', 'package', 'update_cache', 'upgrade']],
@ -1268,7 +1274,7 @@ def main():
if not HAS_PYTHON_APT:
# This interpreter can't see the apt Python library- we'll do the following to try and fix that:
# 1) look in common locations for system-owned interpreters that can see it; if we find one, respawn under it
# 2) finding none, try to install a matching python-apt package for the current interpreter version;
# 2) finding none, try to install a matching python3-apt package for the current interpreter version;
# we limit to the current interpreter version to try and avoid installing a whole other Python just
# for apt support
# 3) if we installed a support package, try to respawn under what we think is the right interpreter (could be
@ -1294,39 +1300,47 @@ def main():
# don't make changes if we're in check_mode
if module.check_mode:
module.fail_json(msg="%s must be installed to use check mode. "
"If run normally this module can auto-install it." % apt_pkg_name)
# We skip cache update in auto install the dependency if the
# user explicitly declared it with update_cache=no.
if module.params.get('update_cache') is False:
module.warn("Auto-installing missing dependency without updating cache: %s" % apt_pkg_name)
else:
module.warn("Updating cache and auto-installing missing dependency: %s" % apt_pkg_name)
module.run_command([APT_GET_CMD, 'update'], check_rc=True)
# try to install the apt Python binding
apt_pkg_cmd = [APT_GET_CMD, 'install', apt_pkg_name, '-y', '-q', dpkg_options]
if install_recommends is False:
apt_pkg_cmd.extend(["-o", "APT::Install-Recommends=no"])
elif install_recommends is True:
apt_pkg_cmd.extend(["-o", "APT::Install-Recommends=yes"])
# install_recommends is None uses the OS default
module.run_command(apt_pkg_cmd, check_rc=True)
# try again to find the bindings in common places
interpreter = probe_interpreters_for_module(interpreters, 'apt')
if interpreter:
# found the Python bindings; respawn this module under the interpreter where we found them
# NB: respawn is somewhat wasteful if it's this interpreter, but simplifies the code
respawn_module(interpreter)
# this is the end of the line for this process, it will exit here once the respawned module has completed
else:
# we've done all we can do; just tell the user it's busted and get out
module.fail_json(msg="{0} must be installed and visible from {1}.".format(apt_pkg_name, sys.executable))
module.fail_json(
msg=f"{apt_pkg_name} must be installed to use check mode. "
"If run normally this module can auto-install it, "
"see the auto_install_module_deps option.",
)
elif p['auto_install_module_deps']:
# We skip cache update in auto install the dependency if the
# user explicitly declared it with update_cache=no.
if module.params.get('update_cache') is False:
module.warn("Auto-installing missing dependency without updating cache: %s" % apt_pkg_name)
else:
module.warn("Updating cache and auto-installing missing dependency: %s" % apt_pkg_name)
module.run_command([APT_GET_CMD, 'update'], check_rc=True)
# try to install the apt Python binding
apt_pkg_cmd = [APT_GET_CMD, 'install', apt_pkg_name, '-y', '-q', dpkg_options]
if install_recommends is False:
apt_pkg_cmd.extend(["-o", "APT::Install-Recommends=no"])
elif install_recommends is True:
apt_pkg_cmd.extend(["-o", "APT::Install-Recommends=yes"])
# install_recommends is None uses the OS default
module.run_command(apt_pkg_cmd, check_rc=True)
# try again to find the bindings in common places
interpreter = probe_interpreters_for_module(interpreters, 'apt')
if interpreter:
# found the Python bindings; respawn this module under the interpreter where we found them
# NB: respawn is somewhat wasteful if it's this interpreter, but simplifies the code
respawn_module(interpreter)
# this is the end of the line for this process, it will exit here once the respawned module has completed
# we've done all we can do; just tell the user it's busted and get out
py_version = sys.version.replace("\n", "")
module.fail_json(
msg=f"Could not import the {apt_pkg_name} module using {sys.executable} ({py_version}). "
f"Ensure {apt_pkg_name} package is installed (either manually or via the auto_install_module_deps option) "
f"or that you have specified the correct ansible_python_interpreter. (attempted {interpreters}).",
)
if p['clean'] is True:
aptclean_stdout, aptclean_stderr, aptclean_diff = aptclean(module)

@ -14,6 +14,12 @@ description:
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:
auto_install_module_deps:
description:
- Automatically install dependencies required to run this module.
type: bool
default: yes
version_added: 2.19
name:
description:
- "A package name or package specifier with version, like C(name-1.0).
@ -246,6 +252,10 @@ attributes:
platforms: rhel
requirements:
- "python3-libdnf5"
notes:
- If the interpreter can't import C(python3-libdnf5) the module will check for it in system-owned interpreters as well.
If the dependency can't be found, depending on the value of O(auto_install_module_deps) the module will attempt to install it.
If the dependency is found or installed, the module will be respawned under the correct interpreter.
version_added: 2.15
"""
@ -460,6 +470,8 @@ def get_unneeded_pkgs(base):
class Dnf5Module(YumDnf):
def __init__(self, module):
super(Dnf5Module, self).__init__(module)
self.auto_install_module_deps = self.module.params["auto_install_module_deps"]
self._ensure_dnf()
self.pkg_mgr_name = "dnf5"
@ -509,21 +521,30 @@ class Dnf5Module(YumDnf):
]
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
for attempt in (1, 2):
# probe well-known system Python locations for accessible bindings
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
if attempt == 1:
if self.module.check_mode:
self.module.fail_json(
msg="python3-libdnf5 must be installed to use check mode. "
"If run normally this module can auto-install it, "
"see the auto_install_module_deps option.",
)
elif self.auto_install_module_deps:
self.module.run_command(["dnf", "install", "-y", "python3-libdnf5"], check_rc=True)
else:
break
# done all we can do, something is just broken (auto-install isn't useful anymore with respawn, so it was removed)
py_version = sys.version.replace("\n", "")
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
),
msg=f"Could not import the libdnf5 python module using {sys.executable} ({py_version}). "
"Ensure python3-libdnf5 package is installed (either manually or via the auto_install_module_deps option) "
f"or that you have specified the correct ansible_python_interpreter. (attempted {system_interpreters}).",
failures=[],
)
@ -780,6 +801,11 @@ class Dnf5Module(YumDnf):
def main():
yumdnf_argument_spec["argument_spec"].update(
dict(
auto_install_module_deps=dict(type="bool", default=True),
)
)
Dnf5Module(AnsibleModule(**yumdnf_argument_spec)).run()

@ -8,17 +8,17 @@
distro_mirror: http://archive.ubuntu.com/ubuntu
when: ansible_distribution == 'Ubuntu'
# UNINSTALL 'python-apt'
# The `apt` module has the smarts to auto-install `python-apt(3)`. To test, we
# will first uninstall `python-apt`.
- name: uninstall python-apt with apt
# UNINSTALL 'python3-apt'
# The `apt` module has the smarts to auto-install `python3-apt`. To test, we
# will first uninstall `python3-apt`.
- name: uninstall python3-apt with apt
apt:
pkg: [python-apt, python3-apt]
pkg: python3-apt
state: absent
purge: yes
register: apt_result
# In check mode, auto-install of `python-apt` must fail
# In check mode, auto-install of `python3-apt` must fail
- name: test fail uninstall hello without required apt deps in check mode
apt:
pkg: hello
@ -32,13 +32,25 @@
assert:
that:
- apt_result is failed
- '"If run normally this module can auto-install it." in apt_result.msg'
- '"If run normally this module can auto-install it" in apt_result.msg'
- name: check with dpkg
shell: dpkg -s python-apt python3-apt
shell: dpkg -s python3-apt
register: dpkg_result
ignore_errors: true
- name: Test the auto_install_module_deps option
apt:
pkg: hello
auto_install_module_deps: false
register: r
ignore_errors: true
- assert:
that:
- r is failed
- r.msg is contains("Could not import the python3-apt module")
# UNINSTALL 'hello'
# With 'python-apt' uninstalled, the first call to 'apt' should install
# python-apt without updating the cache.

@ -2,9 +2,26 @@
tasks:
- block:
- command: "dnf install -y 'dnf-command(copr)'"
- command: dnf copr enable -y rpmsoftwaremanagement/dnf-nightly
- command: dnf install -y -x condor python3-libdnf5
- name: Test against dnf5 nightly build to detect any issues early
command: dnf copr enable -y rpmsoftwaremanagement/dnf-nightly
- name: Ensure module deps are not installed
command: dnf remove -y python3-libdnf5
- name: Test the auto_install_module_deps option
dnf5:
name: sos
auto_install_module_deps: false
register: r
ignore_errors: true
- assert:
that:
- r is failed
- r.msg is contains("Could not import the libdnf5 python module")
# Now the first dnf5 task in the dnf role should auto install python3-libdnf5 as
# auto_install_module_deps is true by default.
- include_role:
name: dnf
vars:

Loading…
Cancel
Save