mirror of https://github.com/ansible/ansible.git
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
555 lines
18 KiB
Python
555 lines
18 KiB
Python
# (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.
|
|
- For SUSE-based systems C(python3-rpm) package must be installed on targeted hosts.
|
|
This package is required because SUSE does not include RPM Python bindings by default.
|
|
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()
|