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:
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:
- 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.
@ -223,6 +239,20 @@ EXAMPLES = """
name: baz
state: absent
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
@ -331,7 +361,14 @@ class Pacman(object):
def install_packages(self, pkgs):
pkgs_to_install = []
pkgs_to_install_from_url = []
pkgs_to_set_reason = []
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:
# URL packages bypass the latest / upgradable_pkgs test
# 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)
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.add_exit_infos("package(s) already installed")
return
@ -377,8 +414,13 @@ class Pacman(object):
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))
before.append("%s-%s-%s" % (name, self.inventory["installed_pkgs"][name], self.inventory["pkg_reasons"][name]))
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)
return (to_be_installed, before, after)
@ -398,7 +440,7 @@ class Pacman(object):
before.extend(b)
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
self.exit_params["packages"] = []
self.add_exit_infos("package(s) already installed")
@ -411,9 +453,11 @@ class Pacman(object):
"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:
self.add_exit_infos("Would have installed %d packages" % len(installed_pkgs))
self.exit_params["packages"] = sorted(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 + changed_reason_pkgs)
return
# actually do it
@ -430,8 +474,22 @@ class Pacman(object):
if pkgs_to_install_from_url:
_install_packages_for_real("--upgrade", pkgs_to_install_from_url)
self.exit_params["packages"] = installed_pkgs
self.add_exit_infos("Installed %d package(s)" % len(installed_pkgs))
# set reason
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):
# filter out pkgs that are already absent
@ -631,6 +689,7 @@ class Pacman(object):
"available_pkgs": {pkgname: version},
"available_groups": {groupname: set(pkgnames)},
"upgradable_pkgs": {pkgname: (current_version,latest_version)},
"pkg_reasons": {pkgname: reason},
}
Fails the module if a package requested for install cannot be found
@ -723,12 +782,31 @@ class Pacman(object):
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(
installed_pkgs=installed_pkgs,
installed_groups=installed_groups,
available_pkgs=available_pkgs,
available_groups=available_groups,
upgradable_pkgs=upgradable_pkgs,
pkg_reasons=pkg_reasons,
)
@ -749,6 +827,8 @@ def setup_module():
upgrade_extra_args=dict(type="str", default=""),
update_cache=dict(type="bool"),
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"]],
mutually_exclusive=[["name", "upgrade"]],

View file

@ -12,3 +12,4 @@
- include: 'remove_nosave.yml'
- include: 'update_cache.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": {
"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 = {
@ -108,6 +121,7 @@ empty_inventory = {
"installed_groups": {},
"available_groups": {},
"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,
),
@ -272,6 +307,8 @@ class TestPacman:
"",
"warning: config file /etc/pacman.conf, line 34: directive 'TotalDownload' in section 'options' not recognized.",
),
(0, "", ""),
(0, "", ""),
],
None,
),
@ -288,6 +325,8 @@ class TestPacman:
"partial\npkg\\nlist",
"some warning",
),
(0, "", ""),
(0, "", ""),
],
AnsibleFailJson,
),
@ -375,6 +414,8 @@ class TestPacman:
(["pacman", "--query", "--groups"], {'check_rc': True}, 0, '', ''),
(["pacman", "--sync", "--groups", "--groups"], {'check_rc': True}, 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,
),
@ -843,7 +884,7 @@ class TestPacman:
],
"state": "present",
},
["sudo", "somepackage", "otherpkg"],
["otherpkg", "somepackage", "sudo"],
[
Package("sudo", "sudo"),
Package("grep", "grep"),