repoquery.py 21 KB

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