mirror of
				https://github.com/ansible-collections/community.general.git
				synced 2025-10-25 13:34:01 -07:00 
			
		
		
		
	pacman: support yay as root (#6713)
* pacman: support yay as root
* make pylint happy
* minor adjustments
* rollback some test actions
* removal of user and pkgs in handlers
* add comment to note
* add changelog frag
* fix doc
* Update tests/integration/targets/pacman/tasks/yay-become.yml
Co-authored-by: Felix Fontein <felix@fontein.de>
* Update tests/integration/targets/pacman/tasks/yay-become.yml
Co-authored-by: Felix Fontein <felix@fontein.de>
* simplify pkg install in int. tests
---------
Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit f6ee2177a2)
Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
		
	
			
		
			
				
	
	
		
			871 lines
		
	
	
	
		
			32 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			871 lines
		
	
	
	
		
			32 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!/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.
 | |
|               Cannot be used in combination with O(upgrade).
 | |
|         aliases: [ package, pkg ]
 | |
|         type: list
 | |
|         elements: str
 | |
| 
 | |
|     state:
 | |
|         description:
 | |
|           - Whether to install (V(present) or V(installed), V(latest)), or remove (V(absent) or V(removed)) a package.
 | |
|           - V(present) and V(installed) will simply ensure that a desired package is installed.
 | |
|           - V(latest) will update the specified package if it is not of the latest available version.
 | |
|           - V(absent) and V(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 O(extra_args="--nodeps --nodeps").
 | |
|             - When combined with O(update_cache), force a refresh of all package databases.
 | |
|               Same as O(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 O(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 V(false).
 | |
|             - Please note that this option only had an influence on the module's C(changed) state
 | |
|               if O(name) and O(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 O(update_cache).
 | |
|         default: ''
 | |
|         type: str
 | |
| 
 | |
|     upgrade:
 | |
|         description:
 | |
|             - Whether or not to upgrade the whole system.
 | |
|               Cannot be used in combination with O(name).
 | |
|             - If not specified, it defaults to V(false).
 | |
|         type: bool
 | |
| 
 | |
|     upgrade_extra_args:
 | |
|         description:
 | |
|             - Additional option to pass to pacman when enforcing O(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 V(all) packages or only for V(new) packages.
 | |
|             - In case of O(state=latest) already installed packages which will be updated to a newer version are not counted as V(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 O(name) option.
 | |
|   - To use an AUR helper (O(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.
 | |
|   - >
 | |
|     In the tests, while using C(yay) as the O(executable) option, the module failed to install AUR packages
 | |
|     with the error: C(error: target not found: <pkg>).
 | |
| """
 | |
| 
 | |
| RETURN = """
 | |
| packages:
 | |
|     description:
 | |
|         - A list of packages that have been changed.
 | |
|         - Before community.general 4.5.0 this was only returned when O(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 O(name) is specified or
 | |
|           O(upgrade=true).
 | |
|     returned: success and O(name) is specified or O(upgrade=true)
 | |
|     type: list
 | |
|     elements: str
 | |
|     sample: [ package, other-package ]
 | |
| 
 | |
| cache_updated:
 | |
|     description:
 | |
|         - The changed status of C(pacman -Sy).
 | |
|         - Useful when O(name) or O(upgrade=true) are specified next to O(update_cache=true).
 | |
|     returned: success, when O(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 re
 | |
| 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 or 'Avoid running' 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"
 | |
|         query_re = re.compile(r'^\s*(?P<pkg>\S+)\s+(?P<ver>\S+)\s*$')
 | |
|         for l in stdout.splitlines():
 | |
|             query_match = query_re.match(l)
 | |
|             if not query_match:
 | |
|                 continue
 | |
|             pkg, ver = query_match.groups()
 | |
|             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
 | |
|         #     ...
 | |
|         query_groups_re = re.compile(r'^\s*(?P<group>\S+)\s+(?P<pkg>\S+)\s*$')
 | |
|         for l in stdout.splitlines():
 | |
|             query_groups_match = query_groups_re.match(l)
 | |
|             if not query_groups_match:
 | |
|                 continue
 | |
|             group, pkgname = query_groups_match.groups()
 | |
|             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
 | |
|         #     ...
 | |
|         sync_groups_re = re.compile(r'^\s*(?P<group>\S+)\s+(?P<pkg>\S+)\s*$')
 | |
|         for l in stdout.splitlines():
 | |
|             sync_groups_match = sync_groups_re.match(l)
 | |
|             if not sync_groups_match:
 | |
|                 continue
 | |
|             group, pkg = sync_groups_match.groups()
 | |
|             available_groups[group].add(pkg)
 | |
| 
 | |
|         upgradable_pkgs = {}
 | |
|         rc, stdout, stderr = self.m.run_command(
 | |
|             [self.pacman_path, "--query", "--upgrades"], check_rc=False
 | |
|         )
 | |
| 
 | |
|         stdout = stdout.splitlines()
 | |
|         if stdout and "Avoid running" in stdout[0]:
 | |
|             stdout = stdout[1:]
 | |
|         stdout = "\n".join(stdout)
 | |
| 
 | |
|         # non-zero exit with nothing in stdout -> nothing to upgrade, all good
 | |
|         # stderr can have warnings, so not checked here
 | |
|         if rc == 1 and not 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 or "Avoid running" 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()
 |