1#!/usr/bin/python 2# (c) 2017, Ansible Project 3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 5# most of it copied from AWX's scan_packages module 6 7from __future__ import absolute_import, division, print_function 8__metaclass__ = type 9 10 11DOCUMENTATION = ''' 12module: package_facts 13short_description: Package information as facts 14description: 15 - Return information about installed packages as facts. 16options: 17 manager: 18 description: 19 - The package manager used by the system so we can query the package information. 20 - Since 2.8 this is a list and can support multiple package managers per system. 21 - The 'portage' and 'pkg' options were added in version 2.8. 22 default: ['auto'] 23 choices: ['auto', 'rpm', 'apt', 'portage', 'pkg', 'pacman'] 24 required: False 25 type: list 26 strategy: 27 description: 28 - This option controls how the module queries the package managers on the system. 29 C(first) means it will return only information for the first supported package manager available. 30 C(all) will return information for all supported and available package managers on the system. 31 choices: ['first', 'all'] 32 default: 'first' 33 version_added: "2.8" 34version_added: "2.5" 35requirements: 36 - For 'portage' support it requires the C(qlist) utility, which is part of 'app-portage/portage-utils'. 37 - For Debian-based systems C(python-apt) package must be installed on targeted hosts. 38author: 39 - Matthew Jones (@matburt) 40 - Brian Coca (@bcoca) 41 - Adam Miller (@maxamillion) 42notes: 43 - Supports C(check_mode). 44''' 45 46EXAMPLES = ''' 47- name: Gather the package facts 48 ansible.builtin.package_facts: 49 manager: auto 50 51- name: Print the package facts 52 ansible.builtin.debug: 53 var: ansible_facts.packages 54 55- name: Check whether a package called foobar is installed 56 ansible.builtin.debug: 57 msg: "{{ ansible_facts.packages['foobar'] | length }} versions of foobar are installed!" 58 when: "'foobar' in ansible_facts.packages" 59 60''' 61 62RETURN = ''' 63ansible_facts: 64 description: Facts to add to ansible_facts. 65 returned: always 66 type: complex 67 contains: 68 packages: 69 description: 70 - Maps the package name to a non-empty list of dicts with package information. 71 - Every dict in the list corresponds to one installed version of the package. 72 - The fields described below are present for all package managers. Depending on the 73 package manager, there might be more fields for a package. 74 returned: when operating system level package manager is specified or auto detected manager 75 type: dict 76 contains: 77 name: 78 description: The package's name. 79 returned: always 80 type: str 81 version: 82 description: The package's version. 83 returned: always 84 type: str 85 source: 86 description: Where information on the package came from. 87 returned: always 88 type: str 89 sample: |- 90 { 91 "packages": { 92 "kernel": [ 93 { 94 "name": "kernel", 95 "source": "rpm", 96 "version": "3.10.0", 97 ... 98 }, 99 { 100 "name": "kernel", 101 "source": "rpm", 102 "version": "3.10.0", 103 ... 104 }, 105 ... 106 ], 107 "kernel-tools": [ 108 { 109 "name": "kernel-tools", 110 "source": "rpm", 111 "version": "3.10.0", 112 ... 113 } 114 ], 115 ... 116 } 117 } 118 # Sample rpm 119 { 120 "packages": { 121 "kernel": [ 122 { 123 "arch": "x86_64", 124 "epoch": null, 125 "name": "kernel", 126 "release": "514.26.2.el7", 127 "source": "rpm", 128 "version": "3.10.0" 129 }, 130 { 131 "arch": "x86_64", 132 "epoch": null, 133 "name": "kernel", 134 "release": "514.16.1.el7", 135 "source": "rpm", 136 "version": "3.10.0" 137 }, 138 { 139 "arch": "x86_64", 140 "epoch": null, 141 "name": "kernel", 142 "release": "514.10.2.el7", 143 "source": "rpm", 144 "version": "3.10.0" 145 }, 146 { 147 "arch": "x86_64", 148 "epoch": null, 149 "name": "kernel", 150 "release": "514.21.1.el7", 151 "source": "rpm", 152 "version": "3.10.0" 153 }, 154 { 155 "arch": "x86_64", 156 "epoch": null, 157 "name": "kernel", 158 "release": "693.2.2.el7", 159 "source": "rpm", 160 "version": "3.10.0" 161 } 162 ], 163 "kernel-tools": [ 164 { 165 "arch": "x86_64", 166 "epoch": null, 167 "name": "kernel-tools", 168 "release": "693.2.2.el7", 169 "source": "rpm", 170 "version": "3.10.0" 171 } 172 ], 173 "kernel-tools-libs": [ 174 { 175 "arch": "x86_64", 176 "epoch": null, 177 "name": "kernel-tools-libs", 178 "release": "693.2.2.el7", 179 "source": "rpm", 180 "version": "3.10.0" 181 } 182 ], 183 } 184 } 185 # Sample deb 186 { 187 "packages": { 188 "libbz2-1.0": [ 189 { 190 "version": "1.0.6-5", 191 "source": "apt", 192 "arch": "amd64", 193 "name": "libbz2-1.0" 194 } 195 ], 196 "patch": [ 197 { 198 "version": "2.7.1-4ubuntu1", 199 "source": "apt", 200 "arch": "amd64", 201 "name": "patch" 202 } 203 ], 204 } 205 } 206''' 207 208import re 209 210from ansible.module_utils._text import to_native, to_text 211from ansible.module_utils.basic import AnsibleModule, missing_required_lib 212from ansible.module_utils.common.process import get_bin_path 213from ansible.module_utils.facts.packages import LibMgr, CLIMgr, get_all_pkg_managers 214 215 216class RPM(LibMgr): 217 218 LIB = 'rpm' 219 220 def list_installed(self): 221 return self._lib.TransactionSet().dbMatch() 222 223 def get_package_details(self, package): 224 return dict(name=package[self._lib.RPMTAG_NAME], 225 version=package[self._lib.RPMTAG_VERSION], 226 release=package[self._lib.RPMTAG_RELEASE], 227 epoch=package[self._lib.RPMTAG_EPOCH], 228 arch=package[self._lib.RPMTAG_ARCH],) 229 230 def is_available(self): 231 ''' we expect the python bindings installed, but this gives warning if they are missing and we have rpm cli''' 232 we_have_lib = super(RPM, self).is_available() 233 234 try: 235 get_bin_path('rpm') 236 if not we_have_lib: 237 module.warn('Found "rpm" but %s' % (missing_required_lib('rpm'))) 238 except ValueError: 239 pass 240 241 return we_have_lib 242 243 244class APT(LibMgr): 245 246 LIB = 'apt' 247 248 def __init__(self): 249 self._cache = None 250 super(APT, self).__init__() 251 252 @property 253 def pkg_cache(self): 254 if self._cache is not None: 255 return self._cache 256 257 self._cache = self._lib.Cache() 258 return self._cache 259 260 def is_available(self): 261 ''' we expect the python bindings installed, but if there is apt/apt-get give warning about missing bindings''' 262 we_have_lib = super(APT, self).is_available() 263 if not we_have_lib: 264 for exe in ('apt', 'apt-get', 'aptitude'): 265 try: 266 get_bin_path(exe) 267 except ValueError: 268 continue 269 else: 270 module.warn('Found "%s" but %s' % (exe, missing_required_lib('apt'))) 271 break 272 return we_have_lib 273 274 def list_installed(self): 275 # Store the cache to avoid running pkg_cache() for each item in the comprehension, which is very slow 276 cache = self.pkg_cache 277 return [pk for pk in cache.keys() if cache[pk].is_installed] 278 279 def get_package_details(self, package): 280 ac_pkg = self.pkg_cache[package].installed 281 return dict(name=package, version=ac_pkg.version, arch=ac_pkg.architecture, category=ac_pkg.section, origin=ac_pkg.origins[0].origin) 282 283 284class PACMAN(CLIMgr): 285 286 CLI = 'pacman' 287 288 def list_installed(self): 289 rc, out, err = module.run_command([self._cli, '-Qi'], environ_update=dict(LC_ALL='C')) 290 if rc != 0 or err: 291 raise Exception("Unable to list packages rc=%s : %s" % (rc, err)) 292 return out.split("\n\n")[:-1] 293 294 def get_package_details(self, package): 295 # parse values of details that might extend over several lines 296 raw_pkg_details = {} 297 last_detail = None 298 for line in package.splitlines(): 299 m = re.match(r"([\w ]*[\w]) +: (.*)", line) 300 if m: 301 last_detail = m.group(1) 302 raw_pkg_details[last_detail] = m.group(2) 303 else: 304 # append value to previous detail 305 raw_pkg_details[last_detail] = raw_pkg_details[last_detail] + " " + line.lstrip() 306 307 provides = None 308 if raw_pkg_details['Provides'] != 'None': 309 provides = [ 310 p.split('=')[0] 311 for p in raw_pkg_details['Provides'].split(' ') 312 ] 313 314 return { 315 'name': raw_pkg_details['Name'], 316 'version': raw_pkg_details['Version'], 317 'arch': raw_pkg_details['Architecture'], 318 'provides': provides, 319 } 320 321 322class PKG(CLIMgr): 323 324 CLI = 'pkg' 325 atoms = ['name', 'version', 'origin', 'installed', 'automatic', 'arch', 'category', 'prefix', 'vital'] 326 327 def list_installed(self): 328 rc, out, err = module.run_command([self._cli, 'query', "%%%s" % '\t%'.join(['n', 'v', 'R', 't', 'a', 'q', 'o', 'p', 'V'])]) 329 if rc != 0 or err: 330 raise Exception("Unable to list packages rc=%s : %s" % (rc, err)) 331 return out.splitlines() 332 333 def get_package_details(self, package): 334 335 pkg = dict(zip(self.atoms, package.split('\t'))) 336 337 if 'arch' in pkg: 338 try: 339 pkg['arch'] = pkg['arch'].split(':')[2] 340 except IndexError: 341 pass 342 343 if 'automatic' in pkg: 344 pkg['automatic'] = bool(int(pkg['automatic'])) 345 346 if 'category' in pkg: 347 pkg['category'] = pkg['category'].split('/', 1)[0] 348 349 if 'version' in pkg: 350 if ',' in pkg['version']: 351 pkg['version'], pkg['port_epoch'] = pkg['version'].split(',', 1) 352 else: 353 pkg['port_epoch'] = 0 354 355 if '_' in pkg['version']: 356 pkg['version'], pkg['revision'] = pkg['version'].split('_', 1) 357 else: 358 pkg['revision'] = '0' 359 360 if 'vital' in pkg: 361 pkg['vital'] = bool(int(pkg['vital'])) 362 363 return pkg 364 365 366class PORTAGE(CLIMgr): 367 368 CLI = 'qlist' 369 atoms = ['category', 'name', 'version', 'ebuild_revision', 'slots', 'prefixes', 'sufixes'] 370 371 def list_installed(self): 372 rc, out, err = module.run_command(' '.join([self._cli, '-Iv', '|', 'xargs', '-n', '1024', 'qatom']), use_unsafe_shell=True) 373 if rc != 0: 374 raise RuntimeError("Unable to list packages rc=%s : %s" % (rc, to_native(err))) 375 return out.splitlines() 376 377 def get_package_details(self, package): 378 return dict(zip(self.atoms, package.split())) 379 380 381def main(): 382 383 # get supported pkg managers 384 PKG_MANAGERS = get_all_pkg_managers() 385 PKG_MANAGER_NAMES = [x.lower() for x in PKG_MANAGERS.keys()] 386 387 # start work 388 global module 389 module = AnsibleModule(argument_spec=dict(manager={'type': 'list', 'default': ['auto']}, 390 strategy={'choices': ['first', 'all'], 'default': 'first'}), 391 supports_check_mode=True) 392 packages = {} 393 results = {'ansible_facts': {}} 394 managers = [x.lower() for x in module.params['manager']] 395 strategy = module.params['strategy'] 396 397 if 'auto' in managers: 398 # keep order from user, we do dedupe below 399 managers.extend(PKG_MANAGER_NAMES) 400 managers.remove('auto') 401 402 unsupported = set(managers).difference(PKG_MANAGER_NAMES) 403 if unsupported: 404 if 'auto' in module.params['manager']: 405 msg = 'Could not auto detect a usable package manager, check warnings for details.' 406 else: 407 msg = 'Unsupported package managers requested: %s' % (', '.join(unsupported)) 408 module.fail_json(msg=msg) 409 410 found = 0 411 seen = set() 412 for pkgmgr in managers: 413 414 if found and strategy == 'first': 415 break 416 417 # dedupe as per above 418 if pkgmgr in seen: 419 continue 420 seen.add(pkgmgr) 421 try: 422 try: 423 # manager throws exception on init (calls self.test) if not usable. 424 manager = PKG_MANAGERS[pkgmgr]() 425 if manager.is_available(): 426 found += 1 427 packages.update(manager.get_packages()) 428 429 except Exception as e: 430 if pkgmgr in module.params['manager']: 431 module.warn('Requested package manager %s was not usable by this module: %s' % (pkgmgr, to_text(e))) 432 continue 433 434 except Exception as e: 435 if pkgmgr in module.params['manager']: 436 module.warn('Failed to retrieve packages with %s: %s' % (pkgmgr, to_text(e))) 437 438 if found == 0: 439 msg = ('Could not detect a supported package manager from the following list: %s, ' 440 'or the required Python library is not installed. Check warnings for details.' % managers) 441 module.fail_json(msg=msg) 442 443 # Set the facts, this will override the facts in ansible_facts that might exist from previous runs 444 # when using operating system level or distribution package managers 445 results['ansible_facts']['packages'] = packages 446 447 module.exit_json(**results) 448 449 450if __name__ == '__main__': 451 main() 452