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

Pacman: Add support for install reason (#4956)

* Pacman: Add support for setting install reason

* Improved description

* Fix documentation

* Add changelog fragment

* Use source for installation

* Get all reasons at once

* Removed default for reason

* Added version info to documentation

* Fix NameError

* Moved determination of reason to _build_inventory

* Fix duplication and sanity errors

* adjust tests for changed inventory

* Documentation: remove empty default for reason

* mention packages with changed reason in exit params/info

* Added integration tests for reason and reason_for

Inspired by the integration tests for url packages

* Correct indentation

* Fix indentation

* Also sort changed packages in normal mode

* Also sort result in unit test

* Apply suggestions from code review

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

Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
Minei3oat 2022-07-31 22:10:49 +02:00 committed by GitHub
parent d214f49be7
commit 9f3841703f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 230 additions and 9 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- pacman - added parameters ``reason`` and ``reason_for`` to set/change the install reason of packages (https://github.com/ansible-collections/community.general/pull/4956).

View file

@ -104,6 +104,22 @@ options:
default: default:
type: str type: str
reason:
description:
- The install reason to set for the packages.
choices: [ dependency, explicit ]
type: str
version_added: 5.4.0
reason_for:
description:
- Set the install reason for C(all) packages or only for C(new) packages.
- In case of C(state=latest) already installed packages which will be updated to a newer version are not counted as C(new).
default: new
choices: [ all, new ]
type: str
version_added: 5.4.0
notes: notes:
- When used with a C(loop:) each package will be processed individually, - 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. it is much more efficient to pass the list directly to the I(name) option.
@ -223,6 +239,20 @@ EXAMPLES = """
name: baz name: baz
state: absent state: absent
force: yes force: yes
- name: Install foo as dependency and leave reason untouched if already installed
community.general.pacman:
name: foo
state: present
reason: dependency
reason_for: new
- name: Run the equivalent of "pacman -S --asexplicit", mark foo as explicit and install it if not present
community.general.pacman:
name: foo
state: present
reason: explicit
reason_for: all
""" """
import shlex import shlex
@ -331,7 +361,14 @@ class Pacman(object):
def install_packages(self, pkgs): def install_packages(self, pkgs):
pkgs_to_install = [] pkgs_to_install = []
pkgs_to_install_from_url = [] pkgs_to_install_from_url = []
pkgs_to_set_reason = []
for p in pkgs: for p in pkgs:
if self.m.params["reason"] and (
p.name not in self.inventory["pkg_reasons"]
or self.m.params["reason_for"] == "all"
and self.inventory["pkg_reasons"][p.name] != self.m.params["reason"]
):
pkgs_to_set_reason.append(p.name)
if p.source_is_URL: if p.source_is_URL:
# URL packages bypass the latest / upgradable_pkgs test # URL packages bypass the latest / upgradable_pkgs test
# They go through the dry-run to let pacman decide if they will be installed # They go through the dry-run to let pacman decide if they will be installed
@ -344,7 +381,7 @@ class Pacman(object):
): ):
pkgs_to_install.append(p) pkgs_to_install.append(p)
if len(pkgs_to_install) == 0 and len(pkgs_to_install_from_url) == 0: if len(pkgs_to_install) == 0 and len(pkgs_to_install_from_url) == 0 and len(pkgs_to_set_reason) == 0:
self.exit_params["packages"] = [] self.exit_params["packages"] = []
self.add_exit_infos("package(s) already installed") self.add_exit_infos("package(s) already installed")
return return
@ -377,8 +414,13 @@ class Pacman(object):
continue continue
name, version = p.split() name, version = p.split()
if name in self.inventory["installed_pkgs"]: if name in self.inventory["installed_pkgs"]:
before.append("%s-%s" % (name, self.inventory["installed_pkgs"][name])) before.append("%s-%s-%s" % (name, self.inventory["installed_pkgs"][name], self.inventory["pkg_reasons"][name]))
after.append("%s-%s" % (name, version)) if name in pkgs_to_set_reason:
after.append("%s-%s-%s" % (name, version, self.m.params["reason"]))
elif name in self.inventory["pkg_reasons"]:
after.append("%s-%s-%s" % (name, version, self.inventory["pkg_reasons"][name]))
else:
after.append("%s-%s" % (name, version))
to_be_installed.append(name) to_be_installed.append(name)
return (to_be_installed, before, after) return (to_be_installed, before, after)
@ -398,7 +440,7 @@ class Pacman(object):
before.extend(b) before.extend(b)
after.extend(a) after.extend(a)
if len(installed_pkgs) == 0: if len(installed_pkgs) == 0 and len(pkgs_to_set_reason) == 0:
# This can happen with URL packages if pacman decides there's nothing to do # This can happen with URL packages if pacman decides there's nothing to do
self.exit_params["packages"] = [] self.exit_params["packages"] = []
self.add_exit_infos("package(s) already installed") self.add_exit_infos("package(s) already installed")
@ -411,9 +453,11 @@ class Pacman(object):
"after": "\n".join(sorted(after)) + "\n" if after else "", "after": "\n".join(sorted(after)) + "\n" if after else "",
} }
changed_reason_pkgs = [p for p in pkgs_to_set_reason if p not in installed_pkgs]
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) + len(changed_reason_pkgs)))
self.exit_params["packages"] = sorted(installed_pkgs) self.exit_params["packages"] = sorted(installed_pkgs + changed_reason_pkgs)
return return
# actually do it # actually do it
@ -430,8 +474,22 @@ class Pacman(object):
if pkgs_to_install_from_url: if pkgs_to_install_from_url:
_install_packages_for_real("--upgrade", pkgs_to_install_from_url) _install_packages_for_real("--upgrade", pkgs_to_install_from_url)
self.exit_params["packages"] = installed_pkgs # set reason
self.add_exit_infos("Installed %d package(s)" % len(installed_pkgs)) if pkgs_to_set_reason:
cmd = [self.pacman_path, "--noconfirm", "--database"]
if self.m.params["reason"] == "dependency":
cmd.append("--asdeps")
else:
cmd.append("--asexplicit")
cmd.extend(pkgs_to_set_reason)
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)
self.exit_params["packages"] = sorted(installed_pkgs + changed_reason_pkgs)
self.add_exit_infos("Installed %d package(s)" % (len(installed_pkgs) + len(changed_reason_pkgs)))
def remove_packages(self, pkgs): def remove_packages(self, pkgs):
# filter out pkgs that are already absent # filter out pkgs that are already absent
@ -631,6 +689,7 @@ class Pacman(object):
"available_pkgs": {pkgname: version}, "available_pkgs": {pkgname: version},
"available_groups": {groupname: set(pkgnames)}, "available_groups": {groupname: set(pkgnames)},
"upgradable_pkgs": {pkgname: (current_version,latest_version)}, "upgradable_pkgs": {pkgname: (current_version,latest_version)},
"pkg_reasons": {pkgname: reason},
} }
Fails the module if a package requested for install cannot be found Fails the module if a package requested for install cannot be found
@ -723,12 +782,31 @@ class Pacman(object):
rc=rc, rc=rc,
) )
pkg_reasons = {}
dummy, stdout, dummy = self.m.run_command([self.pacman_path, "--query", "--explicit"], check_rc=True)
# Format of a line: "pacman 6.0.1-2"
for l in stdout.splitlines():
l = l.strip()
if not l:
continue
pkg = l.split()[0]
pkg_reasons[pkg] = "explicit"
dummy, stdout, dummy = self.m.run_command([self.pacman_path, "--query", "--deps"], check_rc=True)
# Format of a line: "pacman 6.0.1-2"
for l in stdout.splitlines():
l = l.strip()
if not l:
continue
pkg = l.split()[0]
pkg_reasons[pkg] = "dependency"
return dict( return dict(
installed_pkgs=installed_pkgs, installed_pkgs=installed_pkgs,
installed_groups=installed_groups, installed_groups=installed_groups,
available_pkgs=available_pkgs, available_pkgs=available_pkgs,
available_groups=available_groups, available_groups=available_groups,
upgradable_pkgs=upgradable_pkgs, upgradable_pkgs=upgradable_pkgs,
pkg_reasons=pkg_reasons,
) )
@ -749,6 +827,8 @@ def setup_module():
upgrade_extra_args=dict(type="str", default=""), upgrade_extra_args=dict(type="str", default=""),
update_cache=dict(type="bool"), update_cache=dict(type="bool"),
update_cache_extra_args=dict(type="str", default=""), update_cache_extra_args=dict(type="str", default=""),
reason=dict(type="str", choices=["explicit", "dependency"]),
reason_for=dict(type="str", default="new", choices=["new", "all"]),
), ),
required_one_of=[["name", "update_cache", "upgrade"]], required_one_of=[["name", "update_cache", "upgrade"]],
mutually_exclusive=[["name", "upgrade"]], mutually_exclusive=[["name", "upgrade"]],

View file

@ -12,3 +12,4 @@
- include: 'remove_nosave.yml' - include: 'remove_nosave.yml'
- include: 'update_cache.yml' - include: 'update_cache.yml'
- include: 'locally_installed_package.yml' - include: 'locally_installed_package.yml'
- include: 'reason.yml'

View file

@ -0,0 +1,97 @@
---
- 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 as dependency (check mode)
pacman:
name:
- '{{reg_pkg}}'
- '{{url_pkg_url.stdout}}'
- '{{file_pkg_path}}'
reason: dependency
check_mode: True
register: install_1
- name: Install packages from mixed sources as explicit
pacman:
name:
- '{{reg_pkg}}'
- '{{url_pkg_url.stdout}}'
- '{{file_pkg_path}}'
reason: explicit
register: install_2
- name: Install packages from mixed sources with new packages being installed as dependency - (idempotency)
pacman:
name:
- '{{reg_pkg}}'
- '{{url_pkg_url.stdout}}'
- '{{file_pkg_path}}'
reason: dependency
register: install_3
- name: Install new package with already installed packages from mixed sources as dependency
pacman:
name:
- '{{reg_pkg}}'
- '{{url_pkg_url.stdout}}'
- '{{file_pkg_path}}'
- '{{extra_pkg}}'
reason: dependency
register: install_4
- name: Set install reason for all packages to dependency
pacman:
name:
- '{{reg_pkg}}'
- '{{url_pkg_url.stdout}}'
- '{{file_pkg_path}}'
- '{{extra_pkg}}'
reason: dependency
reason_for: all
register: install_5
- 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_2.packages|sort() == [reg_pkg, url_pkg, file_pkg]|sort()
- install_3 is not changed
- install_3.msg == 'package(s) already installed'
- install_4 is changed
- install_4.msg == 'Installed 1 package(s)'
- install_4.packages == [extra_pkg_outfmt]
- install_5 is changed
- install_5.msg == 'Installed 3 package(s)'
- install_5.packages|sort() == [reg_pkg, url_pkg, file_pkg]|sort()

View file

@ -100,6 +100,19 @@ valid_inventory = {
"upgradable_pkgs": { "upgradable_pkgs": {
"sqlite": VersionTuple(current="3.36.0-1", latest="3.37.0-1"), "sqlite": VersionTuple(current="3.36.0-1", latest="3.37.0-1"),
}, },
"pkg_reasons": {
"file": "explicit",
"filesystem": "explicit",
"findutils": "explicit",
"gawk": "explicit",
"gettext": "explicit",
"grep": "explicit",
"gzip": "explicit",
"pacman": "explicit",
"pacman-mirrorlist": "dependency",
"sed": "explicit",
"sqlite": "explicit",
},
} }
empty_inventory = { empty_inventory = {
@ -108,6 +121,7 @@ empty_inventory = {
"installed_groups": {}, "installed_groups": {},
"available_groups": {}, "available_groups": {},
"upgradable_pkgs": {}, "upgradable_pkgs": {},
"pkg_reasons": {},
} }
@ -255,6 +269,27 @@ class TestPacman:
""", """,
"", "",
), ),
( # pacman --query --explicit
0,
"""file 5.41-1
filesystem 2021.11.11-1
findutils 4.8.0-1
gawk 5.1.1-1
gettext 0.21-1
grep 3.7-1
gzip 1.11-1
pacman 6.0.1-2
sed 4.8-1
sqlite 3.36.0-1
""",
"",
),
( # pacman --query --deps
0,
"""pacman-mirrorlist 20211114-1
""",
"",
),
], ],
None, None,
), ),
@ -272,6 +307,8 @@ class TestPacman:
"", "",
"warning: config file /etc/pacman.conf, line 34: directive 'TotalDownload' in section 'options' not recognized.", "warning: config file /etc/pacman.conf, line 34: directive 'TotalDownload' in section 'options' not recognized.",
), ),
(0, "", ""),
(0, "", ""),
], ],
None, None,
), ),
@ -288,6 +325,8 @@ class TestPacman:
"partial\npkg\\nlist", "partial\npkg\\nlist",
"some warning", "some warning",
), ),
(0, "", ""),
(0, "", ""),
], ],
AnsibleFailJson, AnsibleFailJson,
), ),
@ -375,6 +414,8 @@ class TestPacman:
(["pacman", "--query", "--groups"], {'check_rc': True}, 0, '', ''), (["pacman", "--query", "--groups"], {'check_rc': True}, 0, '', ''),
(["pacman", "--sync", "--groups", "--groups"], {'check_rc': True}, 0, '', ''), (["pacman", "--sync", "--groups", "--groups"], {'check_rc': True}, 0, '', ''),
(["pacman", "--query", "--upgrades"], {'check_rc': False}, 0, '', ''), (["pacman", "--query", "--upgrades"], {'check_rc': False}, 0, '', ''),
(["pacman", "--query", "--explicit"], {'check_rc': True}, 0, 'foo 1.0.0-1', ''),
(["pacman", "--query", "--deps"], {'check_rc': True}, 0, '', ''),
], ],
False, False,
), ),
@ -843,7 +884,7 @@ class TestPacman:
], ],
"state": "present", "state": "present",
}, },
["sudo", "somepackage", "otherpkg"], ["otherpkg", "somepackage", "sudo"],
[ [
Package("sudo", "sudo"), Package("sudo", "sudo"),
Package("grep", "grep"), Package("grep", "grep"),