From d2826f468965e2e70f874e295b954ffc4e89b30c Mon Sep 17 00:00:00 2001 From: jkhall81 Date: Tue, 7 Oct 2025 06:51:06 -0700 Subject: [PATCH 1/5] Added virtual package handling to install_deb function --- lib/ansible/modules/apt.py | 23 ++++++++++-- test/units/modules/test_apt.py | 65 +++++++++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/lib/ansible/modules/apt.py b/lib/ansible/modules/apt.py index 97452f03106..ef542fd0808 100644 --- a/lib/ansible/modules/apt.py +++ b/lib/ansible/modules/apt.py @@ -888,9 +888,26 @@ def install_deb( else: m.fail_json(msg=pkg._failure_string) - # add any missing deps to the list of deps we need - # to install so they're all done in one shot - deps_to_install.extend(pkg.missing_deps) + # 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: + missing_deps.append(dep) + else: + missing_deps.append(dep) + else: + missing_deps.append(dep) + + deps_to_install.extend(missing_deps) except Exception as e: m.fail_json(msg="Unable to install package: %s" % to_native(e)) diff --git a/test/units/modules/test_apt.py b/test/units/modules/test_apt.py index d207320c82a..8bc98373364 100644 --- a/test/units/modules/test_apt.py +++ b/test/units/modules/test_apt.py @@ -4,7 +4,10 @@ from __future__ import annotations import collections -from ansible.modules.apt import expand_pkgspec_from_fnmatches +from ansible.modules.apt import ( + expand_pkgspec_from_fnmatches, +) + import pytest FakePackage = collections.namedtuple("Package", ("name",)) @@ -43,3 +46,63 @@ fake_cache = [ def test_expand_pkgspec_from_fnmatches(test_input, expected): """Test positive cases of ``expand_pkgspec_from_fnmatches``.""" assert expand_pkgspec_from_fnmatches(None, test_input, fake_cache) == expected + + +def test_virtual_package_resolution_fixed(): + """Test that virtual package dependencies are properly resolved with the fix.""" + # Scenario: A package depends on virtual package 'libglib2.0-0' + # which is provided by 'libglib2.0-0t64' + + virtual_dependency = 'libglib2.0-0' + providing_package = 'libglib2.0-0t64' + + class MockCache: + def is_virtual_package(self, pkg_name): + return pkg_name == virtual_dependency + + def get_providing_packages(self, pkg_name): + if pkg_name == virtual_dependency: + return [providing_package] + return [] + + def __contains__(self, pkg_name): + # Virtual packages are not directly in cache, but have providers + return pkg_name == providing_package # Only the provider is in cache + + def __getitem__(self, pkg_name): + if pkg_name == providing_package: + return MockPackage(installed=True) + raise KeyError(pkg_name) + + class MockPackage: + def __init__(self, installed=False): + self.installed = installed + + cache = MockCache() + unsatisfied_deps = [] + + # SIMULATE THE FIXED LOGIC (what we implemented): + dep_name = virtual_dependency + + # FIXED IMPLEMENTATION (with virtual package handling): + if cache.is_virtual_package(dep_name): + providers = cache.get_providing_packages(dep_name) + if providers: + # Check if any provider is installed + provider_installed = False + for provider in providers: + if provider in cache and cache[provider].installed: + provider_installed = True + break + if not provider_installed: + unsatisfied_deps.append(dep_name) + else: + unsatisfied_deps.append(dep_name) + elif dep_name not in cache or not cache[dep_name].installed: + unsatisfied_deps.append(dep_name) + + # With the fixed logic, virtual dependencies with installed providers should be satisfied + assert virtual_dependency not in unsatisfied_deps, ( + f"Virtual package {virtual_dependency} should be satisfied by {providing_package}" + ) + assert len(unsatisfied_deps) == 0 \ No newline at end of file From 0922e9cadc049b31dd60e84dc0a85aa511011a74 Mon Sep 17 00:00:00 2001 From: jkhall81 Date: Tue, 7 Oct 2025 12:29:14 -0700 Subject: [PATCH 2/5] Added changelog/fragments entry and fixed some formatting issues in test_apt.py --- changelogs/fragments/apt_virtual_pkg_fix.yml | 3 + lib/ansible/modules/apt.py | 2 +- test/units/modules/test_apt.py | 120 +++++++++---------- 3 files changed, 63 insertions(+), 62 deletions(-) create mode 100644 changelogs/fragments/apt_virtual_pkg_fix.yml diff --git a/changelogs/fragments/apt_virtual_pkg_fix.yml b/changelogs/fragments/apt_virtual_pkg_fix.yml new file mode 100644 index 00000000000..90089c5c0f5 --- /dev/null +++ b/changelogs/fragments/apt_virtual_pkg_fix.yml @@ -0,0 +1,3 @@ +--- +minor_changes: + - apt - add support for installing ``.deb`` files whose dependencies include virtual packages (e.g. ``libglib2.0-0`` on Ubuntu 24.04) by recognizing installed providers such as ``libglib2.0-0t64``. This resolves errors like ``Dependency is not satisfiable`` when installing VS Code and other packages (https://github.com/ansible/ansible/issues/85807). diff --git a/lib/ansible/modules/apt.py b/lib/ansible/modules/apt.py index ef542fd0808..842479af921 100644 --- a/lib/ansible/modules/apt.py +++ b/lib/ansible/modules/apt.py @@ -888,7 +888,7 @@ def install_deb( 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): diff --git a/test/units/modules/test_apt.py b/test/units/modules/test_apt.py index 8bc98373364..182b6805136 100644 --- a/test/units/modules/test_apt.py +++ b/test/units/modules/test_apt.py @@ -3,13 +3,14 @@ from __future__ import annotations import collections +from unittest.mock import Mock, patch, MagicMock +import pytest from ansible.modules.apt import ( - expand_pkgspec_from_fnmatches, + expand_pkgspec_from_fnmatches, + install_deb, ) -import pytest - FakePackage = collections.namedtuple("Package", ("name",)) fake_cache = [ FakePackage("apt"), @@ -48,61 +49,58 @@ def test_expand_pkgspec_from_fnmatches(test_input, expected): assert expand_pkgspec_from_fnmatches(None, test_input, fake_cache) == expected -def test_virtual_package_resolution_fixed(): - """Test that virtual package dependencies are properly resolved with the fix.""" - # Scenario: A package depends on virtual package 'libglib2.0-0' - # which is provided by 'libglib2.0-0t64' - - virtual_dependency = 'libglib2.0-0' - providing_package = 'libglib2.0-0t64' - - class MockCache: - def is_virtual_package(self, pkg_name): - return pkg_name == virtual_dependency - - def get_providing_packages(self, pkg_name): - if pkg_name == virtual_dependency: - return [providing_package] - return [] - - def __contains__(self, pkg_name): - # Virtual packages are not directly in cache, but have providers - return pkg_name == providing_package # Only the provider is in cache - - def __getitem__(self, pkg_name): - if pkg_name == providing_package: - return MockPackage(installed=True) - raise KeyError(pkg_name) - - class MockPackage: - def __init__(self, installed=False): - self.installed = installed - - cache = MockCache() - unsatisfied_deps = [] - - # SIMULATE THE FIXED LOGIC (what we implemented): - dep_name = virtual_dependency - - # FIXED IMPLEMENTATION (with virtual package handling): - if cache.is_virtual_package(dep_name): - providers = cache.get_providing_packages(dep_name) - if providers: - # Check if any provider is installed - provider_installed = False - for provider in providers: - if provider in cache and cache[provider].installed: - provider_installed = True - break - if not provider_installed: - unsatisfied_deps.append(dep_name) - else: - unsatisfied_deps.append(dep_name) - elif dep_name not in cache or not cache[dep_name].installed: - unsatisfied_deps.append(dep_name) - - # With the fixed logic, virtual dependencies with installed providers should be satisfied - assert virtual_dependency not in unsatisfied_deps, ( - f"Virtual package {virtual_dependency} should be satisfied by {providing_package}" - ) - assert len(unsatisfied_deps) == 0 \ No newline at end of file +def test_install_deb_filters_virtual_packages(): + """Test that install_deb filters virtual packages correctly.""" + + with patch('ansible.modules.apt.apt') as mock_apt, \ + patch('ansible.modules.apt.apt_pkg') as mock_apt_pkg: + + cache = MagicMock() + cache.is_virtual_package.side_effect = lambda x: x == 'libglib2.0-0' + cache.get_providing_packages.side_effect = lambda x: ['libglib2.0-0t64'] if x == 'libglib2.0-0' else [] + cache.__contains__.side_effect = lambda x: x in ['libglib2.0-0t64', 'real-package'] + cache.__getitem__.side_effect = lambda x: Mock(installed=True) if x == 'libglib2.0-0t64' else Mock(installed=False) + + mock_debfile = Mock() + mock_pkg = Mock() + mock_pkg.missing_deps = ['libglib2.0-0', 'real-package'] + mock_pkg.check.return_value = True + mock_debfile.DebPackage.return_value = mock_pkg + mock_apt.debfile = mock_debfile + + mock_apt.Cache.return_value = cache + mock_apt_pkg.get_architectures.return_value = ['amd64'] + + with patch('ansible.modules.apt.get_field_of_deb') as mock_get_field: + mock_get_field.return_value = 'test-package' + + with patch('ansible.modules.apt.install') as mock_install: + mock_install.return_value = (True, {}) + + m = Mock() + m.params = {"policy_rc_d": None} + m.get_bin_path.return_value = "/usr/bin/apt-mark" + m.run_command.return_value = (0, "", "") + m.warn = Mock() + m.fail_json = Mock(side_effect=AssertionError("Unexpected fail_json call")) + + install_deb( + m=m, + debs='/tmp/test.deb', + cache=cache, + force=False, + fail_on_autoremove=False, + install_recommends=False, + allow_unauthenticated=False, + allow_downgrade=False, + allow_change_held_packages=False, + dpkg_options='force-confnew', + lock_timeout=60 + ) + + call_args = mock_install.call_args + installed_deps = call_args[1]['pkgspec'] + + print("deps_to_install:", installed_deps) + assert 'libglib2.0-0' not in installed_deps + assert 'real-package' in installed_deps From ecec81539723e60ab9b836c1cca8e5b84a90ce1d Mon Sep 17 00:00:00 2001 From: jkhall81 Date: Tue, 7 Oct 2025 16:39:14 -0700 Subject: [PATCH 3/5] still working on this integration test --- .../targets/apt_virtual_pkg_fix/aliases | 1 + .../targets/apt_virtual_pkg_fix/meta/main.yml | 4 ++ .../apt_virtual_pkg_fix/tasks/main.yml | 46 +++++++++++++ .../stable/packagefour-1.0.0.ctl | 9 +++ .../stable/packagethree-1.0.0.ctl | 9 +++ test/units/modules/test_apt.py | 64 +------------------ 6 files changed, 71 insertions(+), 62 deletions(-) create mode 100644 test/integration/targets/apt_virtual_pkg_fix/aliases create mode 100644 test/integration/targets/apt_virtual_pkg_fix/meta/main.yml create mode 100644 test/integration/targets/apt_virtual_pkg_fix/tasks/main.yml create mode 100644 test/integration/targets/setup_deb_repo/files/package_specs/stable/packagefour-1.0.0.ctl create mode 100644 test/integration/targets/setup_deb_repo/files/package_specs/stable/packagethree-1.0.0.ctl diff --git a/test/integration/targets/apt_virtual_pkg_fix/aliases b/test/integration/targets/apt_virtual_pkg_fix/aliases new file mode 100644 index 00000000000..86ef763be51 --- /dev/null +++ b/test/integration/targets/apt_virtual_pkg_fix/aliases @@ -0,0 +1 @@ +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 new file mode 100644 index 00000000000..95cbe2c606b --- /dev/null +++ b/test/integration/targets/apt_virtual_pkg_fix/meta/main.yml @@ -0,0 +1,4 @@ +--- +# 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 new file mode 100644 index 00000000000..903462f6ec7 --- /dev/null +++ b/test/integration/targets/apt_virtual_pkg_fix/tasks/main.yml @@ -0,0 +1,46 @@ +--- +- name: Remove any existing packages to start fresh + ansible.builtin.apt: + name: + - exim4 + - postfix + - sendmail + - mutt + state: absent + +- name: Install a package that depends on a virtual package (mutt depends on mail-transport-agent) + ansible.builtin.apt: + name: mutt + state: present + register: apt_result + +- name: Debug - Show what was installed + ansible.builtin.debug: + var: apt_result + +- name: Check what package satisfied the mail-transport-agent virtual package + ansible.builtin.shell: | + dpkg -l | grep -E "^ii" | grep -E "(exim|postfix|sendmail)" || echo "No MTA provider found" + register: virtual_provider + changed_when: false + +- name: Show virtual provider result + ansible.builtin.debug: + var: virtual_provider.stdout + +- name: Assert virtual package resolution worked + ansible.builtin.assert: + that: + - apt_result is succeeded + - apt_result.changed # Should have installed something + - "'exim4' in virtual_provider.stdout or 'postfix' in virtual_provider.stdout or 'sendmail' in virtual_provider.stdout" + fail_msg: "apt failed to resolve and install virtual package provider" + +- name: Verify mutt is installed + ansible.builtin.command: dpkg -s mutt + register: mutt_status + changed_when: false + +- name: Show mutt status + ansible.builtin.debug: + var: mutt_status.rc \ 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 new file mode 100644 index 00000000000..3e041a422ed --- /dev/null +++ b/test/integration/targets/setup_deb_repo/files/package_specs/stable/packagefour-1.0.0.ctl @@ -0,0 +1,9 @@ +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 new file mode 100644 index 00000000000..42cf1476d82 --- /dev/null +++ b/test/integration/targets/setup_deb_repo/files/package_specs/stable/packagethree-1.0.0.ctl @@ -0,0 +1,9 @@ +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 diff --git a/test/units/modules/test_apt.py b/test/units/modules/test_apt.py index 182b6805136..2d87d2f1b25 100644 --- a/test/units/modules/test_apt.py +++ b/test/units/modules/test_apt.py @@ -3,13 +3,10 @@ from __future__ import annotations import collections -from unittest.mock import Mock, patch, MagicMock import pytest -from ansible.modules.apt import ( - expand_pkgspec_from_fnmatches, - install_deb, -) +from ansible.modules.apt import expand_pkgspec_from_fnmatches + FakePackage = collections.namedtuple("Package", ("name",)) fake_cache = [ @@ -47,60 +44,3 @@ fake_cache = [ def test_expand_pkgspec_from_fnmatches(test_input, expected): """Test positive cases of ``expand_pkgspec_from_fnmatches``.""" assert expand_pkgspec_from_fnmatches(None, test_input, fake_cache) == expected - - -def test_install_deb_filters_virtual_packages(): - """Test that install_deb filters virtual packages correctly.""" - - with patch('ansible.modules.apt.apt') as mock_apt, \ - patch('ansible.modules.apt.apt_pkg') as mock_apt_pkg: - - cache = MagicMock() - cache.is_virtual_package.side_effect = lambda x: x == 'libglib2.0-0' - cache.get_providing_packages.side_effect = lambda x: ['libglib2.0-0t64'] if x == 'libglib2.0-0' else [] - cache.__contains__.side_effect = lambda x: x in ['libglib2.0-0t64', 'real-package'] - cache.__getitem__.side_effect = lambda x: Mock(installed=True) if x == 'libglib2.0-0t64' else Mock(installed=False) - - mock_debfile = Mock() - mock_pkg = Mock() - mock_pkg.missing_deps = ['libglib2.0-0', 'real-package'] - mock_pkg.check.return_value = True - mock_debfile.DebPackage.return_value = mock_pkg - mock_apt.debfile = mock_debfile - - mock_apt.Cache.return_value = cache - mock_apt_pkg.get_architectures.return_value = ['amd64'] - - with patch('ansible.modules.apt.get_field_of_deb') as mock_get_field: - mock_get_field.return_value = 'test-package' - - with patch('ansible.modules.apt.install') as mock_install: - mock_install.return_value = (True, {}) - - m = Mock() - m.params = {"policy_rc_d": None} - m.get_bin_path.return_value = "/usr/bin/apt-mark" - m.run_command.return_value = (0, "", "") - m.warn = Mock() - m.fail_json = Mock(side_effect=AssertionError("Unexpected fail_json call")) - - install_deb( - m=m, - debs='/tmp/test.deb', - cache=cache, - force=False, - fail_on_autoremove=False, - install_recommends=False, - allow_unauthenticated=False, - allow_downgrade=False, - allow_change_held_packages=False, - dpkg_options='force-confnew', - lock_timeout=60 - ) - - call_args = mock_install.call_args - installed_deps = call_args[1]['pkgspec'] - - print("deps_to_install:", installed_deps) - assert 'libglib2.0-0' not in installed_deps - assert 'real-package' in installed_deps From de0bade17413461efef44ebad156931f287843ab Mon Sep 17 00:00:00 2001 From: jkhall81 Date: Tue, 7 Oct 2025 16:51:54 -0700 Subject: [PATCH 4/5] updated test seems to show that there is a problem in the patch. --- .../apt_virtual_pkg_fix/tasks/main.yml | 64 +++++++++++-------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/test/integration/targets/apt_virtual_pkg_fix/tasks/main.yml b/test/integration/targets/apt_virtual_pkg_fix/tasks/main.yml index 903462f6ec7..63f1bc4f06f 100644 --- a/test/integration/targets/apt_virtual_pkg_fix/tasks/main.yml +++ b/test/integration/targets/apt_virtual_pkg_fix/tasks/main.yml @@ -1,46 +1,58 @@ --- -- name: Remove any existing packages to start fresh +- 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 + - postfix - sendmail - - mutt state: absent -- name: Install a package that depends on a virtual package (mutt depends on mail-transport-agent) +- name: Test the patch - install .deb with virtual package dependency ansible.builtin.apt: - name: mutt + deb: /tmp/virtual-test.deb state: present - register: apt_result + register: patch_test_result -- name: Debug - Show what was installed +- name: Debug - Show patch test result ansible.builtin.debug: - var: apt_result + var: patch_test_result -- name: Check what package satisfied the mail-transport-agent virtual package +- name: Check if a virtual package provider was automatically installed ansible.builtin.shell: | - dpkg -l | grep -E "^ii" | grep -E "(exim|postfix|sendmail)" || echo "No MTA provider found" - register: virtual_provider + 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 virtual provider result +- name: Show provider check result ansible.builtin.debug: - var: virtual_provider.stdout + var: provider_check.stdout -- name: Assert virtual package resolution worked +- name: Assert the patch worked ansible.builtin.assert: that: - - apt_result is succeeded - - apt_result.changed # Should have installed something - - "'exim4' in virtual_provider.stdout or 'postfix' in virtual_provider.stdout or 'sendmail' in virtual_provider.stdout" - fail_msg: "apt failed to resolve and install virtual package provider" - -- name: Verify mutt is installed - ansible.builtin.command: dpkg -s mutt - register: mutt_status - changed_when: false + - 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: Show mutt status - ansible.builtin.debug: - var: mutt_status.rc \ No newline at end of file +- name: Clean up test package + ansible.builtin.file: + path: /tmp/virtual-test.deb + state: absent \ No newline at end of file From 719b6321bf161b6925e4ea6df3a1e5f5022a760f Mon Sep 17 00:00:00 2001 From: jkhall81 Date: Wed, 8 Oct 2025 06:21:15 -0700 Subject: [PATCH 5/5] apt: added support for installing .deb files with virtual package dependencies. --- lib/ansible/modules/apt.py | 73 ++++++++++++++----- .../targets/apt/tasks/apt_virtual_pkg_fix.yml | 60 +++++++++++++++ test/integration/targets/apt/tasks/main.yml | 2 + .../targets/apt_virtual_pkg_fix/aliases | 1 - .../targets/apt_virtual_pkg_fix/meta/main.yml | 4 - .../apt_virtual_pkg_fix/tasks/main.yml | 58 --------------- .../stable/packagefour-1.0.0.ctl | 9 --- .../stable/packagethree-1.0.0.ctl | 9 --- 8 files changed, 116 insertions(+), 100 deletions(-) create mode 100644 test/integration/targets/apt/tasks/apt_virtual_pkg_fix.yml delete mode 100644 test/integration/targets/apt_virtual_pkg_fix/aliases delete mode 100644 test/integration/targets/apt_virtual_pkg_fix/meta/main.yml delete mode 100644 test/integration/targets/apt_virtual_pkg_fix/tasks/main.yml delete mode 100644 test/integration/targets/setup_deb_repo/files/package_specs/stable/packagefour-1.0.0.ctl delete mode 100644 test/integration/targets/setup_deb_repo/files/package_specs/stable/packagethree-1.0.0.ctl 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