apt: include apt preferences (e.g. pinning) when selecting packages (#78327)

Fixes #77969
pull/78466/head
Patrick Hemmer 3 years ago committed by GitHub
parent 85acf4d1e5
commit 04e8927579
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,2 @@
bugfixes:
- apt - fix package selection to include /etc/apt/preferences(.d) (https://github.com/ansible/ansible/issues/77969)

@ -210,6 +210,8 @@ notes:
(If you typo C(foo) as C(fo) apt-get would install packages that have "fo" in their name with a warning and a prompt for the user. (If you typo C(foo) as C(fo) apt-get would install packages that have "fo" in their name with a warning and a prompt for the user.
Since we don't have warnings and prompts before installing we disallow this.Use an explicit fnmatch pattern if you want wildcarding) Since we don't have warnings and prompts before installing we disallow this.Use an explicit fnmatch pattern if you want wildcarding)
- When used with a C(loop:) each package will be processed individually, it is much more efficient to pass the list directly to the I(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 I(name) option.
- When C(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.
''' '''
EXAMPLES = ''' EXAMPLES = '''
@ -485,12 +487,17 @@ def package_version_compare(version, other_version):
def package_best_match(pkgname, version_cmp, version, release, cache): def package_best_match(pkgname, version_cmp, version, release, cache):
policy = apt_pkg.Policy(cache) policy = apt_pkg.Policy(cache)
policy.read_pinfile(apt_pkg.config.find_file("Dir::Etc::preferences"))
policy.read_pindir(apt_pkg.config.find_file("Dir::Etc::preferencesparts"))
if release: if release:
# 990 is the priority used in `apt-get -t` # 990 is the priority used in `apt-get -t`
policy.create_pin('Release', pkgname, release, 990) policy.create_pin('Release', pkgname, release, 990)
if version_cmp == "=": if version_cmp == "=":
# You can't pin to a minimum version, only equality with a glob # Installing a specific version from command line overrides all pinning
policy.create_pin('Version', pkgname, version, 991) # We don't mimmic this exactly, but instead set a priority which is higher than all APT built-in pin priorities.
policy.create_pin('Version', pkgname, version, 1001)
pkg = cache[pkgname] pkg = cache[pkgname]
pkgver = policy.get_candidate_ver(pkg) pkgver = policy.get_candidate_ver(pkg)
if not pkgver: if not pkgver:
@ -503,6 +510,14 @@ def package_best_match(pkgname, version_cmp, version, release, cache):
def package_status(m, pkgname, version_cmp, version, default_release, cache, state): def package_status(m, pkgname, version_cmp, version, default_release, cache, state):
"""
:return: A tuple of (installed, installed_version, version_installable, has_files). *installed* indicates whether
the package (regardless of version) is installed. *installed_version* indicates whether the installed package
matches the provided version criteria. *version_installable* provides the latest matching version that can be
installed. In the case of virtual packages where we can't determine an applicable match, True is returned.
*has_files* indicates whether the package has files on the filesystem (even if not installed, meaning a purge is
required).
"""
try: try:
# get the package from the cache, as well as the # get the package from the cache, as well as the
# low-level apt_pkg.Package object which contains # low-level apt_pkg.Package object which contains
@ -527,15 +542,15 @@ def package_status(m, pkgname, version_cmp, version, default_release, cache, sta
# Otherwise return nothing so apt will sort out # Otherwise return nothing so apt will sort out
# what package to satisfy this with # what package to satisfy this with
return False, False, None, False return False, False, True, False
m.fail_json(msg="No package matching '%s' is available" % pkgname) m.fail_json(msg="No package matching '%s' is available" % pkgname)
except AttributeError: except AttributeError:
# python-apt version too old to detect virtual packages # python-apt version too old to detect virtual packages
# mark as not installed and let apt-get install deal with it # mark as not installed and let apt-get install deal with it
return False, False, None, False return False, False, True, False
else: else:
return False, False, False, False return False, False, None, False
try: try:
has_files = len(pkg.installed_files) > 0 has_files = len(pkg.installed_files) > 0
except UnicodeDecodeError: except UnicodeDecodeError:
@ -565,13 +580,16 @@ def package_status(m, pkgname, version_cmp, version, default_release, cache, sta
if version_cmp == "=": if version_cmp == "=":
# check if the version is matched as well # check if the version is matched as well
version_is_installed = fnmatch.fnmatch(installed_version, version) version_is_installed = fnmatch.fnmatch(installed_version, version)
if version_best and installed_version != version_best and fnmatch.fnmatch(version_best, version):
version_installable = version_best
elif version_cmp == ">=": elif version_cmp == ">=":
version_is_installed = apt_pkg.version_compare(installed_version, version) >= 0 version_is_installed = apt_pkg.version_compare(installed_version, version) >= 0
if version_best and installed_version != version_best and apt_pkg.version_compare(version_best, version) >= 0:
version_installable = version_best
else: else:
version_is_installed = True version_is_installed = True
if version_best and installed_version != version_best:
if installed_version != version_best: version_installable = version_best
version_installable = version_best
else: else:
version_installable = version_best version_installable = version_best
@ -692,23 +710,27 @@ def install(m, pkgspec, cache, upgrade=False, default_release=None,
name, version_cmp, version = package_split(package) name, version_cmp, version = package_split(package)
package_names.append(name) package_names.append(name)
installed, installed_version, version_installable, has_files = package_status(m, name, version_cmp, version, default_release, cache, state='install') installed, installed_version, version_installable, has_files = package_status(m, name, version_cmp, version, default_release, cache, state='install')
if (not installed and not only_upgrade) or (installed and not installed_version) or (upgrade and version_installable):
if version_installable or version: if (not installed_version and not version_installable) or (not installed and only_upgrade):
pkg_list.append("'%s=%s'" % (name, version_installable or version)) status = False
data = dict(msg="no available installation candidate for %s" % package)
return (status, data)
if version_installable and ((not installed and not only_upgrade) or upgrade or not installed_version):
if version_installable is not True:
pkg_list.append("'%s=%s'" % (name, version_installable))
elif version:
pkg_list.append("'%s=%s'" % (name, version))
else: else:
pkg_list.append("'%s'" % name) pkg_list.append("'%s'" % name)
elif installed_version and version_installable and version_cmp == "=": elif installed_version and version_installable and version_cmp == "=":
# This happens when the package is installed, a newer version is # This happens when the package is installed, a newer version is
# available, and the version is a wildcard that matches both # available, and the version is a wildcard that matches both
# #
# We do not apply the upgrade flag because we cannot specify both
# a version and state=latest. (This behaviour mirrors how apt
# treats a version with wildcard in the package)
#
# This is legacy behavior, and isn't documented (in fact it does # This is legacy behavior, and isn't documented (in fact it does
# things documentations says it shouldn't). It should not be relied # things documentations says it shouldn't). It should not be relied
# upon. # upon.
pkg_list.append("'%s=%s'" % (name, version_installable)) pkg_list.append("'%s=%s'" % (name, version))
packages = ' '.join(pkg_list) packages = ' '.join(pkg_list)
if packages: if packages:

@ -40,6 +40,17 @@
state: absent state: absent
allow_unauthenticated: yes allow_unauthenticated: yes
- name: Try to install non-existent version
apt:
name: foo=99
state: present
ignore_errors: true
register: apt_result
- name: Check if install failed
assert:
that:
- apt_result is failed
# https://github.com/ansible/ansible/issues/30638 # https://github.com/ansible/ansible/issues/30638
- block: - block:
@ -56,6 +67,7 @@
assert: assert:
that: that:
- "apt_result is not changed" - "apt_result is not changed"
- "apt_result is failed"
- apt: - apt:
name: foo=1.0.0 name: foo=1.0.0
@ -122,12 +134,58 @@
- item.item.3 is none or "Inst foo [1.0.0] (" + item.item.3 + " localhost [all])" in item.stdout_lines - item.item.3 is none or "Inst foo [1.0.0] (" + item.item.3 + " localhost [all])" in item.stdout_lines
loop: '{{ apt_result.results }}' loop: '{{ apt_result.results }}'
- name: Pin foo=1.0.0
copy:
content: |-
Package: foo
Pin: version 1.0.0
Pin-Priority: 1000
dest: /etc/apt/preferences.d/foo
- name: Run pinning version test matrix
apt:
name: foo{{ item.0 }}
default_release: '{{ item.1 }}'
state: '{{ item.2 | ternary("latest","present") }}'
check_mode: true
ignore_errors: true
register: apt_result
loop:
# [filter, release, state_latest, expected] # expected=null for no change. expected=False to assert an error
- ["", null, false, null]
- ["", null, true, null]
- ["=1.0.0", null, false, null]
- ["=1.0.0", null, true, null]
- ["=1.0.1", null, false, "1.0.1"]
#- ["=1.0.*", null, false, null] # legacy behavior. should not upgrade without state=latest
- ["=1.0.*", null, true, "1.0.1"]
- [">=1.0.0", null, false, null]
- [">=1.0.0", null, true, null]
- [">=1.0.1", null, false, False]
- ["", "testing", false, null]
- ["", "testing", true, null]
- ["=2.0.0", null, false, "2.0.0"]
- [">=2.0.0", "testing", false, False]
- name: Validate pinning version test matrix
assert:
that:
- (item.item.3 != False) or (item.item.3 == False and item is failed)
- (item.item.3 is string) == (item.stdout is defined)
- item.item.3 is not string or "Inst foo [1.0.0] (" + item.item.3 + " localhost [all])" in item.stdout_lines
loop: '{{ apt_result.results }}'
always: always:
- name: Uninstall foo - name: Uninstall foo
apt: apt:
name: foo name: foo
state: absent state: absent
- name: Unpin foo
file:
path: /etc/apt/preferences.d/foo
state: absent
# https://github.com/ansible/ansible/issues/35900 # https://github.com/ansible/ansible/issues/35900
- block: - block:
- name: Disable ubuntu repos so system packages are not upgraded and do not change testing env - name: Disable ubuntu repos so system packages are not upgraded and do not change testing env

Loading…
Cancel
Save