From 737c68ace3d0f6c748f465ef561856e343293d7e Mon Sep 17 00:00:00 2001 From: Patrick Kingston <66141901+pkingstonxyz@users.noreply.github.com> Date: Tue, 22 Jul 2025 11:48:33 -0400 Subject: [PATCH] Add install_python_debian parameter to deb822_repository (#85487) * Add python3-debian dependency installation in module * Add tests to exercise automatic dependency installation * Fix broken tests to include new module parameter Co-authored-by: Abhijeet Kasurde --- ...ency-installation-to-deb822_repository.yml | 2 + lib/ansible/modules/deb822_repository.py | 81 ++++++++++++++++++- .../targets/deb822_repository/tasks/main.yml | 49 +++++++++++ .../targets/deb822_repository/tasks/test.yml | 2 + 4 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 changelogs/fragments/85487-add-dependency-installation-to-deb822_repository.yml diff --git a/changelogs/fragments/85487-add-dependency-installation-to-deb822_repository.yml b/changelogs/fragments/85487-add-dependency-installation-to-deb822_repository.yml new file mode 100644 index 00000000000..3731b75f0b9 --- /dev/null +++ b/changelogs/fragments/85487-add-dependency-installation-to-deb822_repository.yml @@ -0,0 +1,2 @@ +minor_changes: + - deb822_repository - Add automatic installation of the ``python3-debian`` package if it is missing by adding the parameter ``install_python_debian`` \ No newline at end of file diff --git a/lib/ansible/modules/deb822_repository.py b/lib/ansible/modules/deb822_repository.py index d4d6205511e..00278fb0342 100644 --- a/lib/ansible/modules/deb822_repository.py +++ b/lib/ansible/modules/deb822_repository.py @@ -67,6 +67,17 @@ options: - Determines the path to the C(InRelease) file, relative to the normal position of an C(InRelease) file. type: str + install_python_debian: + description: + - Whether to automatically try to install the Python C(debian) library or not, if it is not already installed. + Without this library, the module does not work. + - Runs C(apt install python3-debian). + - Only works with the system Python. If you are using a Python on the remote that is not + the system Python, set O(install_python_debian=false) and ensure that the Python C(debian) library + for your Python version is installed some other way. + type: bool + default: false + version_added: '2.20' languages: description: - Defines which languages information such as translated @@ -228,6 +239,7 @@ key_filename: import os import re +import sys import tempfile import textwrap @@ -235,6 +247,7 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import missing_required_lib from ansible.module_utils.common.collections import is_sequence from ansible.module_utils.common.file import S_IRWXU_RXG_RXO, S_IRWU_RG_RO +from ansible.module_utils.common.respawn import has_respawned, probe_interpreters_for_module, respawn_module from ansible.module_utils.common.text.converters import to_bytes from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.six import raise_from # type: ignore[attr-defined] @@ -357,6 +370,21 @@ def write_signed_by_key(module, v, slug): return changed, filename, None +def install_python_debian(module, deb_pkg_name): + + if not module.check_mode: + apt_path = module.get_bin_path('apt', required=True) + if apt_path: + rc, so, se = module.run_command([apt_path, 'update']) + if rc != 0: + module.fail_json(msg=f"Failed update while auto installing {deb_pkg_name} due to '{se.strip()}'") + rc, so, se = module.run_command([apt_path, 'install', deb_pkg_name, '-y', '-q']) + if rc != 0: + module.fail_json(msg=f"Failed to auto-install {deb_pkg_name} due to : '{se.strip()}'") + else: + module.fail_json(msg=f"{deb_pkg_name} must be installed to use check mode") + + def main(): module = AnsibleModule( argument_spec={ @@ -395,6 +423,10 @@ def main(): 'inrelease_path': { 'type': 'str', }, + 'install_python_debian': { + 'type': 'bool', + 'default': False, + }, 'languages': { 'elements': 'str', 'type': 'list', @@ -453,8 +485,53 @@ def main(): ) if not HAS_DEBIAN: - module.fail_json(msg=missing_required_lib("python3-debian"), - exception=DEBIAN_IMP_ERR) + deb_pkg_name = 'python3-debian' + # This interpreter can't see the debian Python library- we'll do the following to try and fix that as per + # the apt_repository module: + # 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-debian package for the current interpreter version; + # we limit to the current interpreter version to try and avoid installing a whole other Python just + # for deb support + # 3) if we installed a support package, try to respawn under what we think is the right interpreter (could be + # the current interpreter again, but we'll let it respawn anyway for simplicity) + # 4) if still not working, return an error and give up (some corner cases not covered, but this shouldn't be + # made any more complex than it already is to try and cover more, eg, custom interpreters taking over + # system locations) + + if has_respawned(): + # this shouldn't be possible; short-circuit early if it happens... + module.fail_json(msg=f"{deb_pkg_name} must be installed and visible from {sys.executable}.") + + interpreters = ['/usr/bin/python3', '/usr/bin/python'] + + interpreter = probe_interpreters_for_module(interpreters, 'debian') + + if interpreter: + # found the Python bindings; respawn this module under the interpreter where we found them + respawn_module(interpreter) + # this is the end of the line for this process, it will exit here once the respawned module has completed + + # don't make changes if we're in check_mode + if module.check_mode: + module.fail_json(msg=f"{deb_pkg_name} must be installed to use check mode. If run with install_python_debian, this module can auto-install it.") + + if module.params['install_python_debian']: + install_python_debian(module, deb_pkg_name) + else: + module.fail_json(msg=f'{deb_pkg_name} is not installed, and install_python_debian is False') + + # try again to find the bindings in common places + interpreter = probe_interpreters_for_module(interpreters, 'debian') + + 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=missing_required_lib(deb_pkg_name), + exception=DEBIAN_IMP_ERR) check_mode = module.check_mode diff --git a/test/integration/targets/deb822_repository/tasks/main.yml b/test/integration/targets/deb822_repository/tasks/main.yml index 561ef2a6f50..585792f73a2 100644 --- a/test/integration/targets/deb822_repository/tasks/main.yml +++ b/test/integration/targets/deb822_repository/tasks/main.yml @@ -1,13 +1,62 @@ - meta: end_play when: ansible_os_family != 'Debian' +- set_fact: + python_debian: python3-debian + - block: + # UNINSTALL 'python3-debian' + # The `deb822_repository` module has the smarts to auto-install `python3-debian`. To + # test, we will first uninstall `python3-debian`. + - name: check {{ python_debian }} with dpkg + shell: dpkg -s {{ python_debian }} + register: dpkg_result + ignore_errors: true + + - name: uninstall {{ python_debian }} with apt + apt: pkg={{ python_debian }} state=absent purge=yes + register: apt_result + when: dpkg_result is successful + + - name: check failure when python3-debian is absent + deb822_repository: + name: myrepo + types: deb + uris: "http://example.com/debian/" + suites: stable + components: main + register: no_python3_debian + ignore_errors: true + + - name: Assert failure with absent python3-debian + assert: + that: no_python3_debian.msg is contains 'python3-debian is not installed' + + - name: run deb822 to check for python3-debian installation + deb822_repository: + name: myrepo + types: deb + uris: "http://example.com/debian/" + suites: stable + components: main + install_python_debian: true + + - name: Clean up the previously added basic Debian repository + deb822_repository: + name: myrepo + state: absent + - name: install python3-debian apt: name: python3-debian state: present register: py3_deb_install + - name: assert python3-debian already installed + assert: + that: + - not py3_deb_install.changed + - import_tasks: test.yml - import_tasks: install.yml diff --git a/test/integration/targets/deb822_repository/tasks/test.yml b/test/integration/targets/deb822_repository/tasks/test.yml index 345ccb7ad8d..6f8a5204e1c 100644 --- a/test/integration/targets/deb822_repository/tasks/test.yml +++ b/test/integration/targets/deb822_repository/tasks/test.yml @@ -119,6 +119,7 @@ focal_archive_expected: |- Components: main restricted Date-Max-Future: 10 + Install-Python-Debian: no X-Repolib-Name: ansible-test focal archive Suites: focal focal-updates Types: deb @@ -192,6 +193,7 @@ vars: signed_by_inline_expected: |- Components: main contrib non-free + Install-Python-Debian: no X-Repolib-Name: ansible-test Signed-By: -----BEGIN PGP PUBLIC KEY BLOCK-----