repoquery.py 21 KB

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