repoquery.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. #!/usr/bin/env python
  2. # pylint: disable=missing-docstring
  3. # ___ ___ _ _ ___ ___ _ _____ ___ ___
  4. # / __| __| \| | __| _ \ /_\_ _| __| \
  5. # | (_ | _|| .` | _|| / / _ \| | | _|| |) |
  6. # \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
  7. # | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
  8. # | |) | (_) | | .` | (_) || | | _|| |) | | | |
  9. # |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
  10. #
  11. # Copyright 2016 Red Hat, Inc. and/or its affiliates
  12. # and other contributors as indicated by the @author tags.
  13. #
  14. # Licensed under the Apache License, Version 2.0 (the "License");
  15. # you may not use this file except in compliance with the License.
  16. # You may obtain a copy of the License at
  17. #
  18. # http://www.apache.org/licenses/LICENSE-2.0
  19. #
  20. # Unless required by applicable law or agreed to in writing, software
  21. # distributed under the License is distributed on an "AS IS" BASIS,
  22. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  23. # See the License for the specific language governing permissions and
  24. # limitations under the License.
  25. #
  26. # -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
  27. # pylint: disable=wrong-import-order,wrong-import-position,unused-import
  28. from __future__ import print_function # noqa: F401
  29. import json # noqa: F401
  30. import os # noqa: F401
  31. import re # noqa: F401
  32. # pylint: disable=import-error
  33. import ruamel.yaml as yaml # noqa: F401
  34. import shutil # noqa: F401
  35. from ansible.module_utils.basic import AnsibleModule
  36. # -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
  37. # -*- -*- -*- Begin included fragment: doc/repoquery -*- -*- -*-
  38. DOCUMENTATION = '''
  39. ---
  40. module: repoquery
  41. short_description: Query package information from Yum repositories
  42. description:
  43. - Query package information from Yum repositories.
  44. options:
  45. state:
  46. description:
  47. - The expected state. Currently only supports list.
  48. required: false
  49. default: list
  50. choices: ["list"]
  51. aliases: []
  52. name:
  53. description:
  54. - The name of the package to query
  55. required: true
  56. default: None
  57. aliases: []
  58. query_type:
  59. description:
  60. - Narrows the packages queried based off of this value.
  61. - If repos, it narrows the query to repositories defined on the machine.
  62. - If installed, it narrows the query to only packages installed on the machine.
  63. - If available, it narrows the query to packages that are available to be installed.
  64. - If recent, it narrows the query to only recently edited packages.
  65. - If updates, it narrows the query to only packages that are updates to existing installed packages.
  66. - If extras, it narrows the query to packages that are not present in any of the available repositories.
  67. - If all, it queries all of the above.
  68. required: false
  69. default: repos
  70. aliases: []
  71. verbose:
  72. description:
  73. - Shows more detail for the requested query.
  74. required: false
  75. default: false
  76. aliases: []
  77. show_duplicates:
  78. description:
  79. - Shows multiple versions of a package.
  80. required: false
  81. default: false
  82. aliases: []
  83. match_version:
  84. description:
  85. - Match the specific version given to the package.
  86. required: false
  87. default: None
  88. aliases: []
  89. author:
  90. - "Matt Woodson <mwoodson@redhat.com>"
  91. extends_documentation_fragment: []
  92. '''
  93. EXAMPLES = '''
  94. # Example 1: Get bash versions
  95. - name: Get bash version
  96. repoquery:
  97. name: bash
  98. show_duplicates: True
  99. register: bash_out
  100. # Results:
  101. # ok: [localhost] => {
  102. # "bash_out": {
  103. # "changed": false,
  104. # "results": {
  105. # "cmd": "/usr/bin/repoquery --quiet --pkgnarrow=repos --queryformat=%{version}|%{release}|%{arch}|%{repo}|%{version}-%{release} --show-duplicates bash",
  106. # "package_found": true,
  107. # "package_name": "bash",
  108. # "returncode": 0,
  109. # "versions": {
  110. # "available_versions": [
  111. # "4.2.45",
  112. # "4.2.45",
  113. # "4.2.45",
  114. # "4.2.46",
  115. # "4.2.46",
  116. # "4.2.46",
  117. # "4.2.46"
  118. # ],
  119. # "available_versions_full": [
  120. # "4.2.45-5.el7",
  121. # "4.2.45-5.el7_0.2",
  122. # "4.2.45-5.el7_0.4",
  123. # "4.2.46-12.el7",
  124. # "4.2.46-19.el7",
  125. # "4.2.46-20.el7_2",
  126. # "4.2.46-21.el7_3"
  127. # ],
  128. # "latest": "4.2.46",
  129. # "latest_full": "4.2.46-21.el7_3"
  130. # }
  131. # },
  132. # "state": "present"
  133. # }
  134. # }
  135. # Example 2: Get bash versions verbosely
  136. - name: Get bash versions verbosely
  137. repoquery:
  138. name: bash
  139. show_duplicates: True
  140. verbose: True
  141. register: bash_out
  142. # Results:
  143. # ok: [localhost] => {
  144. # "bash_out": {
  145. # "changed": false,
  146. # "results": {
  147. # "cmd": "/usr/bin/repoquery --quiet --pkgnarrow=repos --queryformat=%{version}|%{release}|%{arch}|%{repo}|%{version}-%{release} --show-duplicates bash",
  148. # "package_found": true,
  149. # "package_name": "bash",
  150. # "raw_versions": {
  151. # "4.2.45-5.el7": {
  152. # "arch": "x86_64",
  153. # "release": "5.el7",
  154. # "repo": "rhel-7-server-rpms",
  155. # "version": "4.2.45",
  156. # "version_release": "4.2.45-5.el7"
  157. # },
  158. # "4.2.45-5.el7_0.2": {
  159. # "arch": "x86_64",
  160. # "release": "5.el7_0.2",
  161. # "repo": "rhel-7-server-rpms",
  162. # "version": "4.2.45",
  163. # "version_release": "4.2.45-5.el7_0.2"
  164. # },
  165. # "4.2.45-5.el7_0.4": {
  166. # "arch": "x86_64",
  167. # "release": "5.el7_0.4",
  168. # "repo": "rhel-7-server-rpms",
  169. # "version": "4.2.45",
  170. # "version_release": "4.2.45-5.el7_0.4"
  171. # },
  172. # "4.2.46-12.el7": {
  173. # "arch": "x86_64",
  174. # "release": "12.el7",
  175. # "repo": "rhel-7-server-rpms",
  176. # "version": "4.2.46",
  177. # "version_release": "4.2.46-12.el7"
  178. # },
  179. # "4.2.46-19.el7": {
  180. # "arch": "x86_64",
  181. # "release": "19.el7",
  182. # "repo": "rhel-7-server-rpms",
  183. # "version": "4.2.46",
  184. # "version_release": "4.2.46-19.el7"
  185. # },
  186. # "4.2.46-20.el7_2": {
  187. # "arch": "x86_64",
  188. # "release": "20.el7_2",
  189. # "repo": "rhel-7-server-rpms",
  190. # "version": "4.2.46",
  191. # "version_release": "4.2.46-20.el7_2"
  192. # },
  193. # "4.2.46-21.el7_3": {
  194. # "arch": "x86_64",
  195. # "release": "21.el7_3",
  196. # "repo": "rhel-7-server-rpms",
  197. # "version": "4.2.46",
  198. # "version_release": "4.2.46-21.el7_3"
  199. # }
  200. # },
  201. # "results": "4.2.45|5.el7|x86_64|rhel-7-server-rpms|4.2.45-5.el7\n4.2.45|5.el7_0.2|x86_64|rhel-7-server-rpms|4.2.45-5.el7_0.2\n4.2.45|5.el7_0.4|x86_64|rhel-7-server-rpms|4.2.45-5.el7_0.4\n4.2.46|12.el7|x86_64|rhel-7-server-rpms|4.2.46-12.el7\n4.2.46|19.el7|x86_64|rhel-7-server-rpms|4.2.46-19.el7\n4.2.46|20.el7_2|x86_64|rhel-7-server-rpms|4.2.46-20.el7_2\n4.2.46|21.el7_3|x86_64|rhel-7-server-rpms|4.2.46-21.el7_3\n",
  202. # "returncode": 0,
  203. # "versions": {
  204. # "available_versions": [
  205. # "4.2.45",
  206. # "4.2.45",
  207. # "4.2.45",
  208. # "4.2.46",
  209. # "4.2.46",
  210. # "4.2.46",
  211. # "4.2.46"
  212. # ],
  213. # "available_versions_full": [
  214. # "4.2.45-5.el7",
  215. # "4.2.45-5.el7_0.2",
  216. # "4.2.45-5.el7_0.4",
  217. # "4.2.46-12.el7",
  218. # "4.2.46-19.el7",
  219. # "4.2.46-20.el7_2",
  220. # "4.2.46-21.el7_3"
  221. # ],
  222. # "latest": "4.2.46",
  223. # "latest_full": "4.2.46-21.el7_3"
  224. # }
  225. # },
  226. # "state": "present"
  227. # }
  228. # }
  229. # Example 3: Match a specific version
  230. - name: matched versions repoquery test
  231. repoquery:
  232. name: atomic-openshift
  233. show_duplicates: True
  234. match_version: 3.3
  235. register: openshift_out
  236. # Result:
  237. # ok: [localhost] => {
  238. # "openshift_out": {
  239. # "changed": false,
  240. # "results": {
  241. # "cmd": "/usr/bin/repoquery --quiet --pkgnarrow=repos --queryformat=%{version}|%{release}|%{arch}|%{repo}|%{version}-%{release} --show-duplicates atomic-openshift",
  242. # "package_found": true,
  243. # "package_name": "atomic-openshift",
  244. # "returncode": 0,
  245. # "versions": {
  246. # "available_versions": [
  247. # "3.2.0.43",
  248. # "3.2.1.23",
  249. # "3.3.0.32",
  250. # "3.3.0.34",
  251. # "3.3.0.35",
  252. # "3.3.1.3",
  253. # "3.3.1.4",
  254. # "3.3.1.5",
  255. # "3.3.1.7",
  256. # "3.4.0.39"
  257. # ],
  258. # "available_versions_full": [
  259. # "3.2.0.43-1.git.0.672599f.el7",
  260. # "3.2.1.23-1.git.0.88a7a1d.el7",
  261. # "3.3.0.32-1.git.0.37bd7ea.el7",
  262. # "3.3.0.34-1.git.0.83f306f.el7",
  263. # "3.3.0.35-1.git.0.d7bd9b6.el7",
  264. # "3.3.1.3-1.git.0.86dc49a.el7",
  265. # "3.3.1.4-1.git.0.7c8657c.el7",
  266. # "3.3.1.5-1.git.0.62700af.el7",
  267. # "3.3.1.7-1.git.0.0988966.el7",
  268. # "3.4.0.39-1.git.0.5f32f06.el7"
  269. # ],
  270. # "latest": "3.4.0.39",
  271. # "latest_full": "3.4.0.39-1.git.0.5f32f06.el7",
  272. # "matched_version_found": true,
  273. # "matched_version_full_latest": "3.3.1.7-1.git.0.0988966.el7",
  274. # "matched_version_latest": "3.3.1.7",
  275. # "matched_versions": [
  276. # "3.3.0.32",
  277. # "3.3.0.34",
  278. # "3.3.0.35",
  279. # "3.3.1.3",
  280. # "3.3.1.4",
  281. # "3.3.1.5",
  282. # "3.3.1.7"
  283. # ],
  284. # "matched_versions_full": [
  285. # "3.3.0.32-1.git.0.37bd7ea.el7",
  286. # "3.3.0.34-1.git.0.83f306f.el7",
  287. # "3.3.0.35-1.git.0.d7bd9b6.el7",
  288. # "3.3.1.3-1.git.0.86dc49a.el7",
  289. # "3.3.1.4-1.git.0.7c8657c.el7",
  290. # "3.3.1.5-1.git.0.62700af.el7",
  291. # "3.3.1.7-1.git.0.0988966.el7"
  292. # ],
  293. # "requested_match_version": "3.3"
  294. # }
  295. # },
  296. # "state": "present"
  297. # }
  298. # }
  299. '''
  300. # -*- -*- -*- End included fragment: doc/repoquery -*- -*- -*-
  301. # -*- -*- -*- Begin included fragment: lib/repoquery.py -*- -*- -*-
  302. '''
  303. class that wraps the repoquery commands in a subprocess
  304. '''
  305. # pylint: disable=too-many-lines,wrong-import-position,wrong-import-order
  306. from collections import defaultdict # noqa: E402
  307. # pylint: disable=no-name-in-module,import-error
  308. # Reason: pylint errors with "No name 'version' in module 'distutils'".
  309. # This is a bug: https://github.com/PyCQA/pylint/issues/73
  310. from distutils.version import LooseVersion # noqa: E402
  311. import subprocess # noqa: E402
  312. class RepoqueryCLIError(Exception):
  313. '''Exception class for repoquerycli'''
  314. pass
  315. def _run(cmds):
  316. ''' Actually executes the command. This makes mocking easier. '''
  317. proc = subprocess.Popen(cmds,
  318. stdin=subprocess.PIPE,
  319. stdout=subprocess.PIPE,
  320. stderr=subprocess.PIPE)
  321. stdout, stderr = proc.communicate()
  322. return proc.returncode, stdout, stderr
  323. # pylint: disable=too-few-public-methods
  324. class RepoqueryCLI(object):
  325. ''' Class to wrap the command line tools '''
  326. def __init__(self,
  327. verbose=False):
  328. ''' Constructor for RepoqueryCLI '''
  329. self.verbose = verbose
  330. self.verbose = True
  331. def _repoquery_cmd(self, cmd, output=False, output_type='json'):
  332. '''Base command for repoquery '''
  333. cmds = ['/usr/bin/repoquery', '--plugins', '--quiet']
  334. cmds.extend(cmd)
  335. rval = {}
  336. results = ''
  337. err = None
  338. if self.verbose:
  339. print(' '.join(cmds))
  340. returncode, stdout, stderr = _run(cmds)
  341. rval = {
  342. "returncode": returncode,
  343. "results": results,
  344. "cmd": ' '.join(cmds),
  345. }
  346. if returncode == 0:
  347. if output:
  348. if output_type == 'raw':
  349. rval['results'] = stdout
  350. if self.verbose:
  351. print(stdout)
  352. print(stderr)
  353. if err:
  354. rval.update({
  355. "err": err,
  356. "stderr": stderr,
  357. "stdout": stdout,
  358. "cmd": cmds
  359. })
  360. else:
  361. rval.update({
  362. "stderr": stderr,
  363. "stdout": stdout,
  364. "results": {},
  365. })
  366. return rval
  367. # -*- -*- -*- End included fragment: lib/repoquery.py -*- -*- -*-
  368. # -*- -*- -*- Begin included fragment: class/repoquery.py -*- -*- -*-
  369. class Repoquery(RepoqueryCLI):
  370. ''' Class to wrap the repoquery
  371. '''
  372. # pylint: disable=too-many-arguments
  373. def __init__(self, name, query_type, show_duplicates,
  374. match_version, verbose):
  375. ''' Constructor for YumList '''
  376. super(Repoquery, self).__init__(None)
  377. self.name = name
  378. self.query_type = query_type
  379. self.show_duplicates = show_duplicates
  380. self.match_version = match_version
  381. self.verbose = verbose
  382. if self.match_version:
  383. self.show_duplicates = True
  384. self.query_format = "%{version}|%{release}|%{arch}|%{repo}|%{version}-%{release}"
  385. def build_cmd(self):
  386. ''' build the repoquery cmd options '''
  387. repo_cmd = []
  388. repo_cmd.append("--pkgnarrow=" + self.query_type)
  389. repo_cmd.append("--queryformat=" + self.query_format)
  390. if self.show_duplicates:
  391. repo_cmd.append('--show-duplicates')
  392. repo_cmd.append(self.name)
  393. return repo_cmd
  394. @staticmethod
  395. def process_versions(query_output):
  396. ''' format the package data into something that can be presented '''
  397. version_dict = defaultdict(dict)
  398. for version in query_output.split('\n'):
  399. pkg_info = version.split("|")
  400. pkg_version = {}
  401. pkg_version['version'] = pkg_info[0]
  402. pkg_version['release'] = pkg_info[1]
  403. pkg_version['arch'] = pkg_info[2]
  404. pkg_version['repo'] = pkg_info[3]
  405. pkg_version['version_release'] = pkg_info[4]
  406. version_dict[pkg_info[4]] = pkg_version
  407. return version_dict
  408. def format_versions(self, formatted_versions):
  409. ''' Gather and present the versions of each package '''
  410. versions_dict = {}
  411. versions_dict['available_versions_full'] = formatted_versions.keys()
  412. # set the match version, if called
  413. if self.match_version:
  414. versions_dict['matched_versions_full'] = []
  415. versions_dict['requested_match_version'] = self.match_version
  416. versions_dict['matched_versions'] = []
  417. # get the "full version (version - release)
  418. versions_dict['available_versions_full'].sort(key=LooseVersion)
  419. versions_dict['latest_full'] = versions_dict['available_versions_full'][-1]
  420. # get the "short version (version)
  421. versions_dict['available_versions'] = []
  422. for version in versions_dict['available_versions_full']:
  423. versions_dict['available_versions'].append(formatted_versions[version]['version'])
  424. if self.match_version:
  425. if version.startswith(self.match_version):
  426. versions_dict['matched_versions_full'].append(version)
  427. versions_dict['matched_versions'].append(formatted_versions[version]['version'])
  428. versions_dict['available_versions'].sort(key=LooseVersion)
  429. versions_dict['latest'] = versions_dict['available_versions'][-1]
  430. # finish up the matched version
  431. if self.match_version:
  432. if versions_dict['matched_versions_full']:
  433. versions_dict['matched_version_found'] = True
  434. versions_dict['matched_versions'].sort(key=LooseVersion)
  435. versions_dict['matched_version_latest'] = versions_dict['matched_versions'][-1]
  436. versions_dict['matched_version_full_latest'] = versions_dict['matched_versions_full'][-1]
  437. else:
  438. versions_dict['matched_version_found'] = False
  439. versions_dict['matched_versions'] = []
  440. versions_dict['matched_version_latest'] = ""
  441. versions_dict['matched_version_full_latest'] = ""
  442. return versions_dict
  443. def repoquery(self):
  444. '''perform a repoquery '''
  445. repoquery_cmd = self.build_cmd()
  446. rval = self._repoquery_cmd(repoquery_cmd, True, 'raw')
  447. # check to see if there are actual results
  448. if rval['results']:
  449. processed_versions = Repoquery.process_versions(rval['results'].strip())
  450. formatted_versions = self.format_versions(processed_versions)
  451. rval['package_found'] = True
  452. rval['versions'] = formatted_versions
  453. rval['package_name'] = self.name
  454. if self.verbose:
  455. rval['raw_versions'] = processed_versions
  456. else:
  457. del rval['results']
  458. # No packages found
  459. else:
  460. rval['package_found'] = False
  461. return rval
  462. @staticmethod
  463. def run_ansible(params, check_mode):
  464. '''run the ansible idempotent code'''
  465. repoquery = Repoquery(
  466. params['name'],
  467. params['query_type'],
  468. params['show_duplicates'],
  469. params['match_version'],
  470. params['verbose'],
  471. )
  472. state = params['state']
  473. if state == 'list':
  474. results = repoquery.repoquery()
  475. if results['returncode'] != 0:
  476. return {'failed': True,
  477. 'msg': results}
  478. return {'changed': False, 'results': results, 'state': 'list', 'check_mode': check_mode}
  479. return {'failed': True,
  480. 'changed': False,
  481. 'msg': 'Unknown state passed. %s' % state,
  482. 'state': 'unknown'}
  483. # -*- -*- -*- End included fragment: class/repoquery.py -*- -*- -*-
  484. # -*- -*- -*- Begin included fragment: ansible/repoquery.py -*- -*- -*-
  485. def main():
  486. '''
  487. ansible repoquery module
  488. '''
  489. module = AnsibleModule(
  490. argument_spec=dict(
  491. state=dict(default='list', type='str', choices=['list']),
  492. name=dict(default=None, required=True, type='str'),
  493. query_type=dict(default='repos', required=False, type='str',
  494. choices=[
  495. 'installed', 'available', 'recent',
  496. 'updates', 'extras', 'all', 'repos'
  497. ]),
  498. verbose=dict(default=False, required=False, type='bool'),
  499. show_duplicates=dict(default=False, required=False, type='bool'),
  500. match_version=dict(default=None, required=False, type='str'),
  501. ),
  502. supports_check_mode=False,
  503. required_if=[('show_duplicates', True, ['name'])],
  504. )
  505. rval = Repoquery.run_ansible(module.params, module.check_mode)
  506. if 'failed' in rval:
  507. module.fail_json(**rval)
  508. module.exit_json(**rval)
  509. if __name__ == "__main__":
  510. main()
  511. # -*- -*- -*- End included fragment: ansible/repoquery.py -*- -*- -*-