apt: add support for package version >= (#75002)

This adds the ability to specify a package version using >=. This will ensure the package is at the specified version or above.

* If the package is not installed, the latest version will be installed.
* If the package is installed and less than the specified version, it will be upgraded.
* If the package is installed and greater than or equal to the specified version, it will be left alone.

The version selection is handled by Apt itself, so things like the system policy, pinning, etc, are considered.
pull/76736/head
Patrick Hemmer 2 years ago committed by GitHub
parent a5bea8b2f5
commit 4a62c4e3e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,2 @@
minor_changes:
- apt - Add support for using ">=" in package version number matching.

@ -20,7 +20,7 @@ version_added: "0.0.2"
options:
name:
description:
- A list of package names, like C(foo), or package specifier with version, like C(foo=1.0).
- A list of package names, like C(foo), or package specifier with version, like C(foo=1.0) or C(foo>=1.0).
Name wildcards (fnmatch) like C(apt*) and version wildcards like C(foo=1.0*) are also supported.
aliases: [ package, pkg ]
type: list
@ -454,24 +454,10 @@ class PolicyRcD(object):
def package_split(pkgspec):
parts = pkgspec.split('=', 1)
version = None
parts = re.split(r'(>?=)', pkgspec, 1)
if len(parts) > 1:
version = parts[1]
return parts[0], version
def package_versions(pkgname, pkg, pkg_cache):
try:
versions = set(p.version for p in pkg.versions)
except AttributeError:
# assume older version of python-apt is installed
# apt.package.Package#versions require python-apt >= 0.7.9.
pkg_cache_list = (p for p in pkg_cache.Packages if p.Name == pkgname)
pkg_versions = (p.VersionList for p in pkg_cache_list)
versions = set(p.VerStr for p in itertools.chain(*pkg_versions))
return versions
return parts
return parts[0], None, None
def package_version_compare(version, other_version):
@ -481,7 +467,26 @@ def package_version_compare(version, other_version):
return apt_pkg.VersionCompare(version, other_version)
def package_status(m, pkgname, version, cache, state):
def package_best_match(pkgname, version_cmp, version, release, cache):
policy = apt_pkg.Policy(cache)
if release:
# 990 is the priority used in `apt-get -t`
policy.create_pin('Release', pkgname, release, 990)
if version_cmp == "=":
# You can't pin to a minimum version, only equality with a glob
policy.create_pin('Version', pkgname, version, 991)
pkg = cache[pkgname]
pkgver = policy.get_candidate_ver(pkg)
if not pkgver:
return None
if version_cmp == "=" and not fnmatch.fnmatch(pkgver.ver_str, version):
# Even though we put in a pin policy, it can be ignored if there is no
# possible candidate.
return None
return pkgver.ver_str
def package_status(m, pkgname, version_cmp, version, default_release, cache, state):
try:
# get the package from the cache, as well as the
# low-level apt_pkg.Package object which contains
@ -495,20 +500,21 @@ def package_status(m, pkgname, version, cache, state):
provided_packages = cache.get_providing_packages(pkgname)
if provided_packages:
is_installed = False
upgradable = False
version_installable = None
version_ok = False
# when virtual package providing only one package, look up status of target package
if cache.is_virtual_package(pkgname) and len(provided_packages) == 1:
package = provided_packages[0]
installed, version_ok, upgradable, has_files = package_status(m, package.name, version, cache, state='install')
installed, version_ok, version_installable, has_files = \
package_status(m, package.name, version_cmp, version, default_release, cache, state='install')
if installed:
is_installed = True
return is_installed, version_ok, upgradable, False
return is_installed, version_ok, version_installable, False
m.fail_json(msg="No package matching '%s' is available" % pkgname)
except AttributeError:
# python-apt version too old to detect virtual packages
# mark as upgradable and let apt-get install deal with it
return False, False, True, False
# mark as not installed and let apt-get install deal with it
return False, False, None, False
else:
return False, False, False, False
try:
@ -528,36 +534,29 @@ def package_status(m, pkgname, version, cache, state):
# assume older version of python-apt is installed
package_is_installed = pkg.isInstalled
version_is_installed = package_is_installed
if version:
versions = package_versions(pkgname, pkg, cache._cache)
avail_upgrades = fnmatch.filter(versions, version)
if package_is_installed:
try:
installed_version = pkg.installed.version
except AttributeError:
installed_version = pkg.installedVersion
version_best = package_best_match(pkgname, version_cmp, version, default_release, cache._cache)
version_is_installed = False
version_installable = None
if package_is_installed:
try:
installed_version = pkg.installed.version
except AttributeError:
installed_version = pkg.installedVersion
if version_cmp == "=":
# check if the version is matched as well
version_is_installed = fnmatch.fnmatch(installed_version, version)
# Only claim the package is upgradable if a candidate matches the version
package_is_upgradable = False
for candidate in avail_upgrades:
if package_version_compare(candidate, installed_version) > 0:
package_is_upgradable = True
break
elif version_cmp == ">=":
version_is_installed = apt_pkg.version_compare(installed_version, version) >= 0
else:
package_is_upgradable = bool(avail_upgrades)
version_is_installed = True
if installed_version != version_best:
version_installable = version_best
else:
try:
package_is_upgradable = pkg.is_upgradable
except AttributeError:
# assume older version of python-apt is installed
package_is_upgradable = pkg.isUpgradable
version_installable = version_best
return package_is_installed, version_is_installed, package_is_upgradable, has_files
return package_is_installed, version_is_installed, version_installable, has_files
def expand_dpkg_options(dpkg_options_compressed):
@ -581,7 +580,7 @@ def expand_pkgspec_from_fnmatches(m, pkgspec, cache):
new_pkgspec = []
if pkgspec:
for pkgspec_pattern in pkgspec:
pkgname_pattern, version = package_split(pkgspec_pattern)
pkgname_pattern, version_cmp, version = package_split(pkgspec_pattern)
# note that none of these chars is allowed in a (debian) pkgname
if frozenset('*?[]!').intersection(pkgname_pattern):
@ -671,19 +670,26 @@ def install(m, pkgspec, cache, upgrade=False, default_release=None,
pkg_list.append("'%s'" % package)
continue
name, version = package_split(package)
name, version_cmp, version = package_split(package)
package_names.append(name)
installed, installed_version, upgradable, has_files = package_status(m, name, version, cache, state='install')
if (not installed and not only_upgrade) or (installed and not installed_version) or (upgrade and upgradable):
pkg_list.append("'%s'" % package)
if installed_version and upgradable and version:
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:
pkg_list.append("'%s=%s'" % (name, version_installable or version))
else:
pkg_list.append("'%s'" % name)
elif installed_version and version_installable and version_cmp == "=":
# This happens when the package is installed, a newer version is
# 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)
pkg_list.append("'%s'" % package)
#
# This is legacy behavior, and isn't documented (in fact it does
# things documentations says it shouldn't). It should not be relied
# upon.
pkg_list.append("'%s=%s'" % (name, version_installable))
packages = ' '.join(pkg_list)
if packages:
@ -877,8 +883,8 @@ def remove(m, pkgspec, cache, purge=False, force=False,
pkg_list = []
pkgspec = expand_pkgspec_from_fnmatches(m, pkgspec, cache)
for package in pkgspec:
name, version = package_split(package)
installed, installed_version, upgradable, has_files = package_status(m, name, version, cache, state='remove')
name, version_cmp, version = package_split(package)
installed, installed_version, upgradable, has_files = package_status(m, name, version_cmp, version, None, cache, state='remove')
if installed_version or (has_files and purge):
pkg_list.append("'%s'" % package)
packages = ' '.join(pkg_list)
@ -1340,8 +1346,6 @@ def main():
for package in packages:
if package.count('=') > 1:
module.fail_json(msg="invalid package spec: %s" % package)
if latest and '=' in package:
module.fail_json(msg='version number inconsistent with state=latest: %s' % package)
if not packages:
if autoclean:

@ -86,6 +86,47 @@
state: absent
allow_unauthenticated: yes
- block:
- name: Install foo=1.0.0
apt:
name: foo=1.0.0
- name: Run version test matrix
apt:
name: foo{{ item.0 }}
default_release: '{{ item.1 }}'
state: '{{ item.2 | ternary("latest","present") }}'
check_mode: true
register: apt_result
loop:
# [filter, release, state_latest, expected]
- ["", null, false, null]
- ["", null, true, "1.0.1"]
- ["=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, "1.0.1"]
- [">=1.0.1", null, false, "1.0.1"]
- ["", "testing", false, null]
- ["", "testing", true, "2.0.1"]
- ["=2.0.0", null, false, "2.0.0"]
- [">=2.0.0", "testing", false, "2.0.1"]
- name: Validate version test matrix
assert:
that:
- (item.item.3 is not none) == (item.stdout is defined)
- item.item.3 is none or "Inst foo [1.0.0] (" + item.item.3 + " localhost [all])" in item.stdout_lines
loop: '{{ apt_result.results }}'
always:
- name: Uninstall foo
apt:
name: foo
state: absent
# https://github.com/ansible/ansible/issues/35900
- block:

@ -0,0 +1,10 @@
Section: misc
Priority: optional
Standards-Version: 2.3.3
Package: foo
Version: 2.0.0
Section: system
Maintainer: John Doe <john@doe.com>
Architecture: all
Description: Dummy package

@ -0,0 +1,10 @@
Section: misc
Priority: optional
Standards-Version: 2.3.3
Package: foo
Version: 2.0.1
Section: system
Maintainer: John Doe <john@doe.com>
Architecture: all
Description: Dummy package

@ -10,36 +10,55 @@
- set_fact:
repodir: /tmp/repo/
- name: Create repo dir
- name: Create repo dirs
file:
path: "{{ repodir }}"
path: "{{ repodir }}/dists/{{ item }}/main/binary-all"
state: directory
mode: 0755
loop:
- stable
- testing
- name: Copy package specs to remote
copy:
src: "{{ item }}"
dest: "{{ remote_tmp_dir }}/{{ item | basename }}"
with_fileglob:
- "files/package_specs/*"
src: package_specs
dest: "{{ remote_tmp_dir }}"
- name: Create deb files
shell: "equivs-build {{ remote_tmp_dir }}/{{ item | basename }}"
shell: "find {{ remote_tmp_dir }}/package_specs/{{ item }} -type f -exec equivs-build {} \\;"
args:
chdir: "{{ repodir }}"
with_fileglob:
- "files/package_specs/*"
chdir: "{{ repodir }}/dists/{{ item }}/main/binary-all"
loop:
- stable
- testing
- name: Create repo
shell: dpkg-scanpackages --multiversion . /dev/null | gzip -9c > Packages.gz
- name: Create repo Packages
shell: dpkg-scanpackages --multiversion . /dev/null dists/{{ item }}/main/binary-all/ | gzip -9c > Packages.gz
args:
chdir: "{{ repodir }}"
chdir: "{{ repodir }}/dists/{{ item }}/main/binary-all"
loop:
- stable
- testing
# Can't use apt_repository as it doesn't expose a trusted=yes option
- name: Install the repo
- name: Create repo Release
copy:
content: deb [trusted=yes] file:{{ repodir }} ./
dest: /etc/apt/sources.list.d/file_tmp_repo.list
content: |
Codename: {{ item.0 }}
{% for k,v in item.1.items() %}
{{ k }}: {{ v }}
{% endfor %}
dest: "{{ repodir }}/dists/{{ item.0 }}/Release"
loop:
- [stable, {}]
- [testing, {NotAutomatic: "yes", ButAutomaticUpgrades: "yes"}]
- name: Install the repo
apt_repository:
repo: deb [trusted=yes arch=all] file:{{ repodir }} {{ item }} main
update_cache: false # interferes with task 'Test update_cache 1'
loop:
- stable
- testing
# Need to uncomment the deb-src for the universe component for build-dep state
- name: Ensure deb-src for the universe component

Loading…
Cancel
Save