From 4997063b4afb05561ee9aa6de2b815dfcffffc3d Mon Sep 17 00:00:00 2001 From: psi / Ryo Hirafuji Date: Tue, 30 Jun 2020 22:53:14 +0900 Subject: [PATCH] apt - add fail_on_autoremove option to avoid unintended package removals (#70056) * Ensure not to remove existing packages while installing apt packages. * Make all lines shorter than 160 characters * Allow removing packages only when upgrading. * Add integration tests --- ...-module-to-avoid-unintended-uninstalls.yml | 2 + lib/ansible/modules/apt.py | 48 +++++++++++++++---- test/integration/targets/apt/tasks/apt.yml | 38 +++++++++++++++ 3 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 changelogs/fragments/70056-add-a-param-to-apt-module-to-avoid-unintended-uninstalls.yml diff --git a/changelogs/fragments/70056-add-a-param-to-apt-module-to-avoid-unintended-uninstalls.yml b/changelogs/fragments/70056-add-a-param-to-apt-module-to-avoid-unintended-uninstalls.yml new file mode 100644 index 00000000000..4c31a33ed2e --- /dev/null +++ b/changelogs/fragments/70056-add-a-param-to-apt-module-to-avoid-unintended-uninstalls.yml @@ -0,0 +1,2 @@ +bugfixes: + - apt - add ``fail_on_autoremove`` param to apt module to avoid unintended package removals (https://github.com/ansible/ansible/issues/63231) diff --git a/lib/ansible/modules/apt.py b/lib/ansible/modules/apt.py index d90d12dca63..e7b1450e97f 100644 --- a/lib/ansible/modules/apt.py +++ b/lib/ansible/modules/apt.py @@ -135,6 +135,14 @@ options: type: bool default: 'no' version_added: "2.1" + fail_on_autoremove: + description: + - 'Corresponds to the C(--no-remove) option for C(apt).' + - 'If C(yes), it is ensured that no packages will be removed or the task will fail.' + - 'C(fail_on_autoremove) is only supported with state except C(absent)' + type: bool + default: 'no' + version_added: "2.11" force_apt_get: description: - Force usage of apt-get instead of aptitude @@ -194,6 +202,12 @@ EXAMPLES = ''' default_release: squeeze-backports update_cache: yes +- name: Install zfsutils-linux with ensuring conflicted packages (e.g. zfs-fuse) will not be removed. + apt: + name: zfsutils-linux + state: latest + fail_on_autoremove: yes + - name: Install latest version of "openjdk-6-jdk" ignoring "install-recommends" apt: name: openjdk-6-jdk @@ -613,7 +627,7 @@ def mark_installed_manually(m, packages): def install(m, pkgspec, cache, upgrade=False, default_release=None, install_recommends=None, force=False, dpkg_options=expand_dpkg_options(DPKG_OPTIONS), - build_dep=False, fixed=False, autoremove=False, only_upgrade=False, + build_dep=False, fixed=False, autoremove=False, fail_on_autoremove=False, only_upgrade=False, allow_unauthenticated=False): pkg_list = [] packages = "" @@ -656,6 +670,11 @@ def install(m, pkgspec, cache, upgrade=False, default_release=None, else: autoremove = '' + if fail_on_autoremove: + fail_on_autoremove = '--no-remove' + else: + fail_on_autoremove = '' + if only_upgrade: only_upgrade = '--only-upgrade' else: @@ -667,9 +686,10 @@ def install(m, pkgspec, cache, upgrade=False, default_release=None, fixed = '' if build_dep: - cmd = "%s -y %s %s %s %s %s build-dep %s" % (APT_GET_CMD, dpkg_options, only_upgrade, fixed, force_yes, check_arg, packages) + cmd = "%s -y %s %s %s %s %s %s build-dep %s" % (APT_GET_CMD, dpkg_options, only_upgrade, fixed, force_yes, fail_on_autoremove, check_arg, packages) else: - cmd = "%s -y %s %s %s %s %s %s install %s" % (APT_GET_CMD, dpkg_options, only_upgrade, fixed, force_yes, autoremove, check_arg, packages) + cmd = "%s -y %s %s %s %s %s %s %s install %s" % \ + (APT_GET_CMD, dpkg_options, only_upgrade, fixed, force_yes, autoremove, fail_on_autoremove, check_arg, packages) if default_release: cmd += " -t '%s'" % (default_release,) @@ -719,7 +739,7 @@ def get_field_of_deb(m, deb_file, field="Version"): return to_native(stdout).strip('\n') -def install_deb(m, debs, cache, force, install_recommends, allow_unauthenticated, dpkg_options): +def install_deb(m, debs, cache, force, fail_on_autoremove, install_recommends, allow_unauthenticated, dpkg_options): changed = False deps_to_install = [] pkgs_to_install = [] @@ -761,6 +781,7 @@ def install_deb(m, debs, cache, force, install_recommends, allow_unauthenticated if deps_to_install: (success, retvals) = install(m=m, pkgspec=deps_to_install, cache=cache, install_recommends=install_recommends, + fail_on_autoremove=fail_on_autoremove, allow_unauthenticated=allow_unauthenticated, dpkg_options=expand_dpkg_options(dpkg_options)) if not success: @@ -890,7 +911,7 @@ def cleanup(m, purge=False, force=False, operation=None, def upgrade(m, mode="yes", force=False, default_release=None, use_apt_get=False, - dpkg_options=expand_dpkg_options(DPKG_OPTIONS), autoremove=False, + dpkg_options=expand_dpkg_options(DPKG_OPTIONS), autoremove=False, fail_on_autoremove=False, allow_unauthenticated=False, ): @@ -932,6 +953,11 @@ def upgrade(m, mode="yes", force=False, default_release=None, else: force_yes = '' + if fail_on_autoremove: + fail_on_autoremove = '--no-remove' + else: + fail_on_autoremove = '' + allow_unauthenticated = '--allow-unauthenticated' if allow_unauthenticated else '' if apt_cmd is None: @@ -942,8 +968,7 @@ def upgrade(m, mode="yes", force=False, default_release=None, "to have APTITUDE in path or use 'force_apt_get=True'") apt_cmd_path = m.get_bin_path(apt_cmd, required=True) - cmd = '%s -y %s %s %s %s %s' % (apt_cmd_path, dpkg_options, force_yes, allow_unauthenticated, - check_arg, upgrade_command) + cmd = '%s -y %s %s %s %s %s %s' % (apt_cmd_path, dpkg_options, force_yes, fail_on_autoremove, allow_unauthenticated, check_arg, upgrade_command) if default_release: cmd += " -t '%s'" % (default_release,) @@ -1029,6 +1054,7 @@ def main(): dpkg_options=dict(type='str', default=DPKG_OPTIONS), autoremove=dict(type='bool', default=False), autoclean=dict(type='bool', default=False), + fail_on_autoremove=dict(type='bool', default=False), policy_rc_d=dict(type='int', default=None), only_upgrade=dict(type='bool', default=False), force_apt_get=dict(type='bool', default=False), @@ -1084,6 +1110,7 @@ def main(): allow_unauthenticated = p['allow_unauthenticated'] dpkg_options = expand_dpkg_options(p['dpkg_options']) autoremove = p['autoremove'] + fail_on_autoremove = p['fail_on_autoremove'] autoclean = p['autoclean'] # Get the cache object @@ -1145,7 +1172,7 @@ def main(): force_yes = p['force'] if p['upgrade']: - upgrade(module, p['upgrade'], force_yes, p['default_release'], use_apt_get, dpkg_options, autoremove, allow_unauthenticated) + upgrade(module, p['upgrade'], force_yes, p['default_release'], use_apt_get, dpkg_options, autoremove, fail_on_autoremove, allow_unauthenticated) if p['deb']: if p['state'] != 'present': @@ -1155,7 +1182,7 @@ def main(): install_deb(module, p['deb'], cache, install_recommends=install_recommends, allow_unauthenticated=allow_unauthenticated, - force=force_yes, dpkg_options=p['dpkg_options']) + force=force_yes, fail_on_autoremove=fail_on_autoremove, dpkg_options=p['dpkg_options']) unfiltered_packages = p['package'] or () packages = [package.strip() for package in unfiltered_packages if package != '*'] @@ -1165,7 +1192,7 @@ def main(): if latest and all_installed: if packages: module.fail_json(msg='unable to install additional packages when upgrading all installed packages') - upgrade(module, 'yes', force_yes, p['default_release'], use_apt_get, dpkg_options, autoremove, allow_unauthenticated) + upgrade(module, 'yes', force_yes, p['default_release'], use_apt_get, dpkg_options, autoremove, fail_on_autoremove, allow_unauthenticated) if packages: for package in packages: @@ -1203,6 +1230,7 @@ def main(): build_dep=state_builddep, fixed=state_fixed, autoremove=autoremove, + fail_on_autoremove=fail_on_autoremove, only_upgrade=p['only_upgrade'], allow_unauthenticated=allow_unauthenticated ) diff --git a/test/integration/targets/apt/tasks/apt.yml b/test/integration/targets/apt/tasks/apt.yml index 871cb0d7ba9..abeb2e5dd80 100644 --- a/test/integration/targets/apt/tasks/apt.yml +++ b/test/integration/targets/apt/tasks/apt.yml @@ -179,6 +179,44 @@ - name: uninstall hello with apt apt: pkg=hello state=absent purge=yes +# INSTALL WITHOUT REMOVALS +- name: Install hello, that conflicts with hello-traditional + apt: + pkg: hello + state: present + update_cache: no + +- name: check hello + shell: dpkg-query -l hello + register: dpkg_result + +- name: verify installation of hello + assert: + that: + - "apt_result.changed" + - "dpkg_result.rc == 0" + +- name: Try installing hello-traditional, that conflicts with hello + apt: + pkg: hello-traditional + state: present + fail_on_autoremove: yes + ignore_errors: yes + register: apt_result + +- name: verify failure of installing hello-traditional, because it is required to remove hello to install. + assert: + that: + - apt_result is failed + - '"Packages need to be removed but remove is disabled." in apt_result.msg' + +- name: uninstall hello with apt + apt: + pkg: hello + state: absent + purge: yes + update_cache: no + - name: install deb file apt: deb="/var/cache/apt/archives/hello_{{ hello_version.stdout }}_{{ hello_architecture.stdout }}.deb" register: apt_initial