#!/usr/bin/env python # pylint: disable=missing-docstring # ___ ___ _ _ ___ ___ _ _____ ___ ___ # / __| __| \| | __| _ \ /_\_ _| __| \ # | (_ | _|| .` | _|| / / _ \| | | _|| |) | # \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____ # | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _| # | |) | (_) | | .` | (_) || | | _|| |) | | | | # |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_| # # Copyright 2016 Red Hat, Inc. and/or its affiliates # and other contributors as indicated by the @author tags. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*- # pylint: disable=wrong-import-order,wrong-import-position,unused-import from __future__ import print_function # noqa: F401 import copy # noqa: F401 import json # noqa: F401 import os # noqa: F401 import re # noqa: F401 import shutil # noqa: F401 import tempfile # noqa: F401 import time # noqa: F401 try: import ruamel.yaml as yaml # noqa: F401 except ImportError: import yaml # noqa: F401 from ansible.module_utils.basic import AnsibleModule # -*- -*- -*- End included fragment: lib/import.py -*- -*- -*- # -*- -*- -*- Begin included fragment: doc/repoquery -*- -*- -*- DOCUMENTATION = ''' --- module: repoquery short_description: Query package information from Yum repositories description: - Query package information from Yum repositories. options: state: description: - The expected state. Currently only supports list. required: false default: list choices: ["list"] aliases: [] name: description: - The name of the package to query required: true default: None aliases: [] query_type: description: - Narrows the packages queried based off of this value. - If repos, it narrows the query to repositories defined on the machine. - If installed, it narrows the query to only packages installed on the machine. - If available, it narrows the query to packages that are available to be installed. - If recent, it narrows the query to only recently edited packages. - If updates, it narrows the query to only packages that are updates to existing installed packages. - If extras, it narrows the query to packages that are not present in any of the available repositories. - If all, it queries all of the above. required: false default: repos aliases: [] verbose: description: - Shows more detail for the requested query. required: false default: false aliases: [] show_duplicates: description: - Shows multiple versions of a package. required: false default: false aliases: [] match_version: description: - Match the specific version given to the package. required: false default: None aliases: [] author: - "Matt Woodson " extends_documentation_fragment: [] ''' EXAMPLES = ''' # Example 1: Get bash versions - name: Get bash version repoquery: name: bash show_duplicates: True register: bash_out # Results: # ok: [localhost] => { # "bash_out": { # "changed": false, # "results": { # "cmd": "/usr/bin/repoquery --quiet --pkgnarrow=repos --queryformat=%{version}|%{release}|%{arch}|%{repo}|%{version}-%{release} --show-duplicates bash", # "package_found": true, # "package_name": "bash", # "returncode": 0, # "versions": { # "available_versions": [ # "4.2.45", # "4.2.45", # "4.2.45", # "4.2.46", # "4.2.46", # "4.2.46", # "4.2.46" # ], # "available_versions_full": [ # "4.2.45-5.el7", # "4.2.45-5.el7_0.2", # "4.2.45-5.el7_0.4", # "4.2.46-12.el7", # "4.2.46-19.el7", # "4.2.46-20.el7_2", # "4.2.46-21.el7_3" # ], # "latest": "4.2.46", # "latest_full": "4.2.46-21.el7_3" # } # }, # "state": "present" # } # } # Example 2: Get bash versions verbosely - name: Get bash versions verbosely repoquery: name: bash show_duplicates: True verbose: True register: bash_out # Results: # ok: [localhost] => { # "bash_out": { # "changed": false, # "results": { # "cmd": "/usr/bin/repoquery --quiet --pkgnarrow=repos --queryformat=%{version}|%{release}|%{arch}|%{repo}|%{version}-%{release} --show-duplicates bash", # "package_found": true, # "package_name": "bash", # "raw_versions": { # "4.2.45-5.el7": { # "arch": "x86_64", # "release": "5.el7", # "repo": "rhel-7-server-rpms", # "version": "4.2.45", # "version_release": "4.2.45-5.el7" # }, # "4.2.45-5.el7_0.2": { # "arch": "x86_64", # "release": "5.el7_0.2", # "repo": "rhel-7-server-rpms", # "version": "4.2.45", # "version_release": "4.2.45-5.el7_0.2" # }, # "4.2.45-5.el7_0.4": { # "arch": "x86_64", # "release": "5.el7_0.4", # "repo": "rhel-7-server-rpms", # "version": "4.2.45", # "version_release": "4.2.45-5.el7_0.4" # }, # "4.2.46-12.el7": { # "arch": "x86_64", # "release": "12.el7", # "repo": "rhel-7-server-rpms", # "version": "4.2.46", # "version_release": "4.2.46-12.el7" # }, # "4.2.46-19.el7": { # "arch": "x86_64", # "release": "19.el7", # "repo": "rhel-7-server-rpms", # "version": "4.2.46", # "version_release": "4.2.46-19.el7" # }, # "4.2.46-20.el7_2": { # "arch": "x86_64", # "release": "20.el7_2", # "repo": "rhel-7-server-rpms", # "version": "4.2.46", # "version_release": "4.2.46-20.el7_2" # }, # "4.2.46-21.el7_3": { # "arch": "x86_64", # "release": "21.el7_3", # "repo": "rhel-7-server-rpms", # "version": "4.2.46", # "version_release": "4.2.46-21.el7_3" # } # }, # "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", # "returncode": 0, # "versions": { # "available_versions": [ # "4.2.45", # "4.2.45", # "4.2.45", # "4.2.46", # "4.2.46", # "4.2.46", # "4.2.46" # ], # "available_versions_full": [ # "4.2.45-5.el7", # "4.2.45-5.el7_0.2", # "4.2.45-5.el7_0.4", # "4.2.46-12.el7", # "4.2.46-19.el7", # "4.2.46-20.el7_2", # "4.2.46-21.el7_3" # ], # "latest": "4.2.46", # "latest_full": "4.2.46-21.el7_3" # } # }, # "state": "present" # } # } # Example 3: Match a specific version - name: matched versions repoquery test repoquery: name: atomic-openshift show_duplicates: True match_version: 3.3 register: openshift_out # Result: # ok: [localhost] => { # "openshift_out": { # "changed": false, # "results": { # "cmd": "/usr/bin/repoquery --quiet --pkgnarrow=repos --queryformat=%{version}|%{release}|%{arch}|%{repo}|%{version}-%{release} --show-duplicates atomic-openshift", # "package_found": true, # "package_name": "atomic-openshift", # "returncode": 0, # "versions": { # "available_versions": [ # "3.2.0.43", # "3.2.1.23", # "3.3.0.32", # "3.3.0.34", # "3.3.0.35", # "3.3.1.3", # "3.3.1.4", # "3.3.1.5", # "3.3.1.7", # "3.4.0.39" # ], # "available_versions_full": [ # "3.2.0.43-1.git.0.672599f.el7", # "3.2.1.23-1.git.0.88a7a1d.el7", # "3.3.0.32-1.git.0.37bd7ea.el7", # "3.3.0.34-1.git.0.83f306f.el7", # "3.3.0.35-1.git.0.d7bd9b6.el7", # "3.3.1.3-1.git.0.86dc49a.el7", # "3.3.1.4-1.git.0.7c8657c.el7", # "3.3.1.5-1.git.0.62700af.el7", # "3.3.1.7-1.git.0.0988966.el7", # "3.4.0.39-1.git.0.5f32f06.el7" # ], # "latest": "3.4.0.39", # "latest_full": "3.4.0.39-1.git.0.5f32f06.el7", # "matched_version_found": true, # "matched_version_full_latest": "3.3.1.7-1.git.0.0988966.el7", # "matched_version_latest": "3.3.1.7", # "matched_versions": [ # "3.3.0.32", # "3.3.0.34", # "3.3.0.35", # "3.3.1.3", # "3.3.1.4", # "3.3.1.5", # "3.3.1.7" # ], # "matched_versions_full": [ # "3.3.0.32-1.git.0.37bd7ea.el7", # "3.3.0.34-1.git.0.83f306f.el7", # "3.3.0.35-1.git.0.d7bd9b6.el7", # "3.3.1.3-1.git.0.86dc49a.el7", # "3.3.1.4-1.git.0.7c8657c.el7", # "3.3.1.5-1.git.0.62700af.el7", # "3.3.1.7-1.git.0.0988966.el7" # ], # "requested_match_version": "3.3" # } # }, # "state": "present" # } # } ''' # -*- -*- -*- End included fragment: doc/repoquery -*- -*- -*- # -*- -*- -*- Begin included fragment: lib/repoquery.py -*- -*- -*- ''' class that wraps the repoquery commands in a subprocess ''' # pylint: disable=too-many-lines,wrong-import-position,wrong-import-order from collections import defaultdict # noqa: E402 # pylint: disable=no-name-in-module,import-error # Reason: pylint errors with "No name 'version' in module 'distutils'". # This is a bug: https://github.com/PyCQA/pylint/issues/73 from distutils.version import LooseVersion # noqa: E402 import subprocess # noqa: E402 class RepoqueryCLIError(Exception): '''Exception class for repoquerycli''' pass def _run(cmds): ''' Actually executes the command. This makes mocking easier. ''' proc = subprocess.Popen(cmds, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = proc.communicate() return proc.returncode, stdout, stderr # pylint: disable=too-few-public-methods class RepoqueryCLI(object): ''' Class to wrap the command line tools ''' def __init__(self, verbose=False): ''' Constructor for RepoqueryCLI ''' self.verbose = verbose self.verbose = True def _repoquery_cmd(self, cmd, output=False, output_type='json'): '''Base command for repoquery ''' cmds = ['/usr/bin/repoquery', '--plugins', '--quiet'] cmds.extend(cmd) rval = {} results = '' err = None if self.verbose: print(' '.join(cmds)) returncode, stdout, stderr = _run(cmds) rval = { "returncode": returncode, "results": results, "cmd": ' '.join(cmds), } if returncode == 0: if output: if output_type == 'raw': rval['results'] = stdout if self.verbose: print(stdout) print(stderr) if err: rval.update({ "err": err, "stderr": stderr, "stdout": stdout, "cmd": cmds }) else: rval.update({ "stderr": stderr, "stdout": stdout, "results": {}, }) return rval # -*- -*- -*- End included fragment: lib/repoquery.py -*- -*- -*- # -*- -*- -*- Begin included fragment: class/repoquery.py -*- -*- -*- class Repoquery(RepoqueryCLI): ''' Class to wrap the repoquery ''' # pylint: disable=too-many-arguments,too-many-instance-attributes def __init__(self, name, query_type, show_duplicates, match_version, ignore_excluders, verbose): ''' Constructor for YumList ''' super(Repoquery, self).__init__(None) self.name = name self.query_type = query_type self.show_duplicates = show_duplicates self.match_version = match_version self.ignore_excluders = ignore_excluders self.verbose = verbose if self.match_version: self.show_duplicates = True self.query_format = "%{version}|%{release}|%{arch}|%{repo}|%{version}-%{release}" self.tmp_file = None def build_cmd(self): ''' build the repoquery cmd options ''' repo_cmd = [] repo_cmd.append("--pkgnarrow=" + self.query_type) repo_cmd.append("--queryformat=" + self.query_format) if self.show_duplicates: repo_cmd.append('--show-duplicates') if self.ignore_excluders: repo_cmd.append('--config=' + self.tmp_file.name) repo_cmd.append(self.name) return repo_cmd @staticmethod def process_versions(query_output): ''' format the package data into something that can be presented ''' version_dict = defaultdict(dict) for version in query_output.decode().split('\n'): pkg_info = version.split("|") pkg_version = {} pkg_version['version'] = pkg_info[0] pkg_version['release'] = pkg_info[1] pkg_version['arch'] = pkg_info[2] pkg_version['repo'] = pkg_info[3] pkg_version['version_release'] = pkg_info[4] version_dict[pkg_info[4]] = pkg_version return version_dict def format_versions(self, formatted_versions): ''' Gather and present the versions of each package ''' versions_dict = {} versions_dict['available_versions_full'] = list(formatted_versions.keys()) # set the match version, if called if self.match_version: versions_dict['matched_versions_full'] = [] versions_dict['requested_match_version'] = self.match_version versions_dict['matched_versions'] = [] # get the "full version (version - release) versions_dict['available_versions_full'].sort(key=LooseVersion) versions_dict['latest_full'] = versions_dict['available_versions_full'][-1] # get the "short version (version) versions_dict['available_versions'] = [] for version in versions_dict['available_versions_full']: versions_dict['available_versions'].append(formatted_versions[version]['version']) if self.match_version: if version.startswith(self.match_version): versions_dict['matched_versions_full'].append(version) versions_dict['matched_versions'].append(formatted_versions[version]['version']) versions_dict['available_versions'].sort(key=LooseVersion) versions_dict['latest'] = versions_dict['available_versions'][-1] # finish up the matched version if self.match_version: if versions_dict['matched_versions_full']: versions_dict['matched_version_found'] = True versions_dict['matched_versions'].sort(key=LooseVersion) versions_dict['matched_version_latest'] = versions_dict['matched_versions'][-1] versions_dict['matched_version_full_latest'] = versions_dict['matched_versions_full'][-1] else: versions_dict['matched_version_found'] = False versions_dict['matched_versions'] = [] versions_dict['matched_version_latest'] = "" versions_dict['matched_version_full_latest'] = "" return versions_dict def repoquery(self): '''perform a repoquery ''' if self.ignore_excluders: # Duplicate yum.conf and reset exclude= line to an empty string # to clear a list of all excluded packages self.tmp_file = tempfile.NamedTemporaryFile() with open("/etc/yum.conf", "r") as file_handler: yum_conf_lines = file_handler.readlines() yum_conf_lines = ["exclude=" if l.startswith("exclude=") else l for l in yum_conf_lines] with open(self.tmp_file.name, "w") as file_handler: file_handler.writelines(yum_conf_lines) file_handler.flush() repoquery_cmd = self.build_cmd() rval = self._repoquery_cmd(repoquery_cmd, True, 'raw') # check to see if there are actual results if rval['results']: processed_versions = Repoquery.process_versions(rval['results'].strip()) formatted_versions = self.format_versions(processed_versions) rval['package_found'] = True rval['versions'] = formatted_versions rval['package_name'] = self.name if self.verbose: rval['raw_versions'] = processed_versions else: del rval['results'] # No packages found else: rval['package_found'] = False if self.ignore_excluders: self.tmp_file.close() return rval @staticmethod def run_ansible(params, check_mode): '''run the ansible idempotent code''' repoquery = Repoquery( params['name'], params['query_type'], params['show_duplicates'], params['match_version'], params['ignore_excluders'], params['verbose'], ) state = params['state'] if state == 'list': results = repoquery.repoquery() if results['returncode'] != 0: return {'failed': True, 'msg': results} return {'changed': False, 'results': results, 'state': 'list', 'check_mode': check_mode} return {'failed': True, 'changed': False, 'msg': 'Unknown state passed. %s' % state, 'state': 'unknown'} # -*- -*- -*- End included fragment: class/repoquery.py -*- -*- -*- # -*- -*- -*- Begin included fragment: ansible/repoquery.py -*- -*- -*- def main(): ''' ansible repoquery module ''' module = AnsibleModule( argument_spec=dict( state=dict(default='list', type='str', choices=['list']), name=dict(default=None, required=True, type='str'), query_type=dict(default='repos', required=False, type='str', choices=[ 'installed', 'available', 'recent', 'updates', 'extras', 'all', 'repos' ]), verbose=dict(default=False, required=False, type='bool'), show_duplicates=dict(default=False, required=False, type='bool'), match_version=dict(default=None, required=False, type='str'), ignore_excluders=dict(default=False, required=False, type='bool'), retries=dict(default=4, required=False, type='int'), retry_interval=dict(default=5, required=False, type='int'), ), supports_check_mode=False, required_if=[('show_duplicates', True, ['name'])], ) tries = 1 while True: rval = Repoquery.run_ansible(module.params, module.check_mode) if 'failed' not in rval: module.exit_json(**rval) elif tries > module.params['retries']: module.fail_json(**rval) tries += 1 time.sleep(module.params['retry_interval']) if __name__ == "__main__": main() # -*- -*- -*- End included fragment: ansible/repoquery.py -*- -*- -*-