1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

pacman: re-adding support for URL based pkgs (#4286)

* pacman: re-adding support for URL based pkgs

* Update plugins/modules/packaging/os/pacman.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/packaging/os/pacman.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* cmd=cmd in every call to self.fail()

* pacman: integration test for mixed pkg sources

* Add more tests + fix minor bug with URL packages

Version checking for URL packages is left to pacman, so add a check
after the dry run to see if it would actually install anything.

* remove double templating

Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
Jean Raby 2022-03-01 00:03:18 -05:00 committed by GitHub
parent 9a34e97702
commit a9db4742fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 300 additions and 46 deletions

View file

@ -0,0 +1,3 @@
bugfixes:
- pacman - fix URL based package installation
(https://github.com/ansible-collections/community.general/pull/4286, https://github.com/ansible-collections/community.general/issues/4285).

View file

@ -190,7 +190,22 @@ from ansible.module_utils.basic import AnsibleModule
from collections import defaultdict, namedtuple from collections import defaultdict, namedtuple
Package = namedtuple("Package", ["name", "source"]) class Package(object):
def __init__(self, name, source, source_is_URL=False):
self.name = name
self.source = source
self.source_is_URL = source_is_URL
def __eq__(self, o):
return self.name == o.name and self.source == o.source and self.source_is_URL == o.source_is_URL
def __lt__(self, o):
return self.name < o.name
def __repr__(self):
return 'Package("%s", "%s", %s)' % (self.name, self.source, self.source_is_URL)
VersionTuple = namedtuple("VersionTuple", ["current", "latest"]) VersionTuple = namedtuple("VersionTuple", ["current", "latest"])
@ -273,7 +288,13 @@ class Pacman(object):
def install_packages(self, pkgs): def install_packages(self, pkgs):
pkgs_to_install = [] pkgs_to_install = []
pkgs_to_install_from_url = []
for p in pkgs: for p in pkgs:
if p.source_is_URL:
# URL packages bypass the latest / upgradable_pkgs test
# They go through the dry-run to let pacman decide if they will be installed
pkgs_to_install_from_url.append(p)
continue
if ( if (
p.name not in self.inventory["installed_pkgs"] p.name not in self.inventory["installed_pkgs"]
or self.target_state == "latest" or self.target_state == "latest"
@ -281,14 +302,12 @@ class Pacman(object):
): ):
pkgs_to_install.append(p) pkgs_to_install.append(p)
if len(pkgs_to_install) == 0: if len(pkgs_to_install) == 0 and len(pkgs_to_install_from_url) == 0:
self.add_exit_infos("package(s) already installed") self.add_exit_infos("package(s) already installed")
return return
self.changed = True
cmd_base = [ cmd_base = [
self.pacman_path, self.pacman_path,
"--sync",
"--noconfirm", "--noconfirm",
"--noprogressbar", "--noprogressbar",
"--needed", "--needed",
@ -296,45 +315,78 @@ class Pacman(object):
if self.m.params["extra_args"]: if self.m.params["extra_args"]:
cmd_base.extend(self.m.params["extra_args"]) cmd_base.extend(self.m.params["extra_args"])
# Dry run first to gather what will be done def _build_install_diff(pacman_verb, pkglist):
cmd = cmd_base + ["--print-format", "%n %v"] + [p.source for p in pkgs_to_install] # Dry run to build the installation diff
rc, stdout, stderr = self.m.run_command(cmd, check_rc=False)
if rc != 0: cmd = cmd_base + [pacman_verb, "--print-format", "%n %v"] + [p.source for p in pkglist]
self.fail("Failed to list package(s) to install", stdout=stdout, stderr=stderr) rc, stdout, stderr = self.m.run_command(cmd, check_rc=False)
if rc != 0:
self.fail("Failed to list package(s) to install", cmd=cmd, stdout=stdout, stderr=stderr)
name_ver = [l.strip() for l in stdout.splitlines()]
before = []
after = []
to_be_installed = []
for p in name_ver:
# With Pacman v6.0.1 - libalpm v13.0.1, --upgrade outputs "loading packages..." on stdout. strip that.
# When installing from URLs, pacman can also output a 'nothing to do' message. strip that too.
if "loading packages" in p or 'there is nothing to do' in p:
continue
name, version = p.split()
if name in self.inventory["installed_pkgs"]:
before.append("%s-%s" % (name, self.inventory["installed_pkgs"][name]))
after.append("%s-%s" % (name, version))
to_be_installed.append(name)
return (to_be_installed, before, after)
name_ver = [l.strip() for l in stdout.splitlines()]
before = [] before = []
after = [] after = []
installed_pkgs = [] installed_pkgs = []
self.exit_params["packages"] = []
for p in name_ver: if pkgs_to_install:
name, version = p.split() p, b, a = _build_install_diff("--sync", pkgs_to_install)
if name in self.inventory["installed_pkgs"]: installed_pkgs.extend(p)
before.append("%s-%s" % (name, self.inventory["installed_pkgs"][name])) before.extend(b)
after.append("%s-%s" % (name, version)) after.extend(a)
installed_pkgs.append(name) if pkgs_to_install_from_url:
p, b, a = _build_install_diff("--upgrade", pkgs_to_install_from_url)
installed_pkgs.extend(p)
before.extend(b)
after.extend(a)
if len(installed_pkgs) == 0:
# This can happen with URL packages if pacman decides there's nothing to do
self.add_exit_infos("package(s) already installed")
return
self.changed = True
self.exit_params["diff"] = { self.exit_params["diff"] = {
"before": "\n".join(before) + "\n" if before else "", "before": "\n".join(sorted(before)) + "\n" if before else "",
"after": "\n".join(after) + "\n" if after else "", "after": "\n".join(sorted(after)) + "\n" if after else "",
} }
if self.m.check_mode: if self.m.check_mode:
self.add_exit_infos("Would have installed %d packages" % len(installed_pkgs)) self.add_exit_infos("Would have installed %d packages" % len(installed_pkgs))
self.exit_params["packages"] = installed_pkgs self.exit_params["packages"] = sorted(installed_pkgs)
return return
# actually do it # actually do it
cmd = cmd_base + [p.source for p in pkgs_to_install] def _install_packages_for_real(pacman_verb, pkglist):
cmd = cmd_base + [pacman_verb] + [p.source for p in pkglist]
rc, stdout, stderr = self.m.run_command(cmd, check_rc=False)
if rc != 0:
self.fail("Failed to install package(s)", cmd=cmd, stdout=stdout, stderr=stderr)
self.add_exit_infos(stdout=stdout, stderr=stderr)
rc, stdout, stderr = self.m.run_command(cmd, check_rc=False) if pkgs_to_install:
if rc != 0: _install_packages_for_real("--sync", pkgs_to_install)
self.fail("Failed to install package(s)", stdout=stdout, stderr=stderr) if pkgs_to_install_from_url:
_install_packages_for_real("--upgrade", pkgs_to_install_from_url)
self.exit_params["packages"] = installed_pkgs self.exit_params["packages"] = installed_pkgs
self.add_exit_infos( self.add_exit_infos("Installed %d package(s)" % len(installed_pkgs))
"Installed %d package(s)" % len(installed_pkgs), stdout=stdout, stderr=stderr
)
def remove_packages(self, pkgs): def remove_packages(self, pkgs):
force_args = ["--nodeps", "--nodeps"] if self.m.params["force"] else [] force_args = ["--nodeps", "--nodeps"] if self.m.params["force"] else []
@ -362,7 +414,7 @@ class Pacman(object):
rc, stdout, stderr = self.m.run_command(cmd, check_rc=False) rc, stdout, stderr = self.m.run_command(cmd, check_rc=False)
if rc != 0: if rc != 0:
self.fail("failed to list package(s) to remove", stdout=stdout, stderr=stderr) self.fail("failed to list package(s) to remove", cmd=cmd, stdout=stdout, stderr=stderr)
removed_pkgs = stdout.split() removed_pkgs = stdout.split()
self.exit_params["packages"] = removed_pkgs self.exit_params["packages"] = removed_pkgs
@ -381,7 +433,7 @@ class Pacman(object):
rc, stdout, stderr = self.m.run_command(cmd, check_rc=False) rc, stdout, stderr = self.m.run_command(cmd, check_rc=False)
if rc != 0: if rc != 0:
self.fail("failed to remove package(s)", stdout=stdout, stderr=stderr) self.fail("failed to remove package(s)", cmd=cmd, stdout=stdout, stderr=stderr)
self.exit_params["packages"] = removed_pkgs self.exit_params["packages"] = removed_pkgs
self.add_exit_infos("Removed %d package(s)" % len(removed_pkgs), stdout=stdout, stderr=stderr) self.add_exit_infos("Removed %d package(s)" % len(removed_pkgs), stdout=stdout, stderr=stderr)
@ -420,7 +472,7 @@ class Pacman(object):
if rc == 0: if rc == 0:
self.add_exit_infos("System upgraded", stdout=stdout, stderr=stderr) self.add_exit_infos("System upgraded", stdout=stdout, stderr=stderr)
else: else:
self.fail("Could not upgrade", stdout=stdout, stderr=stderr) self.fail("Could not upgrade", cmd=cmd, stdout=stdout, stderr=stderr)
def update_package_db(self): def update_package_db(self):
"""runs pacman --sync --refresh""" """runs pacman --sync --refresh"""
@ -446,7 +498,7 @@ class Pacman(object):
if rc == 0: if rc == 0:
self.add_exit_infos("Updated package db", stdout=stdout, stderr=stderr) self.add_exit_infos("Updated package db", stdout=stdout, stderr=stderr)
else: else:
self.fail("could not update package db", stdout=stdout, stderr=stderr) self.fail("could not update package db", cmd=cmd, stdout=stdout, stderr=stderr)
def package_list(self): def package_list(self):
"""Takes the input package list and resolves packages groups to their package list using the inventory, """Takes the input package list and resolves packages groups to their package list using the inventory,
@ -459,6 +511,7 @@ class Pacman(object):
if not pkg: if not pkg:
continue continue
is_URL = False
if pkg in self.inventory["available_groups"]: if pkg in self.inventory["available_groups"]:
# Expand group members # Expand group members
for group_member in self.inventory["available_groups"][pkg]: for group_member in self.inventory["available_groups"][pkg]:
@ -488,8 +541,11 @@ class Pacman(object):
stderr=stderr, stderr=stderr,
rc=rc, rc=rc,
) )
# With Pacman v6.0.1 - libalpm v13.0.1, --upgrade outputs "loading packages..." on stdout. strip that
stdout = stdout.replace("loading packages...\n", "")
is_URL = True
pkg_name = stdout.strip() pkg_name = stdout.strip()
pkg_list.append(Package(name=pkg_name, source=pkg)) pkg_list.append(Package(name=pkg_name, source=pkg, source_is_URL=is_URL))
return pkg_list return pkg_list

View file

@ -8,3 +8,4 @@
block: block:
# Add more tests here by including more task files: # Add more tests here by including more task files:
- include: 'basic.yml' - include: 'basic.yml'
- include: 'package_urls.yml'

View file

@ -0,0 +1,125 @@
---
- vars:
reg_pkg: ed
url_pkg: lemon
file_pkg: hdparm
file_pkg_path: /tmp/pkg.zst
extra_pkg: core/sdparm
extra_pkg_outfmt: sdparm
block:
- name: Make sure that test packages are not installed
pacman:
name:
- '{{reg_pkg}}'
- '{{url_pkg}}'
- '{{file_pkg}}'
- '{{extra_pkg}}'
state: absent
- name: Get URL for {{url_pkg}}
command:
cmd: pacman --sync --print-format "%l" {{url_pkg}}
register: url_pkg_url
- name: Get URL for {{file_pkg}}
command:
cmd: pacman --sync --print-format "%l" {{file_pkg}}
register: file_pkg_url
- name: Download {{file_pkg}} pkg
get_url:
url: '{{file_pkg_url.stdout}}'
dest: '{{file_pkg_path}}'
- name: Install packages from mixed sources (check mode)
pacman:
name:
- '{{reg_pkg}}'
- '{{url_pkg_url.stdout}}'
- '{{file_pkg_path}}'
check_mode: True
register: install_1
- name: Install packages from mixed sources
pacman:
name:
- '{{reg_pkg}}'
- '{{url_pkg_url.stdout}}'
- '{{file_pkg_path}}'
register: install_2
- name: Install packages from mixed sources - (idempotency)
pacman:
name:
- '{{reg_pkg}}'
- '{{url_pkg_url.stdout}}'
- '{{file_pkg_path}}'
register: install_3
- name: Install packages with their regular names (idempotency)
pacman:
name:
- '{{reg_pkg}}'
- '{{url_pkg}}'
- '{{file_pkg}}'
register: install_4
- name: Install new package with already installed packages from mixed sources
pacman:
name:
- '{{reg_pkg}}'
- '{{url_pkg_url.stdout}}'
- '{{file_pkg_path}}'
- '{{extra_pkg}}'
register: install_5
- name: Uninstall packages - mixed sources (check mode)
pacman:
state: absent
name:
- '{{reg_pkg}}'
- '{{url_pkg_url.stdout}}'
- '{{file_pkg_path}}'
check_mode: True
register: uninstall_1
- name: Uninstall packages - mixed sources
pacman:
state: absent
name:
- '{{reg_pkg}}'
- '{{url_pkg_url.stdout}}'
- '{{file_pkg_path}}'
register: uninstall_2
- name: Uninstall packages - mixed sources (idempotency)
pacman:
state: absent
name:
- '{{reg_pkg}}'
- '{{url_pkg_url.stdout}}'
- '{{file_pkg_path}}'
register: uninstall_3
- assert:
that:
- install_1 is changed
- install_1.msg == 'Would have installed 3 packages'
- install_1.packages|sort() == [reg_pkg, url_pkg, file_pkg]|sort()
- install_2 is changed
- install_2.msg == 'Installed 3 package(s)'
- install_1.packages|sort() == [reg_pkg, url_pkg, file_pkg]|sort()
- install_3 is not changed
- install_3.msg == 'package(s) already installed'
- install_4 is not changed
- install_4.msg == 'package(s) already installed'
- install_5 is changed
- install_5.msg == 'Installed 1 package(s)'
- install_5.packages == [extra_pkg_outfmt]
- uninstall_1 is changed
- uninstall_1.msg == 'Would have removed 3 packages'
- uninstall_1.packages | length() == 3 # pkgs have versions here
- uninstall_2 is changed
- uninstall_2.msg == 'Removed 3 package(s)'
- uninstall_2.packages | length() == 3
- uninstall_3 is not changed
- uninstall_3.msg == 'package(s) already absent'

View file

@ -475,7 +475,13 @@ class TestPacman:
# catch all -> call to pacman to resolve (--sync and --upgrade) # catch all -> call to pacman to resolve (--sync and --upgrade)
"present", "present",
["somepackage-12.3-x86_64.pkg.tar.zst"], ["somepackage-12.3-x86_64.pkg.tar.zst"],
[Package(name="somepackage", source="somepackage-12.3-x86_64.pkg.tar.zst")], [
Package(
name="somepackage",
source="somepackage-12.3-x86_64.pkg.tar.zst",
source_is_URL=True,
)
],
{ {
"calls": [ "calls": [
mock.call( mock.call(
@ -582,12 +588,13 @@ class TestPacman:
self.mock_run_command.call_count == 0 self.mock_run_command.call_count == 0
@pytest.mark.parametrize( @pytest.mark.parametrize(
"module_args, expected_packages, run_command_data, raises", "module_args, expected_packages, package_list_out, run_command_data, raises",
[ [
( (
# remove pkg: Check mode -- call to print format but that's it # remove pkg: Check mode -- call to print format but that's it
{"_ansible_check_mode": True, "name": ["grep"], "state": "absent"}, {"_ansible_check_mode": True, "name": ["grep"], "state": "absent"},
["grep-version"], ["grep-version"],
[Package("grep", "grep")],
{ {
"calls": [ "calls": [
mock.call( mock.call(
@ -612,6 +619,7 @@ class TestPacman:
# remove pkg for real now -- with 2 packages # remove pkg for real now -- with 2 packages
{"name": ["grep", "gawk"], "state": "absent"}, {"name": ["grep", "gawk"], "state": "absent"},
["grep-version", "gawk-anotherversion"], ["grep-version", "gawk-anotherversion"],
[Package("grep", "grep"), Package("gawk", "gawk")],
{ {
"calls": [ "calls": [
mock.call( mock.call(
@ -650,6 +658,7 @@ class TestPacman:
"extra_args": "--some --extra arg", "extra_args": "--some --extra arg",
}, },
["grep-version"], ["grep-version"],
[Package("grep", "grep")],
{ {
"calls": [ "calls": [
mock.call( mock.call(
@ -698,6 +707,7 @@ class TestPacman:
# remove pkg -- Failure to list # remove pkg -- Failure to list
{"name": ["grep"], "state": "absent"}, {"name": ["grep"], "state": "absent"},
["grep-3.7-1"], ["grep-3.7-1"],
[Package("grep", "grep")],
{ {
"calls": [ "calls": [
mock.call( mock.call(
@ -724,6 +734,7 @@ class TestPacman:
# remove pkg -- Failure to remove # remove pkg -- Failure to remove
{"name": ["grep"], "state": "absent"}, {"name": ["grep"], "state": "absent"},
["grep-3.7-1"], ["grep-3.7-1"],
[Package("grep", "grep")],
{ {
"calls": [ "calls": [
mock.call( mock.call(
@ -756,16 +767,17 @@ class TestPacman:
# install pkg: Check mode # install pkg: Check mode
{"_ansible_check_mode": True, "name": ["sudo"], "state": "present"}, {"_ansible_check_mode": True, "name": ["sudo"], "state": "present"},
["sudo"], ["sudo"],
[Package("sudo", "sudo")],
{ {
"calls": [ "calls": [
mock.call( mock.call(
mock.ANY, mock.ANY,
[ [
"pacman", "pacman",
"--sync",
"--noconfirm", "--noconfirm",
"--noprogressbar", "--noprogressbar",
"--needed", "--needed",
"--sync",
"--print-format", "--print-format",
"%n %v", "%n %v",
"sudo", "sudo",
@ -778,19 +790,37 @@ class TestPacman:
AnsibleExitJson, AnsibleExitJson,
), ),
( (
# install 2 pkgs, one already present # Install pkgs: one regular, one already installed, one file URL and one https URL
{"name": ["sudo", "grep"], "state": "present"}, {
["sudo"], "name": [
"sudo",
"grep",
"./somepackage-12.3-x86_64.pkg.tar.zst",
"http://example.com/otherpkg-1.2-x86_64.pkg.tar.zst",
],
"state": "present",
},
["sudo", "somepackage", "otherpkg"],
[
Package("sudo", "sudo"),
Package("grep", "grep"),
Package("somepackage", "./somepackage-12.3-x86_64.pkg.tar.zst", source_is_URL=True),
Package(
"otherpkg",
"http://example.com/otherpkg-1.2-x86_64.pkg.tar.zst",
source_is_URL=True,
),
],
{ {
"calls": [ "calls": [
mock.call( mock.call(
mock.ANY, mock.ANY,
[ [
"pacman", "pacman",
"--sync",
"--noconfirm", "--noconfirm",
"--noprogressbar", "--noprogressbar",
"--needed", "--needed",
"--sync",
"--print-format", "--print-format",
"%n %v", "%n %v",
"sudo", "sudo",
@ -801,16 +831,49 @@ class TestPacman:
mock.ANY, mock.ANY,
[ [
"pacman", "pacman",
"--sync",
"--noconfirm", "--noconfirm",
"--noprogressbar", "--noprogressbar",
"--needed", "--needed",
"--upgrade",
"--print-format",
"%n %v",
"./somepackage-12.3-x86_64.pkg.tar.zst",
"http://example.com/otherpkg-1.2-x86_64.pkg.tar.zst",
],
check_rc=False,
),
mock.call(
mock.ANY,
[
"pacman",
"--noconfirm",
"--noprogressbar",
"--needed",
"--sync",
"sudo", "sudo",
], ],
check_rc=False, check_rc=False,
), ),
mock.call(
mock.ANY,
[
"pacman",
"--noconfirm",
"--noprogressbar",
"--needed",
"--upgrade",
"./somepackage-12.3-x86_64.pkg.tar.zst",
"http://example.com/otherpkg-1.2-x86_64.pkg.tar.zst",
],
check_rc=False,
),
],
"side_effect": [
(0, "sudo version", ""),
(0, "somepackage 12.3\notherpkg 1.2", ""),
(0, "", ""),
(0, "", ""),
], ],
"side_effect": [(0, "sudo version", ""), (0, "", "")],
}, },
AnsibleExitJson, AnsibleExitJson,
), ),
@ -818,19 +881,20 @@ class TestPacman:
# install pkg, extra_args # install pkg, extra_args
{"name": ["sudo"], "state": "present", "extra_args": "--some --thing else"}, {"name": ["sudo"], "state": "present", "extra_args": "--some --thing else"},
["sudo"], ["sudo"],
[Package("sudo", "sudo")],
{ {
"calls": [ "calls": [
mock.call( mock.call(
mock.ANY, mock.ANY,
[ [
"pacman", "pacman",
"--sync",
"--noconfirm", "--noconfirm",
"--noprogressbar", "--noprogressbar",
"--needed", "--needed",
"--some", "--some",
"--thing", "--thing",
"else", "else",
"--sync",
"--print-format", "--print-format",
"%n %v", "%n %v",
"sudo", "sudo",
@ -841,13 +905,13 @@ class TestPacman:
mock.ANY, mock.ANY,
[ [
"pacman", "pacman",
"--sync",
"--noconfirm", "--noconfirm",
"--noprogressbar", "--noprogressbar",
"--needed", "--needed",
"--some", "--some",
"--thing", "--thing",
"else", "else",
"--sync",
"sudo", "sudo",
], ],
check_rc=False, check_rc=False,
@ -861,16 +925,17 @@ class TestPacman:
# latest pkg: Check mode # latest pkg: Check mode
{"_ansible_check_mode": True, "name": ["sqlite"], "state": "latest"}, {"_ansible_check_mode": True, "name": ["sqlite"], "state": "latest"},
["sqlite"], ["sqlite"],
[Package("sqlite", "sqlite")],
{ {
"calls": [ "calls": [
mock.call( mock.call(
mock.ANY, mock.ANY,
[ [
"pacman", "pacman",
"--sync",
"--noconfirm", "--noconfirm",
"--noprogressbar", "--noprogressbar",
"--needed", "--needed",
"--sync",
"--print-format", "--print-format",
"%n %v", "%n %v",
"sqlite", "sqlite",
@ -886,16 +951,17 @@ class TestPacman:
# latest pkg -- one already latest # latest pkg -- one already latest
{"name": ["sqlite", "grep"], "state": "latest"}, {"name": ["sqlite", "grep"], "state": "latest"},
["sqlite"], ["sqlite"],
[Package("sqlite", "sqlite")],
{ {
"calls": [ "calls": [
mock.call( mock.call(
mock.ANY, mock.ANY,
[ [
"pacman", "pacman",
"--sync",
"--noconfirm", "--noconfirm",
"--noprogressbar", "--noprogressbar",
"--needed", "--needed",
"--sync",
"--print-format", "--print-format",
"%n %v", "%n %v",
"sqlite", "sqlite",
@ -906,10 +972,10 @@ class TestPacman:
mock.ANY, mock.ANY,
[ [
"pacman", "pacman",
"--sync",
"--noconfirm", "--noconfirm",
"--noprogressbar", "--noprogressbar",
"--needed", "--needed",
"--sync",
"sqlite", "sqlite",
], ],
check_rc=False, check_rc=False,
@ -924,13 +990,16 @@ class TestPacman:
def test_op_packages( def test_op_packages(
self, self,
mock_valid_inventory, mock_valid_inventory,
mock_package_list,
module_args, module_args,
expected_packages, expected_packages,
package_list_out,
run_command_data, run_command_data,
raises, raises,
): ):
set_module_args(module_args) set_module_args(module_args)
self.mock_run_command.side_effect = run_command_data["side_effect"] self.mock_run_command.side_effect = run_command_data["side_effect"]
mock_package_list.return_value = package_list_out
P = pacman.Pacman(pacman.setup_module()) P = pacman.Pacman(pacman.setup_module())
with pytest.raises(raises) as e: with pytest.raises(raises) as e: