dnf5,apt: add auto_install_module_deps option (#84292)

* dnf5,apt: add auto_install_module_deps option

Fixes #84206
devel
Martin Krizek 19 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). - Manages I(apt) packages (such as for Debian/Ubuntu).
version_added: "0.0.2" version_added: "0.0.2"
options: options:
auto_install_module_deps:
description:
- Automatically install dependencies required to run this module.
type: bool
default: yes
version_added: 2.19
name: name:
description: description:
- A list of package names, like V(foo), or package specifier with version, like V(foo=1.0) or V(foo>=1.0). - 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 default: 60
version_added: "2.12" version_added: "2.12"
requirements: requirements:
- python-apt (python 2) - python3-apt
- python3-apt (python 3)
- aptitude (before 2.4) - aptitude (before 2.4)
author: "Matthew Williams (@mgwilliams)" author: "Matthew Williams (@mgwilliams)"
extends_documentation_fragment: action_common_attributes 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 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 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. - 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 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, the module will attempt to install it. 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. 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_downgrade=dict(type='bool', default=False, aliases=['allow-downgrade', 'allow_downgrades', 'allow-downgrades']),
allow_change_held_packages=dict(type='bool', default=False), allow_change_held_packages=dict(type='bool', default=False),
lock_timeout=dict(type='int', default=60), lock_timeout=dict(type='int', default=60),
auto_install_module_deps=dict(type='bool', default=True),
), ),
mutually_exclusive=[['deb', 'package', 'upgrade']], mutually_exclusive=[['deb', 'package', 'upgrade']],
required_one_of=[['autoremove', 'deb', 'package', 'update_cache', 'upgrade']], required_one_of=[['autoremove', 'deb', 'package', 'update_cache', 'upgrade']],
@ -1268,7 +1274,7 @@ def main():
if not HAS_PYTHON_APT: if not HAS_PYTHON_APT:
# This interpreter can't see the apt Python library- we'll do the following to try and fix that: # 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 # 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 # we limit to the current interpreter version to try and avoid installing a whole other Python just
# for apt support # for apt support
# 3) if we installed a support package, try to respawn under what we think is the right interpreter (could be # 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 # don't make changes if we're in check_mode
if module.check_mode: if module.check_mode:
module.fail_json(msg="%s must be installed to use check mode. " module.fail_json(
"If run normally this module can auto-install it." % apt_pkg_name) msg=f"{apt_pkg_name} must be installed to use check mode. "
"If run normally this module can auto-install it, "
# We skip cache update in auto install the dependency if the "see the auto_install_module_deps option.",
# user explicitly declared it with update_cache=no. )
if module.params.get('update_cache') is False: elif p['auto_install_module_deps']:
module.warn("Auto-installing missing dependency without updating cache: %s" % apt_pkg_name) # We skip cache update in auto install the dependency if the
else: # user explicitly declared it with update_cache=no.
module.warn("Updating cache and auto-installing missing dependency: %s" % apt_pkg_name) if module.params.get('update_cache') is False:
module.run_command([APT_GET_CMD, 'update'], check_rc=True) module.warn("Auto-installing missing dependency without updating cache: %s" % apt_pkg_name)
else:
# try to install the apt Python binding module.warn("Updating cache and auto-installing missing dependency: %s" % apt_pkg_name)
apt_pkg_cmd = [APT_GET_CMD, 'install', apt_pkg_name, '-y', '-q', dpkg_options] module.run_command([APT_GET_CMD, 'update'], check_rc=True)
if install_recommends is False: # try to install the apt Python binding
apt_pkg_cmd.extend(["-o", "APT::Install-Recommends=no"]) apt_pkg_cmd = [APT_GET_CMD, 'install', apt_pkg_name, '-y', '-q', dpkg_options]
elif install_recommends is True:
apt_pkg_cmd.extend(["-o", "APT::Install-Recommends=yes"]) if install_recommends is False:
# install_recommends is None uses the OS default apt_pkg_cmd.extend(["-o", "APT::Install-Recommends=no"])
elif install_recommends is True:
module.run_command(apt_pkg_cmd, check_rc=True) apt_pkg_cmd.extend(["-o", "APT::Install-Recommends=yes"])
# install_recommends is None uses the OS default
# try again to find the bindings in common places
interpreter = probe_interpreters_for_module(interpreters, 'apt') module.run_command(apt_pkg_cmd, check_rc=True)
if interpreter: # try again to find the bindings in common places
# found the Python bindings; respawn this module under the interpreter where we found them interpreter = probe_interpreters_for_module(interpreters, 'apt')
# NB: respawn is somewhat wasteful if it's this interpreter, but simplifies the code
respawn_module(interpreter) if interpreter:
# this is the end of the line for this process, it will exit here once the respawned module has completed # found the Python bindings; respawn this module under the interpreter where we found them
else: # NB: respawn is somewhat wasteful if it's this interpreter, but simplifies the code
# we've done all we can do; just tell the user it's busted and get out respawn_module(interpreter)
module.fail_json(msg="{0} must be installed and visible from {1}.".format(apt_pkg_name, sys.executable)) # 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: if p['clean'] is True:
aptclean_stdout, aptclean_stderr, aptclean_diff = aptclean(module) 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." 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 short_description: Manages packages with the I(dnf5) package manager
options: options:
auto_install_module_deps:
description:
- Automatically install dependencies required to run this module.
type: bool
default: yes
version_added: 2.19
name: name:
description: description:
- "A package name or package specifier with version, like C(name-1.0). - "A package name or package specifier with version, like C(name-1.0).
@ -246,6 +252,10 @@ attributes:
platforms: rhel platforms: rhel
requirements: requirements:
- "python3-libdnf5" - "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 version_added: 2.15
""" """
@ -460,6 +470,8 @@ def get_unneeded_pkgs(base):
class Dnf5Module(YumDnf): class Dnf5Module(YumDnf):
def __init__(self, module): def __init__(self, module):
super(Dnf5Module, self).__init__(module) super(Dnf5Module, self).__init__(module)
self.auto_install_module_deps = self.module.params["auto_install_module_deps"]
self._ensure_dnf() self._ensure_dnf()
self.pkg_mgr_name = "dnf5" self.pkg_mgr_name = "dnf5"
@ -509,21 +521,30 @@ class Dnf5Module(YumDnf):
] ]
if not has_respawned(): if not has_respawned():
# probe well-known system Python locations for accessible bindings, favoring py3 for attempt in (1, 2):
interpreter = probe_interpreters_for_module(system_interpreters, "libdnf5") # probe well-known system Python locations for accessible bindings
interpreter = probe_interpreters_for_module(system_interpreters, "libdnf5")
if interpreter: if interpreter:
# respawn under the interpreter where the bindings should be found # respawn under the interpreter where the bindings should be found
respawn_module(interpreter) respawn_module(interpreter)
# end of the line for this module, the process will exit here once the respawned module completes # 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( self.module.fail_json(
msg="Could not import the libdnf5 python module using {0} ({1}). " msg=f"Could not import the libdnf5 python module using {sys.executable} ({py_version}). "
"Please install python3-libdnf5 package or ensure you have specified the " "Ensure python3-libdnf5 package is installed (either manually or via the auto_install_module_deps option) "
"correct ansible_python_interpreter. (attempted {2})".format( f"or that you have specified the correct ansible_python_interpreter. (attempted {system_interpreters}).",
sys.executable, sys.version.replace("\n", ""), system_interpreters
),
failures=[], failures=[],
) )
@ -780,6 +801,11 @@ class Dnf5Module(YumDnf):
def main(): def main():
yumdnf_argument_spec["argument_spec"].update(
dict(
auto_install_module_deps=dict(type="bool", default=True),
)
)
Dnf5Module(AnsibleModule(**yumdnf_argument_spec)).run() Dnf5Module(AnsibleModule(**yumdnf_argument_spec)).run()

@ -8,17 +8,17 @@
distro_mirror: http://archive.ubuntu.com/ubuntu distro_mirror: http://archive.ubuntu.com/ubuntu
when: ansible_distribution == 'Ubuntu' when: ansible_distribution == 'Ubuntu'
# UNINSTALL 'python-apt' # UNINSTALL 'python3-apt'
# The `apt` module has the smarts to auto-install `python-apt(3)`. To test, we # The `apt` module has the smarts to auto-install `python3-apt`. To test, we
# will first uninstall `python-apt`. # will first uninstall `python3-apt`.
- name: uninstall python-apt with apt - name: uninstall python3-apt with apt
apt: apt:
pkg: [python-apt, python3-apt] pkg: python3-apt
state: absent state: absent
purge: yes purge: yes
register: apt_result 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 - name: test fail uninstall hello without required apt deps in check mode
apt: apt:
pkg: hello pkg: hello
@ -32,13 +32,25 @@
assert: assert:
that: that:
- apt_result is failed - 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 - name: check with dpkg
shell: dpkg -s python-apt python3-apt shell: dpkg -s python3-apt
register: dpkg_result register: dpkg_result
ignore_errors: true 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' # UNINSTALL 'hello'
# With 'python-apt' uninstalled, the first call to 'apt' should install # With 'python-apt' uninstalled, the first call to 'apt' should install
# python-apt without updating the cache. # python-apt without updating the cache.

@ -2,9 +2,26 @@
tasks: tasks:
- block: - block:
- command: "dnf install -y 'dnf-command(copr)'" - command: "dnf install -y 'dnf-command(copr)'"
- command: dnf copr enable -y rpmsoftwaremanagement/dnf-nightly - name: Test against dnf5 nightly build to detect any issues early
- command: dnf install -y -x condor python3-libdnf5 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: - include_role:
name: dnf name: dnf
vars: vars:

Loading…
Cancel
Save