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 4 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: options:
name: name:
description: 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. Name wildcards (fnmatch) like C(apt*) and version wildcards like C(foo=1.0*) are also supported.
aliases: [ package, pkg ] aliases: [ package, pkg ]
type: list type: list
@ -454,24 +454,10 @@ class PolicyRcD(object):
def package_split(pkgspec): def package_split(pkgspec):
parts = pkgspec.split('=', 1) parts = re.split(r'(>?=)', pkgspec, 1)
version = None
if len(parts) > 1: if len(parts) > 1:
version = parts[1] return parts
return parts[0], version return parts[0], None, None
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
def package_version_compare(version, other_version): 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) 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: 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
@ -495,20 +500,21 @@ def package_status(m, pkgname, version, cache, state):
provided_packages = cache.get_providing_packages(pkgname) provided_packages = cache.get_providing_packages(pkgname)
if provided_packages: if provided_packages:
is_installed = False is_installed = False
upgradable = False version_installable = None
version_ok = False version_ok = False
# when virtual package providing only one package, look up status of target package # when virtual package providing only one package, look up status of target package
if cache.is_virtual_package(pkgname) and len(provided_packages) == 1: if cache.is_virtual_package(pkgname) and len(provided_packages) == 1:
package = provided_packages[0] 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: if installed:
is_installed = True 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) 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 upgradable and let apt-get install deal with it # mark as not installed and let apt-get install deal with it
return False, False, True, False return False, False, None, False
else: else:
return False, False, False, False return False, False, False, False
try: try:
@ -528,36 +534,29 @@ def package_status(m, pkgname, version, cache, state):
# assume older version of python-apt is installed # assume older version of python-apt is installed
package_is_installed = pkg.isInstalled package_is_installed = pkg.isInstalled
version_is_installed = package_is_installed version_best = package_best_match(pkgname, version_cmp, version, default_release, cache._cache)
if version: version_is_installed = False
versions = package_versions(pkgname, pkg, cache._cache) version_installable = None
avail_upgrades = fnmatch.filter(versions, version) if package_is_installed:
try:
if package_is_installed: installed_version = pkg.installed.version
try: except AttributeError:
installed_version = pkg.installed.version installed_version = pkg.installedVersion
except AttributeError:
installed_version = pkg.installedVersion
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)
elif version_cmp == ">=":
# Only claim the package is upgradable if a candidate matches the version version_is_installed = apt_pkg.version_compare(installed_version, version) >= 0
package_is_upgradable = False
for candidate in avail_upgrades:
if package_version_compare(candidate, installed_version) > 0:
package_is_upgradable = True
break
else: else:
package_is_upgradable = bool(avail_upgrades) version_is_installed = True
if installed_version != version_best:
version_installable = version_best
else: else:
try: version_installable = version_best
package_is_upgradable = pkg.is_upgradable
except AttributeError:
# assume older version of python-apt is installed
package_is_upgradable = pkg.isUpgradable
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): def expand_dpkg_options(dpkg_options_compressed):
@ -581,7 +580,7 @@ def expand_pkgspec_from_fnmatches(m, pkgspec, cache):
new_pkgspec = [] new_pkgspec = []
if pkgspec: if pkgspec:
for pkgspec_pattern in 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 # note that none of these chars is allowed in a (debian) pkgname
if frozenset('*?[]!').intersection(pkgname_pattern): if frozenset('*?[]!').intersection(pkgname_pattern):
@ -671,19 +670,26 @@ def install(m, pkgspec, cache, upgrade=False, default_release=None,
pkg_list.append("'%s'" % package) pkg_list.append("'%s'" % package)
continue continue
name, version = package_split(package) name, version_cmp, version = package_split(package)
package_names.append(name) package_names.append(name)
installed, installed_version, upgradable, has_files = package_status(m, name, version, 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 upgradable): if (not installed and not only_upgrade) or (installed and not installed_version) or (upgrade and version_installable):
pkg_list.append("'%s'" % package) if version_installable or version:
if installed_version and upgradable and 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 # 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 # We do not apply the upgrade flag because we cannot specify both
# a version and state=latest. (This behaviour mirrors how apt # a version and state=latest. (This behaviour mirrors how apt
# treats a version with wildcard in the package) # 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) packages = ' '.join(pkg_list)
if packages: if packages:
@ -877,8 +883,8 @@ def remove(m, pkgspec, cache, purge=False, force=False,
pkg_list = [] pkg_list = []
pkgspec = expand_pkgspec_from_fnmatches(m, pkgspec, cache) pkgspec = expand_pkgspec_from_fnmatches(m, pkgspec, cache)
for package in pkgspec: for package in pkgspec:
name, version = package_split(package) name, version_cmp, version = package_split(package)
installed, installed_version, upgradable, has_files = package_status(m, name, version, cache, state='remove') 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): if installed_version or (has_files and purge):
pkg_list.append("'%s'" % package) pkg_list.append("'%s'" % package)
packages = ' '.join(pkg_list) packages = ' '.join(pkg_list)
@ -1340,8 +1346,6 @@ def main():
for package in packages: for package in packages:
if package.count('=') > 1: if package.count('=') > 1:
module.fail_json(msg="invalid package spec: %s" % package) 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 not packages:
if autoclean: if autoclean:

@ -86,6 +86,47 @@
state: absent state: absent
allow_unauthenticated: yes 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 # https://github.com/ansible/ansible/issues/35900
- block: - 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: - set_fact:
repodir: /tmp/repo/ repodir: /tmp/repo/
- name: Create repo dir - name: Create repo dirs
file: file:
path: "{{ repodir }}" path: "{{ repodir }}/dists/{{ item }}/main/binary-all"
state: directory state: directory
mode: 0755 mode: 0755
loop:
- stable
- testing
- name: Copy package specs to remote - name: Copy package specs to remote
copy: copy:
src: "{{ item }}" src: package_specs
dest: "{{ remote_tmp_dir }}/{{ item | basename }}" dest: "{{ remote_tmp_dir }}"
with_fileglob:
- "files/package_specs/*"
- name: Create deb files - 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: args:
chdir: "{{ repodir }}" chdir: "{{ repodir }}/dists/{{ item }}/main/binary-all"
with_fileglob: loop:
- "files/package_specs/*" - stable
- testing
- name: Create repo - name: Create repo Packages
shell: dpkg-scanpackages --multiversion . /dev/null | gzip -9c > Packages.gz shell: dpkg-scanpackages --multiversion . /dev/null dists/{{ item }}/main/binary-all/ | gzip -9c > Packages.gz
args: 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: Create repo Release
- name: Install the repo
copy: copy:
content: deb [trusted=yes] file:{{ repodir }} ./ content: |
dest: /etc/apt/sources.list.d/file_tmp_repo.list 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 # Need to uncomment the deb-src for the universe component for build-dep state
- name: Ensure deb-src for the universe component - name: Ensure deb-src for the universe component

Loading…
Cancel
Save