diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:04:21 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:04:21 +0000 |
commit | 8a754e0858d922e955e71b253c139e071ecec432 (patch) | |
tree | 527d16e74bfd1840c85efd675fdecad056c54107 /lib/ansible/modules/package_facts.py | |
parent | Initial commit. (diff) | |
download | ansible-core-upstream/2.14.3.tar.xz ansible-core-upstream/2.14.3.zip |
Adding upstream version 2.14.3.upstream/2.14.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'lib/ansible/modules/package_facts.py')
-rw-r--r-- | lib/ansible/modules/package_facts.py | 552 |
1 files changed, 552 insertions, 0 deletions
diff --git a/lib/ansible/modules/package_facts.py b/lib/ansible/modules/package_facts.py new file mode 100644 index 0000000..57c1d3e --- /dev/null +++ b/lib/ansible/modules/package_facts.py @@ -0,0 +1,552 @@ +# (c) 2017, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# most of it copied from AWX's scan_packages module + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +module: package_facts +short_description: Package information as facts +description: + - Return information about installed packages as facts. +options: + manager: + description: + - The package manager used by the system so we can query the package information. + - Since 2.8 this is a list and can support multiple package managers per system. + - The 'portage' and 'pkg' options were added in version 2.8. + - The 'apk' option was added in version 2.11. + - The 'pkg_info' option was added in version 2.13. + default: ['auto'] + choices: ['auto', 'rpm', 'apt', 'portage', 'pkg', 'pacman', 'apk', 'pkg_info'] + type: list + elements: str + strategy: + description: + - This option controls how the module queries the package managers on the system. + C(first) means it will return only information for the first supported package manager available. + C(all) will return information for all supported and available package managers on the system. + choices: ['first', 'all'] + default: 'first' + type: str + version_added: "2.8" +version_added: "2.5" +requirements: + - For 'portage' support it requires the C(qlist) utility, which is part of 'app-portage/portage-utils'. + - For Debian-based systems C(python-apt) package must be installed on targeted hosts. +author: + - Matthew Jones (@matburt) + - Brian Coca (@bcoca) + - Adam Miller (@maxamillion) +extends_documentation_fragment: + - action_common_attributes + - action_common_attributes.facts +attributes: + check_mode: + support: full + diff_mode: + support: none + facts: + support: full + platform: + platforms: posix +''' + +EXAMPLES = ''' +- name: Gather the package facts + ansible.builtin.package_facts: + manager: auto + +- name: Print the package facts + ansible.builtin.debug: + var: ansible_facts.packages + +- name: Check whether a package called foobar is installed + ansible.builtin.debug: + msg: "{{ ansible_facts.packages['foobar'] | length }} versions of foobar are installed!" + when: "'foobar' in ansible_facts.packages" + +''' + +RETURN = ''' +ansible_facts: + description: Facts to add to ansible_facts. + returned: always + type: complex + contains: + packages: + description: + - Maps the package name to a non-empty list of dicts with package information. + - Every dict in the list corresponds to one installed version of the package. + - The fields described below are present for all package managers. Depending on the + package manager, there might be more fields for a package. + returned: when operating system level package manager is specified or auto detected manager + type: dict + contains: + name: + description: The package's name. + returned: always + type: str + version: + description: The package's version. + returned: always + type: str + source: + description: Where information on the package came from. + returned: always + type: str + sample: |- + { + "packages": { + "kernel": [ + { + "name": "kernel", + "source": "rpm", + "version": "3.10.0", + ... + }, + { + "name": "kernel", + "source": "rpm", + "version": "3.10.0", + ... + }, + ... + ], + "kernel-tools": [ + { + "name": "kernel-tools", + "source": "rpm", + "version": "3.10.0", + ... + } + ], + ... + } + } + # Sample rpm + { + "packages": { + "kernel": [ + { + "arch": "x86_64", + "epoch": null, + "name": "kernel", + "release": "514.26.2.el7", + "source": "rpm", + "version": "3.10.0" + }, + { + "arch": "x86_64", + "epoch": null, + "name": "kernel", + "release": "514.16.1.el7", + "source": "rpm", + "version": "3.10.0" + }, + { + "arch": "x86_64", + "epoch": null, + "name": "kernel", + "release": "514.10.2.el7", + "source": "rpm", + "version": "3.10.0" + }, + { + "arch": "x86_64", + "epoch": null, + "name": "kernel", + "release": "514.21.1.el7", + "source": "rpm", + "version": "3.10.0" + }, + { + "arch": "x86_64", + "epoch": null, + "name": "kernel", + "release": "693.2.2.el7", + "source": "rpm", + "version": "3.10.0" + } + ], + "kernel-tools": [ + { + "arch": "x86_64", + "epoch": null, + "name": "kernel-tools", + "release": "693.2.2.el7", + "source": "rpm", + "version": "3.10.0" + } + ], + "kernel-tools-libs": [ + { + "arch": "x86_64", + "epoch": null, + "name": "kernel-tools-libs", + "release": "693.2.2.el7", + "source": "rpm", + "version": "3.10.0" + } + ], + } + } + # Sample deb + { + "packages": { + "libbz2-1.0": [ + { + "version": "1.0.6-5", + "source": "apt", + "arch": "amd64", + "name": "libbz2-1.0" + } + ], + "patch": [ + { + "version": "2.7.1-4ubuntu1", + "source": "apt", + "arch": "amd64", + "name": "patch" + } + ], + } + } + # Sample pkg_info + { + "packages": { + "curl": [ + { + "name": "curl", + "source": "pkg_info", + "version": "7.79.0" + } + ], + "intel-firmware": [ + { + "name": "intel-firmware", + "source": "pkg_info", + "version": "20210608v0" + } + ], + } + } +''' + +import re + +from ansible.module_utils._text import to_native, to_text +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.common.locale import get_best_parsable_locale +from ansible.module_utils.common.process import get_bin_path +from ansible.module_utils.common.respawn import has_respawned, probe_interpreters_for_module, respawn_module +from ansible.module_utils.facts.packages import LibMgr, CLIMgr, get_all_pkg_managers + + +class RPM(LibMgr): + + LIB = 'rpm' + + def list_installed(self): + return self._lib.TransactionSet().dbMatch() + + def get_package_details(self, package): + return dict(name=package[self._lib.RPMTAG_NAME], + version=package[self._lib.RPMTAG_VERSION], + release=package[self._lib.RPMTAG_RELEASE], + epoch=package[self._lib.RPMTAG_EPOCH], + arch=package[self._lib.RPMTAG_ARCH],) + + def is_available(self): + ''' we expect the python bindings installed, but this gives warning if they are missing and we have rpm cli''' + we_have_lib = super(RPM, self).is_available() + + try: + get_bin_path('rpm') + + if not we_have_lib and not has_respawned(): + # try to locate an interpreter with the necessary lib + interpreters = ['/usr/libexec/platform-python', + '/usr/bin/python3', + '/usr/bin/python2'] + interpreter_path = probe_interpreters_for_module(interpreters, self.LIB) + if interpreter_path: + respawn_module(interpreter_path) + # end of the line for this process; this module will exit when the respawned copy completes + + if not we_have_lib: + module.warn('Found "rpm" but %s' % (missing_required_lib(self.LIB))) + except ValueError: + pass + + return we_have_lib + + +class APT(LibMgr): + + LIB = 'apt' + + def __init__(self): + self._cache = None + super(APT, self).__init__() + + @property + def pkg_cache(self): + if self._cache is not None: + return self._cache + + self._cache = self._lib.Cache() + return self._cache + + def is_available(self): + ''' we expect the python bindings installed, but if there is apt/apt-get give warning about missing bindings''' + we_have_lib = super(APT, self).is_available() + if not we_have_lib: + for exe in ('apt', 'apt-get', 'aptitude'): + try: + get_bin_path(exe) + except ValueError: + continue + else: + if not has_respawned(): + # try to locate an interpreter with the necessary lib + interpreters = ['/usr/bin/python3', + '/usr/bin/python2'] + interpreter_path = probe_interpreters_for_module(interpreters, self.LIB) + if interpreter_path: + respawn_module(interpreter_path) + # end of the line for this process; this module will exit here when respawned copy completes + + module.warn('Found "%s" but %s' % (exe, missing_required_lib('apt'))) + break + + return we_have_lib + + def list_installed(self): + # Store the cache to avoid running pkg_cache() for each item in the comprehension, which is very slow + cache = self.pkg_cache + return [pk for pk in cache.keys() if cache[pk].is_installed] + + def get_package_details(self, package): + ac_pkg = self.pkg_cache[package].installed + return dict(name=package, version=ac_pkg.version, arch=ac_pkg.architecture, category=ac_pkg.section, origin=ac_pkg.origins[0].origin) + + +class PACMAN(CLIMgr): + + CLI = 'pacman' + + def list_installed(self): + locale = get_best_parsable_locale(module) + rc, out, err = module.run_command([self._cli, '-Qi'], environ_update=dict(LC_ALL=locale)) + if rc != 0 or err: + raise Exception("Unable to list packages rc=%s : %s" % (rc, err)) + return out.split("\n\n")[:-1] + + def get_package_details(self, package): + # parse values of details that might extend over several lines + raw_pkg_details = {} + last_detail = None + for line in package.splitlines(): + m = re.match(r"([\w ]*[\w]) +: (.*)", line) + if m: + last_detail = m.group(1) + raw_pkg_details[last_detail] = m.group(2) + else: + # append value to previous detail + raw_pkg_details[last_detail] = raw_pkg_details[last_detail] + " " + line.lstrip() + + provides = None + if raw_pkg_details['Provides'] != 'None': + provides = [ + p.split('=')[0] + for p in raw_pkg_details['Provides'].split(' ') + ] + + return { + 'name': raw_pkg_details['Name'], + 'version': raw_pkg_details['Version'], + 'arch': raw_pkg_details['Architecture'], + 'provides': provides, + } + + +class PKG(CLIMgr): + + CLI = 'pkg' + atoms = ['name', 'version', 'origin', 'installed', 'automatic', 'arch', 'category', 'prefix', 'vital'] + + def list_installed(self): + rc, out, err = module.run_command([self._cli, 'query', "%%%s" % '\t%'.join(['n', 'v', 'R', 't', 'a', 'q', 'o', 'p', 'V'])]) + if rc != 0 or err: + raise Exception("Unable to list packages rc=%s : %s" % (rc, err)) + return out.splitlines() + + def get_package_details(self, package): + + pkg = dict(zip(self.atoms, package.split('\t'))) + + if 'arch' in pkg: + try: + pkg['arch'] = pkg['arch'].split(':')[2] + except IndexError: + pass + + if 'automatic' in pkg: + pkg['automatic'] = bool(int(pkg['automatic'])) + + if 'category' in pkg: + pkg['category'] = pkg['category'].split('/', 1)[0] + + if 'version' in pkg: + if ',' in pkg['version']: + pkg['version'], pkg['port_epoch'] = pkg['version'].split(',', 1) + else: + pkg['port_epoch'] = 0 + + if '_' in pkg['version']: + pkg['version'], pkg['revision'] = pkg['version'].split('_', 1) + else: + pkg['revision'] = '0' + + if 'vital' in pkg: + pkg['vital'] = bool(int(pkg['vital'])) + + return pkg + + +class PORTAGE(CLIMgr): + + CLI = 'qlist' + atoms = ['category', 'name', 'version', 'ebuild_revision', 'slots', 'prefixes', 'sufixes'] + + def list_installed(self): + rc, out, err = module.run_command(' '.join([self._cli, '-Iv', '|', 'xargs', '-n', '1024', 'qatom']), use_unsafe_shell=True) + if rc != 0: + raise RuntimeError("Unable to list packages rc=%s : %s" % (rc, to_native(err))) + return out.splitlines() + + def get_package_details(self, package): + return dict(zip(self.atoms, package.split())) + + +class APK(CLIMgr): + + CLI = 'apk' + + def list_installed(self): + rc, out, err = module.run_command([self._cli, 'info', '-v']) + if rc != 0 or err: + raise Exception("Unable to list packages rc=%s : %s" % (rc, err)) + return out.splitlines() + + def get_package_details(self, package): + raw_pkg_details = {'name': package, 'version': '', 'release': ''} + nvr = package.rsplit('-', 2) + try: + return { + 'name': nvr[0], + 'version': nvr[1], + 'release': nvr[2], + } + except IndexError: + return raw_pkg_details + + +class PKG_INFO(CLIMgr): + + CLI = 'pkg_info' + + def list_installed(self): + rc, out, err = module.run_command([self._cli, '-a']) + if rc != 0 or err: + raise Exception("Unable to list packages rc=%s : %s" % (rc, err)) + return out.splitlines() + + def get_package_details(self, package): + raw_pkg_details = {'name': package, 'version': ''} + details = package.split(maxsplit=1)[0].rsplit('-', maxsplit=1) + + try: + return { + 'name': details[0], + 'version': details[1], + } + except IndexError: + return raw_pkg_details + + +def main(): + + # get supported pkg managers + PKG_MANAGERS = get_all_pkg_managers() + PKG_MANAGER_NAMES = [x.lower() for x in PKG_MANAGERS.keys()] + + # start work + global module + module = AnsibleModule(argument_spec=dict(manager={'type': 'list', 'elements': 'str', 'default': ['auto']}, + strategy={'choices': ['first', 'all'], 'default': 'first'}), + supports_check_mode=True) + packages = {} + results = {'ansible_facts': {}} + managers = [x.lower() for x in module.params['manager']] + strategy = module.params['strategy'] + + if 'auto' in managers: + # keep order from user, we do dedupe below + managers.extend(PKG_MANAGER_NAMES) + managers.remove('auto') + + unsupported = set(managers).difference(PKG_MANAGER_NAMES) + if unsupported: + if 'auto' in module.params['manager']: + msg = 'Could not auto detect a usable package manager, check warnings for details.' + else: + msg = 'Unsupported package managers requested: %s' % (', '.join(unsupported)) + module.fail_json(msg=msg) + + found = 0 + seen = set() + for pkgmgr in managers: + + if found and strategy == 'first': + break + + # dedupe as per above + if pkgmgr in seen: + continue + seen.add(pkgmgr) + try: + try: + # manager throws exception on init (calls self.test) if not usable. + manager = PKG_MANAGERS[pkgmgr]() + if manager.is_available(): + found += 1 + packages.update(manager.get_packages()) + + except Exception as e: + if pkgmgr in module.params['manager']: + module.warn('Requested package manager %s was not usable by this module: %s' % (pkgmgr, to_text(e))) + continue + + except Exception as e: + if pkgmgr in module.params['manager']: + module.warn('Failed to retrieve packages with %s: %s' % (pkgmgr, to_text(e))) + + if found == 0: + msg = ('Could not detect a supported package manager from the following list: %s, ' + 'or the required Python library is not installed. Check warnings for details.' % managers) + module.fail_json(msg=msg) + + # Set the facts, this will override the facts in ansible_facts that might exist from previous runs + # when using operating system level or distribution package managers + results['ansible_facts']['packages'] = packages + + module.exit_json(**results) + + +if __name__ == '__main__': + main() |