diff --git a/lib/ansible/modules/apt.py b/lib/ansible/modules/apt.py index 842479af921..40a91e73f22 100644 --- a/lib/ansible/modules/apt.py +++ b/lib/ansible/modules/apt.py @@ -876,35 +876,35 @@ def install_deb( installed_pkg = apt.Cache()[pkg_key] installed_version = installed_pkg.installed.version if package_version_compare(pkg_version, installed_version) == 0: - # Does not need to down-/upgrade, move on to next package continue except Exception: - # Must not be installed, continue with installation pass - # Check if package is installable if not pkg.check(): - if force or ("later version" in pkg._failure_string and allow_downgrade): + missing = getattr(pkg, "missing_deps", []) + all_virtual = all(cache.is_virtual_package(dep) for dep in missing) + if all_virtual: + pass + elif force or ("later version" in pkg._failure_string and allow_downgrade): pass else: m.fail_json(msg=pkg._failure_string) - # Handle virtual package dependencies properly + # handle virtual package dependencies properly missing_deps = [] for dep in pkg.missing_deps: - if cache.is_virtual_package(dep): - providers = cache.get_providing_packages(dep) - if providers: - # Check if any provider is already installed - provider_installed = False - for provider in providers: - if provider in cache and cache[provider].installed: - provider_installed = True - break - if not provider_installed: + try: + if cache.is_virtual_package(dep): + providers = cache.get_providing_packages(dep) + if providers: + if any(cache[p].installed for p in providers if p in cache): + continue + else: + missing_deps.append(dep) + else: missing_deps.append(dep) else: missing_deps.append(dep) - else: + except Exception as e: missing_deps.append(dep) deps_to_install.extend(missing_deps) @@ -912,15 +912,12 @@ def install_deb( except Exception as e: m.fail_json(msg="Unable to install package: %s" % to_native(e)) - # Install 'Recommends' of this deb file if install_recommends: pkg_recommends = get_field_of_deb(m, deb_file, "Recommends") deps_to_install.extend([pkg_name.strip() for pkg_name in pkg_recommends.split()]) - # and add this deb to the list of packages to install pkgs_to_install.append(deb_file) - # install the deps through apt retvals = {} if deps_to_install: install_dpkg_options = f"{expand_dpkg_options(dpkg_options)} -o DPkg::Lock::Timeout={lock_timeout}" @@ -950,6 +947,44 @@ def install_deb( with PolicyRcD(m): rc, out, err = m.run_command(cmd) + # handle missing virtual dependencies + if rc != 0 and "dependency problems" in err: + missing_deps = [] + for line in err.splitlines(): + if "depends on" in line and "however" in line: + dep = line.split("depends on", 1)[1].split(";", 1)[0].strip() + if dep: + missing_deps.append(dep) + + resolved = [] + for dep in missing_deps: + try: + if cache.is_virtual_package(dep): + providers = cache.get_providing_packages(dep) + if providers: + provider = providers[0] + resolved.append(provider.name) + except Exception: + continue + + if resolved: + m.warn("dpkg dependency error — virtual deps: %s → installing providers: %s" + % (', '.join(missing_deps), ', '.join(resolved))) + (success, retvals) = install( + m=m, pkgspec=resolved, cache=cache, + install_recommends=True, + fail_on_autoremove=fail_on_autoremove, + allow_unauthenticated=allow_unauthenticated, + allow_downgrade=allow_downgrade, + allow_change_held_packages=allow_change_held_packages, + dpkg_options=expand_dpkg_options(dpkg_options), + ) + if not success: + m.warn("Provider install failed, attempting apt --fix-broken install") + m.run_command("/usr/bin/apt-get -y --fix-broken install") + + rc, out, err = m.run_command(cmd) + if "stdout" in retvals: stdout = retvals["stdout"] + out else: diff --git a/test/integration/targets/apt/tasks/apt_virtual_pkg_fix.yml b/test/integration/targets/apt/tasks/apt_virtual_pkg_fix.yml new file mode 100644 index 00000000000..a3600379370 --- /dev/null +++ b/test/integration/targets/apt/tasks/apt_virtual_pkg_fix.yml @@ -0,0 +1,60 @@ +--- +- block: + - name: Create directory for test .deb + ansible.builtin.file: + path: /tmp/virtual-test/DEBIAN + state: directory + + - name: Create a test .deb file that depends on a virtual package + ansible.builtin.shell: | + cat > /tmp/virtual-test/DEBIAN/control << EOF + Package: virtual-test-pkg + Version: 1.0 + Architecture: all + Description: Test package depending on virtual package + Depends: mail-transport-agent + EOF + dpkg-deb --build /tmp/virtual-test /tmp/virtual-test.deb + args: + creates: /tmp/virtual-test.deb + + - name: Remove any existing MTA providers + ansible.builtin.apt: + name: + - exim4 + - postfix + - sendmail + state: absent + + - name: Test the patch - install .deb with virtual package dependency + ansible.builtin.apt: + deb: /tmp/virtual-test.deb + state: present + register: patch_test_result + + - name: Debug - Show patch test result + ansible.builtin.debug: + var: patch_test_result + + - name: Check if a virtual package provider was automatically installed + ansible.builtin.shell: | + dpkg -l | grep -E "^ii" | grep -E "(exim|postfix|sendmail)" && echo "SUCCESS: Virtual package provider was installed" || echo "FAIL: No provider installed" + register: provider_check + changed_when: false + + - name: Show provider check result + ansible.builtin.debug: + var: provider_check.stdout + + - name: Assert the patch worked + ansible.builtin.assert: + that: + - patch_test_result is succeeded + - "'SUCCESS' in provider_check.stdout" + fail_msg: "The patch failed to properly resolve virtual package dependencies when installing .deb files" + + always: + - name: Clean up test package + ansible.builtin.file: + path: /tmp/virtual-test.deb + state: absent diff --git a/test/integration/targets/apt/tasks/main.yml b/test/integration/targets/apt/tasks/main.yml index 4ae93219d69..3faeb9cd3be 100644 --- a/test/integration/targets/apt/tasks/main.yml +++ b/test/integration/targets/apt/tasks/main.yml @@ -30,6 +30,8 @@ - import_tasks: 'apt-builddep.yml' + - import_tasks: apt_virtual_pkg_fix.yml + - block: - import_tasks: 'repo.yml' always: diff --git a/test/integration/targets/apt_virtual_pkg_fix/aliases b/test/integration/targets/apt_virtual_pkg_fix/aliases deleted file mode 100644 index 86ef763be51..00000000000 --- a/test/integration/targets/apt_virtual_pkg_fix/aliases +++ /dev/null @@ -1 +0,0 @@ -apt \ No newline at end of file diff --git a/test/integration/targets/apt_virtual_pkg_fix/meta/main.yml b/test/integration/targets/apt_virtual_pkg_fix/meta/main.yml deleted file mode 100644 index 95cbe2c606b..00000000000 --- a/test/integration/targets/apt_virtual_pkg_fix/meta/main.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -# Ensure our fake APT repo is set up before running this test -dependencies: - - setup_deb_repo diff --git a/test/integration/targets/apt_virtual_pkg_fix/tasks/main.yml b/test/integration/targets/apt_virtual_pkg_fix/tasks/main.yml deleted file mode 100644 index 63f1bc4f06f..00000000000 --- a/test/integration/targets/apt_virtual_pkg_fix/tasks/main.yml +++ /dev/null @@ -1,58 +0,0 @@ ---- -- name: Create directory for test .deb - ansible.builtin.file: - path: /tmp/virtual-test/DEBIAN - state: directory - -- name: Create a test .deb file that depends on a virtual package - ansible.builtin.shell: | - cat > /tmp/virtual-test/DEBIAN/control << EOF - Package: virtual-test-pkg - Version: 1.0 - Architecture: all - Description: Test package depending on virtual package - Depends: mail-transport-agent - EOF - dpkg-deb --build /tmp/virtual-test /tmp/virtual-test.deb - args: - creates: /tmp/virtual-test.deb - -- name: Remove any existing MTA providers - ansible.builtin.apt: - name: - - exim4 - - postfix - - sendmail - state: absent - -- name: Test the patch - install .deb with virtual package dependency - ansible.builtin.apt: - deb: /tmp/virtual-test.deb - state: present - register: patch_test_result - -- name: Debug - Show patch test result - ansible.builtin.debug: - var: patch_test_result - -- name: Check if a virtual package provider was automatically installed - ansible.builtin.shell: | - dpkg -l | grep -E "^ii" | grep -E "(exim|postfix|sendmail)" && echo "SUCCESS: Virtual package provider was installed" || echo "FAIL: No provider installed" - register: provider_check - changed_when: false - -- name: Show provider check result - ansible.builtin.debug: - var: provider_check.stdout - -- name: Assert the patch worked - ansible.builtin.assert: - that: - - patch_test_result is succeeded - - "'SUCCESS' in provider_check.stdout" - fail_msg: "The patch failed to properly resolve virtual package dependencies when installing .deb files" - -- name: Clean up test package - ansible.builtin.file: - path: /tmp/virtual-test.deb - state: absent \ No newline at end of file diff --git a/test/integration/targets/setup_deb_repo/files/package_specs/stable/packagefour-1.0.0.ctl b/test/integration/targets/setup_deb_repo/files/package_specs/stable/packagefour-1.0.0.ctl deleted file mode 100644 index 3e041a422ed..00000000000 --- a/test/integration/targets/setup_deb_repo/files/package_specs/stable/packagefour-1.0.0.ctl +++ /dev/null @@ -1,9 +0,0 @@ -Section: misc -Priority: optional -Standards-Version: 2.3.3 - -Package: packagefour -Version: 1.0 -Architecture: all -Description: Depends on virtual package -Depends: mail-transport-agent diff --git a/test/integration/targets/setup_deb_repo/files/package_specs/stable/packagethree-1.0.0.ctl b/test/integration/targets/setup_deb_repo/files/package_specs/stable/packagethree-1.0.0.ctl deleted file mode 100644 index 42cf1476d82..00000000000 --- a/test/integration/targets/setup_deb_repo/files/package_specs/stable/packagethree-1.0.0.ctl +++ /dev/null @@ -1,9 +0,0 @@ -Section: misc -Priority: optional -Standards-Version: 2.3.3 - -Package: packagethree -Version: 1.0 -Architecture: all -Description: Provider of virtual package 'mail-transport-agent' -Provides: mail-transport-agent