diff options
Diffstat (limited to 'ansible_collections/community/general/plugins/modules/pacman.py')
-rw-r--r-- | ansible_collections/community/general/plugins/modules/pacman.py | 859 |
1 files changed, 859 insertions, 0 deletions
diff --git a/ansible_collections/community/general/plugins/modules/pacman.py b/ansible_collections/community/general/plugins/modules/pacman.py new file mode 100644 index 000000000..66f58155d --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/pacman.py @@ -0,0 +1,859 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2012, Afterburn <https://github.com/afterburn> +# Copyright (c) 2013, Aaron Bull Schaefer <aaron@elasticdog.com> +# Copyright (c) 2015, Indrajit Raychaudhuri <irc+code@indrajit.com> +# Copyright (c) 2022, Jean Raby <jean@raby.sh> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: pacman +short_description: Manage packages with I(pacman) +description: + - Manage packages with the I(pacman) package manager, which is used by Arch Linux and its variants. +author: + - Indrajit Raychaudhuri (@indrajitr) + - Aaron Bull Schaefer (@elasticdog) <aaron@elasticdog.com> + - Maxime de Roucy (@tchernomax) + - Jean Raby (@jraby) +extends_documentation_fragment: + - community.general.attributes +attributes: + check_mode: + support: full + diff_mode: + support: full +options: + name: + description: + - Name or list of names of the package(s) or file(s) to install, upgrade, or remove. + Can't be used in combination with C(upgrade). + aliases: [ package, pkg ] + type: list + elements: str + + state: + description: + - Whether to install (C(present) or C(installed), C(latest)), or remove (C(absent) or C(removed)) a package. + - C(present) and C(installed) will simply ensure that a desired package is installed. + - C(latest) will update the specified package if it is not of the latest available version. + - C(absent) and C(removed) will remove the specified package. + default: present + choices: [ absent, installed, latest, present, removed ] + type: str + + force: + description: + - When removing packages, forcefully remove them, without any checks. + Same as I(extra_args="--nodeps --nodeps"). + When combined with I(update_cache), force a refresh of all package databases. + Same as I(update_cache_extra_args="--refresh --refresh"). + default: false + type: bool + + remove_nosave: + description: + - When removing packages, do not save modified configuration files as C(.pacsave) files. + (passes C(--nosave) to pacman) + version_added: 4.6.0 + default: false + type: bool + + executable: + description: + - Path of the binary to use. This can either be C(pacman) or a pacman compatible AUR helper. + - Pacman compatibility is unfortunately ill defined, in particular, this modules makes + extensive use of the C(--print-format) directive which is known not to be implemented by + some AUR helpers (notably, C(yay)). + - Beware that AUR helpers might behave unexpectedly and are therefore not recommended. + default: pacman + type: str + version_added: 3.1.0 + + extra_args: + description: + - Additional option to pass to pacman when enforcing C(state). + default: '' + type: str + + update_cache: + description: + - Whether or not to refresh the master package lists. + - This can be run as part of a package installation or as a separate step. + - If not specified, it defaults to C(false). + - Please note that this option only had an influence on the module's C(changed) state + if I(name) and I(upgrade) are not specified before community.general 5.0.0. + See the examples for how to keep the old behavior. + type: bool + + update_cache_extra_args: + description: + - Additional option to pass to pacman when enforcing C(update_cache). + default: '' + type: str + + upgrade: + description: + - Whether or not to upgrade the whole system. + Can't be used in combination with C(name). + - If not specified, it defaults to C(false). + type: bool + + upgrade_extra_args: + description: + - Additional option to pass to pacman when enforcing C(upgrade). + 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 I(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. + - To use an AUR helper (I(executable) option), a few extra setup steps might be required beforehand. + For example, a dedicated build user with permissions to install packages could be necessary. +""" + +RETURN = """ +packages: + description: + - A list of packages that have been changed. + - Before community.general 4.5.0 this was only returned when I(upgrade=true). + In community.general 4.5.0, it was sometimes omitted when the package list is empty, + but since community.general 4.6.0 it is always returned when I(name) is specified or + I(upgrade=true). + returned: success and I(name) is specified or I(upgrade=true) + type: list + elements: str + sample: [ package, other-package ] + +cache_updated: + description: + - The changed status of C(pacman -Sy). + - Useful when I(name) or I(upgrade=true) are specified next to I(update_cache=true). + returned: success, when I(update_cache=true) + type: bool + sample: false + version_added: 4.6.0 + +stdout: + description: + - Output from pacman. + returned: success, when needed + type: str + sample: ":: Synchronizing package databases... core is up to date :: Starting full system upgrade..." + version_added: 4.1.0 + +stderr: + description: + - Error output from pacman. + returned: success, when needed + type: str + sample: "warning: libtool: local (2.4.6+44+gb9b44533-14) is newer than core (2.4.6+42+gb88cebd5-15)\nwarning ..." + version_added: 4.1.0 +""" + +EXAMPLES = """ +- name: Install package foo from repo + community.general.pacman: + name: foo + state: present + +- name: Install package bar from file + community.general.pacman: + name: ~/bar-1.0-1-any.pkg.tar.xz + state: present + +- name: Install package foo from repo and bar from file + community.general.pacman: + name: + - foo + - ~/bar-1.0-1-any.pkg.tar.xz + state: present + +- name: Install package from AUR using a Pacman compatible AUR helper + community.general.pacman: + name: foo + state: present + executable: yay + extra_args: --builddir /var/cache/yay + +- name: Upgrade package foo + # The 'changed' state of this call will indicate whether the cache was + # updated *or* whether foo was installed/upgraded. + community.general.pacman: + name: foo + state: latest + update_cache: true + +- name: Remove packages foo and bar + community.general.pacman: + name: + - foo + - bar + state: absent + +- name: Recursively remove package baz + community.general.pacman: + name: baz + state: absent + extra_args: --recursive + +- name: Run the equivalent of "pacman -Sy" as a separate step + community.general.pacman: + update_cache: true + +- name: Run the equivalent of "pacman -Su" as a separate step + community.general.pacman: + upgrade: true + +- name: Run the equivalent of "pacman -Syu" as a separate step + # Since community.general 5.0.0 the 'changed' state of this call + # will be 'true' in case the cache was updated, or when a package + # was updated. + # + # The previous behavior was to only indicate whether something was + # upgraded. To keep the old behavior, add the following to the task: + # + # register: result + # changed_when: result.packages | length > 0 + community.general.pacman: + update_cache: true + upgrade: true + +- name: Run the equivalent of "pacman -Rdd", force remove package baz + community.general.pacman: + name: baz + state: absent + force: true + +- 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 +from ansible.module_utils.basic import AnsibleModule +from collections import defaultdict, namedtuple + + +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"]) + + +class Pacman(object): + def __init__(self, module): + self.m = module + + self.m.run_command_environ_update = dict(LC_ALL="C") + p = self.m.params + + self._msgs = [] + self._stdouts = [] + self._stderrs = [] + self.changed = False + self.exit_params = {} + + self.pacman_path = self.m.get_bin_path(p["executable"], True) + + self._cached_database = None + + # Normalize for old configs + if p["state"] == "installed": + self.target_state = "present" + elif p["state"] == "removed": + self.target_state = "absent" + else: + self.target_state = p["state"] + + def add_exit_infos(self, msg=None, stdout=None, stderr=None): + if msg: + self._msgs.append(msg) + if stdout: + self._stdouts.append(stdout) + if stderr: + self._stderrs.append(stderr) + + def _set_mandatory_exit_params(self): + msg = "\n".join(self._msgs) + stdouts = "\n".join(self._stdouts) + stderrs = "\n".join(self._stderrs) + if stdouts: + self.exit_params["stdout"] = stdouts + if stderrs: + self.exit_params["stderr"] = stderrs + self.exit_params["msg"] = msg # mandatory, but might be empty + + def fail(self, msg=None, stdout=None, stderr=None, **kwargs): + self.add_exit_infos(msg, stdout, stderr) + self._set_mandatory_exit_params() + if kwargs: + self.exit_params.update(**kwargs) + self.m.fail_json(**self.exit_params) + + def success(self): + self._set_mandatory_exit_params() + self.m.exit_json(changed=self.changed, **self.exit_params) + + def run(self): + if self.m.params["update_cache"]: + self.update_package_db() + + if not (self.m.params["name"] or self.m.params["upgrade"]): + self.success() + + self.inventory = self._build_inventory() + if self.m.params["upgrade"]: + self.upgrade() + self.success() + + if self.m.params["name"]: + pkgs = self.package_list() + + if self.target_state == "absent": + self.remove_packages(pkgs) + self.success() + else: + self.install_packages(pkgs) + self.success() + + # This shouldn't happen... + self.fail("This is a bug") + + 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 + pkgs_to_install_from_url.append(p) + continue + if ( + p.name not in self.inventory["installed_pkgs"] + or self.target_state == "latest" + and p.name in self.inventory["upgradable_pkgs"] + ): + pkgs_to_install.append(p) + + 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 + + cmd_base = [ + self.pacman_path, + "--noconfirm", + "--noprogressbar", + "--needed", + ] + if self.m.params["extra_args"]: + cmd_base.extend(self.m.params["extra_args"]) + + def _build_install_diff(pacman_verb, pkglist): + # Dry run to build the installation diff + + cmd = cmd_base + [pacman_verb, "--print-format", "%n %v"] + [p.source for p in pkglist] + 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-%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) + + before = [] + after = [] + installed_pkgs = [] + + if pkgs_to_install: + p, b, a = _build_install_diff("--sync", pkgs_to_install) + installed_pkgs.extend(p) + before.extend(b) + after.extend(a) + 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 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") + return + + self.changed = True + + self.exit_params["diff"] = { + "before": "\n".join(sorted(before)) + "\n" if before 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: + 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 + 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) + self._invalidate_database() + + if pkgs_to_install: + _install_packages_for_real("--sync", pkgs_to_install) + if pkgs_to_install_from_url: + _install_packages_for_real("--upgrade", pkgs_to_install_from_url) + + # 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 + pkg_names_to_remove = [p.name for p in pkgs if p.name in self.inventory["installed_pkgs"]] + + if len(pkg_names_to_remove) == 0: + self.exit_params["packages"] = [] + self.add_exit_infos("package(s) already absent") + return + + # There's something to do, set this in advance + self.changed = True + + cmd_base = [self.pacman_path, "--remove", "--noconfirm", "--noprogressbar"] + cmd_base += self.m.params["extra_args"] + cmd_base += ["--nodeps", "--nodeps"] if self.m.params["force"] else [] + # nosave_args conflicts with --print-format. Added later. + # https://github.com/ansible-collections/community.general/issues/4315 + + # This is a bit of a TOCTOU but it is better than parsing the output of + # pacman -R, which is different depending on the user config (VerbosePkgLists) + # Start by gathering what would be removed + cmd = cmd_base + ["--print-format", "%n-%v"] + pkg_names_to_remove + + rc, stdout, stderr = self.m.run_command(cmd, check_rc=False) + if rc != 0: + self.fail("failed to list package(s) to remove", cmd=cmd, stdout=stdout, stderr=stderr) + + removed_pkgs = stdout.split() + self.exit_params["packages"] = removed_pkgs + self.exit_params["diff"] = { + "before": "\n".join(removed_pkgs) + "\n", # trailing \n to avoid diff complaints + "after": "", + } + + if self.m.check_mode: + self.exit_params["packages"] = removed_pkgs + self.add_exit_infos("Would have removed %d packages" % len(removed_pkgs)) + return + + nosave_args = ["--nosave"] if self.m.params["remove_nosave"] else [] + cmd = cmd_base + nosave_args + pkg_names_to_remove + + rc, stdout, stderr = self.m.run_command(cmd, check_rc=False) + if rc != 0: + self.fail("failed to remove package(s)", cmd=cmd, stdout=stdout, stderr=stderr) + self._invalidate_database() + self.exit_params["packages"] = removed_pkgs + self.add_exit_infos("Removed %d package(s)" % len(removed_pkgs), stdout=stdout, stderr=stderr) + + def upgrade(self): + """Runs pacman --sync --sysupgrade if there are upgradable packages""" + + if len(self.inventory["upgradable_pkgs"]) == 0: + self.add_exit_infos("Nothing to upgrade") + return + + self.changed = True # there are upgrades, so there will be changes + + # Build diff based on inventory first. + diff = {"before": "", "after": ""} + for pkg, versions in self.inventory["upgradable_pkgs"].items(): + diff["before"] += "%s-%s\n" % (pkg, versions.current) + diff["after"] += "%s-%s\n" % (pkg, versions.latest) + self.exit_params["diff"] = diff + self.exit_params["packages"] = self.inventory["upgradable_pkgs"].keys() + + if self.m.check_mode: + self.add_exit_infos( + "%d packages would have been upgraded" % (len(self.inventory["upgradable_pkgs"])) + ) + else: + cmd = [ + self.pacman_path, + "--sync", + "--sysupgrade", + "--quiet", + "--noconfirm", + ] + if self.m.params["upgrade_extra_args"]: + cmd += self.m.params["upgrade_extra_args"] + rc, stdout, stderr = self.m.run_command(cmd, check_rc=False) + self._invalidate_database() + if rc == 0: + self.add_exit_infos("System upgraded", stdout=stdout, stderr=stderr) + else: + self.fail("Could not upgrade", cmd=cmd, stdout=stdout, stderr=stderr) + + def _list_database(self): + """runs pacman --sync --list with some caching""" + if self._cached_database is None: + dummy, packages, dummy = self.m.run_command([self.pacman_path, '--sync', '--list'], check_rc=True) + self._cached_database = packages.splitlines() + return self._cached_database + + def _invalidate_database(self): + """invalidates the pacman --sync --list cache""" + self._cached_database = None + + def update_package_db(self): + """runs pacman --sync --refresh""" + if self.m.check_mode: + self.add_exit_infos("Would have updated the package db") + self.changed = True + self.exit_params["cache_updated"] = True + return + + cmd = [ + self.pacman_path, + "--sync", + "--refresh", + ] + if self.m.params["update_cache_extra_args"]: + cmd += self.m.params["update_cache_extra_args"] + if self.m.params["force"]: + cmd += ["--refresh"] + else: + # Dump package database to get contents before update + pre_state = sorted(self._list_database()) + + rc, stdout, stderr = self.m.run_command(cmd, check_rc=False) + self._invalidate_database() + + if self.m.params["force"]: + # Always changed when force=true + self.exit_params["cache_updated"] = True + else: + # Dump package database to get contents after update + post_state = sorted(self._list_database()) + # If contents changed, set changed=true + self.exit_params["cache_updated"] = pre_state != post_state + if self.exit_params["cache_updated"]: + self.changed = True + + if rc == 0: + self.add_exit_infos("Updated package db", stdout=stdout, stderr=stderr) + else: + self.fail("could not update package db", cmd=cmd, stdout=stdout, stderr=stderr) + + def package_list(self): + """Takes the input package list and resolves packages groups to their package list using the inventory, + extracts package names from packages given as files or URLs using calls to pacman + + Returns the expanded/resolved list as a list of Package + """ + pkg_list = [] + for pkg in self.m.params["name"]: + if not pkg: + continue + + is_URL = False + if pkg in self.inventory["available_groups"]: + # Expand group members + for group_member in self.inventory["available_groups"][pkg]: + pkg_list.append(Package(name=group_member, source=group_member)) + elif pkg in self.inventory["available_pkgs"] or pkg in self.inventory["installed_pkgs"]: + # Just a regular pkg, either available in the repositories, + # or locally installed, which we need to know for absent state + pkg_list.append(Package(name=pkg, source=pkg)) + else: + # Last resort, call out to pacman to extract the info, + # pkg is possibly in the <repo>/<pkgname> format, or a filename or a URL + + # Start with <repo>/<pkgname> case + cmd = [self.pacman_path, "--sync", "--print-format", "%n", pkg] + rc, stdout, stderr = self.m.run_command(cmd, check_rc=False) + if rc != 0: + # fallback to filename / URL + cmd = [self.pacman_path, "--upgrade", "--print-format", "%n", pkg] + rc, stdout, stderr = self.m.run_command(cmd, check_rc=False) + if rc != 0: + if self.target_state == "absent": + continue # Don't bark for unavailable packages when trying to remove them + else: + self.fail( + msg="Failed to list package %s" % (pkg), + cmd=cmd, + stdout=stdout, + stderr=stderr, + rc=rc, + ) + # With Pacman v6.0.1 - libalpm v13.0.1, --upgrade outputs " filename_without_extension downloading..." if the URL is unseen. + # In all cases, pacman outputs "loading packages..." on stdout. strip both + stdout = stdout.splitlines()[-1] + is_URL = True + pkg_name = stdout.strip() + pkg_list.append(Package(name=pkg_name, source=pkg, source_is_URL=is_URL)) + + return pkg_list + + def _build_inventory(self): + """Build a cache datastructure used for all pkg lookups + Returns a dict: + { + "installed_pkgs": {pkgname: version}, + "installed_groups": {groupname: set(pkgnames)}, + "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 + """ + + installed_pkgs = {} + dummy, stdout, dummy = self.m.run_command([self.pacman_path, "--query"], 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, ver = l.split() + installed_pkgs[pkg] = ver + + installed_groups = defaultdict(set) + dummy, stdout, dummy = self.m.run_command( + [self.pacman_path, "--query", "--groups"], check_rc=True + ) + # Format of lines: + # base-devel file + # base-devel findutils + # ... + for l in stdout.splitlines(): + l = l.strip() + if not l: + continue + group, pkgname = l.split() + installed_groups[group].add(pkgname) + + available_pkgs = {} + database = self._list_database() + # Format of a line: "core pacman 6.0.1-2" + for l in database: + l = l.strip() + if not l: + continue + repo, pkg, ver = l.split()[:3] + available_pkgs[pkg] = ver + + available_groups = defaultdict(set) + dummy, stdout, dummy = self.m.run_command( + [self.pacman_path, "--sync", "--groups", "--groups"], check_rc=True + ) + # Format of lines: + # vim-plugins vim-airline + # vim-plugins vim-airline-themes + # vim-plugins vim-ale + # ... + for l in stdout.splitlines(): + l = l.strip() + if not l: + continue + group, pkg = l.split() + available_groups[group].add(pkg) + + upgradable_pkgs = {} + rc, stdout, stderr = self.m.run_command( + [self.pacman_path, "--query", "--upgrades"], check_rc=False + ) + + # non-zero exit with nothing in stdout -> nothing to upgrade, all good + # stderr can have warnings, so not checked here + if rc == 1 and stdout == "": + pass # nothing to upgrade + elif rc == 0: + # Format of lines: + # strace 5.14-1 -> 5.15-1 + # systemd 249.7-1 -> 249.7-2 [ignored] + for l in stdout.splitlines(): + l = l.strip() + if not l: + continue + if "[ignored]" in l: + continue + s = l.split() + if len(s) != 4: + self.fail(msg="Invalid line: %s" % l) + + pkg = s[0] + current = s[1] + latest = s[3] + upgradable_pkgs[pkg] = VersionTuple(current=current, latest=latest) + else: + # stuff in stdout but rc!=0, abort + self.fail( + "Couldn't get list of packages available for upgrade", + stdout=stdout, + stderr=stderr, + 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, + ) + + +def setup_module(): + module = AnsibleModule( + argument_spec=dict( + name=dict(type="list", elements="str", aliases=["pkg", "package"]), + state=dict( + type="str", + default="present", + choices=["present", "installed", "latest", "absent", "removed"], + ), + force=dict(type="bool", default=False), + remove_nosave=dict(type="bool", default=False), + executable=dict(type="str", default="pacman"), + extra_args=dict(type="str", default=""), + upgrade=dict(type="bool"), + 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"]], + supports_check_mode=True, + ) + + # Split extra_args as the shell would for easier handling later + for str_args in ["extra_args", "upgrade_extra_args", "update_cache_extra_args"]: + module.params[str_args] = shlex.split(module.params[str_args]) + + return module + + +def main(): + + Pacman(setup_module()).run() + + +if __name__ == "__main__": + main() |