diff options
Diffstat (limited to 'roles/lib_utils')
-rw-r--r-- | roles/lib_utils/library/repoquery.py | 607 | ||||
-rw-r--r-- | roles/lib_utils/library/yedit.py | 19 | ||||
-rw-r--r-- | roles/lib_utils/src/ansible/repoquery.py | 35 | ||||
-rw-r--r-- | roles/lib_utils/src/class/import.py | 11 | ||||
-rw-r--r-- | roles/lib_utils/src/class/repoquery.py | 156 | ||||
-rw-r--r-- | roles/lib_utils/src/doc/repoquery | 275 | ||||
-rw-r--r-- | roles/lib_utils/src/lib/import.py | 14 | ||||
-rw-r--r-- | roles/lib_utils/src/lib/repoquery.py | 92 | ||||
-rw-r--r-- | roles/lib_utils/src/sources.yml | 11 | ||||
-rwxr-xr-x | roles/lib_utils/src/test/integration/repoquery.yml | 136 | ||||
-rwxr-xr-x | roles/lib_utils/src/test/unit/repoquery.py | 87 |
11 files changed, 1423 insertions, 20 deletions
diff --git a/roles/lib_utils/library/repoquery.py b/roles/lib_utils/library/repoquery.py new file mode 100644 index 000000000..7f0105290 --- /dev/null +++ b/roles/lib_utils/library/repoquery.py @@ -0,0 +1,607 @@ +#!/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 json # noqa: F401 +import os # noqa: F401 +import re # noqa: F401 +# pylint: disable=import-error +import ruamel.yaml as yaml # noqa: F401 +import shutil # 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 <mwoodson@redhat.com>" +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 + def __init__(self, name, query_type, show_duplicates, + match_version, 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.verbose = verbose + + if self.match_version: + self.show_duplicates = True + + self.query_format = "%{version}|%{release}|%{arch}|%{repo}|%{version}-%{release}" + + 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') + + 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.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'] = 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 ''' + + 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 + + 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['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'), + ), + supports_check_mode=False, + required_if=[('show_duplicates', True, ['name'])], + ) + + rval = Repoquery.run_ansible(module.params, module.check_mode) + + if 'failed' in rval: + module.fail_json(**rval) + + module.exit_json(**rval) + + +if __name__ == "__main__": + main() + +# -*- -*- -*- End included fragment: ansible/repoquery.py -*- -*- -*- diff --git a/roles/lib_utils/library/yedit.py b/roles/lib_utils/library/yedit.py index 8a2bd92f9..7ad2b7181 100644 --- a/roles/lib_utils/library/yedit.py +++ b/roles/lib_utils/library/yedit.py @@ -24,18 +24,21 @@ # limitations under the License. # -# -*- -*- -*- Begin included fragment: class/import.py -*- -*- -*- +# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*- -# pylint: disable=wrong-import-order -import json -import os -import re +# pylint: disable=wrong-import-order,wrong-import-position,unused-import + +from __future__ import print_function # noqa: F401 +import json # noqa: F401 +import os # noqa: F401 +import re # noqa: F401 # pylint: disable=import-error -import ruamel.yaml as yaml -import shutil +import ruamel.yaml as yaml # noqa: F401 +import shutil # noqa: F401 + from ansible.module_utils.basic import AnsibleModule -# -*- -*- -*- End included fragment: class/import.py -*- -*- -*- +# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*- # -*- -*- -*- Begin included fragment: doc/yedit -*- -*- -*- diff --git a/roles/lib_utils/src/ansible/repoquery.py b/roles/lib_utils/src/ansible/repoquery.py new file mode 100644 index 000000000..cb4efa6c1 --- /dev/null +++ b/roles/lib_utils/src/ansible/repoquery.py @@ -0,0 +1,35 @@ +# pylint: skip-file +# flake8: noqa + + +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'), + ), + supports_check_mode=False, + required_if=[('show_duplicates', True, ['name'])], + ) + + rval = Repoquery.run_ansible(module.params, module.check_mode) + + if 'failed' in rval: + module.fail_json(**rval) + + module.exit_json(**rval) + + +if __name__ == "__main__": + main() diff --git a/roles/lib_utils/src/class/import.py b/roles/lib_utils/src/class/import.py deleted file mode 100644 index 249e07228..000000000 --- a/roles/lib_utils/src/class/import.py +++ /dev/null @@ -1,11 +0,0 @@ -# flake8: noqa -# pylint: skip-file - -# pylint: disable=wrong-import-order -import json -import os -import re -# pylint: disable=import-error -import ruamel.yaml as yaml -import shutil -from ansible.module_utils.basic import AnsibleModule diff --git a/roles/lib_utils/src/class/repoquery.py b/roles/lib_utils/src/class/repoquery.py new file mode 100644 index 000000000..2447719e2 --- /dev/null +++ b/roles/lib_utils/src/class/repoquery.py @@ -0,0 +1,156 @@ +# pylint: skip-file +# flake8: noqa + + +class Repoquery(RepoqueryCLI): + ''' Class to wrap the repoquery + ''' + # pylint: disable=too-many-arguments + def __init__(self, name, query_type, show_duplicates, + match_version, 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.verbose = verbose + + if self.match_version: + self.show_duplicates = True + + self.query_format = "%{version}|%{release}|%{arch}|%{repo}|%{version}-%{release}" + + 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') + + 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.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'] = 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 ''' + + 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 + + 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['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'} diff --git a/roles/lib_utils/src/doc/repoquery b/roles/lib_utils/src/doc/repoquery new file mode 100644 index 000000000..82e273a42 --- /dev/null +++ b/roles/lib_utils/src/doc/repoquery @@ -0,0 +1,275 @@ +# flake8: noqa +# pylint: skip-file + +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 <mwoodson@redhat.com>" +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" +# } +# } + +''' diff --git a/roles/lib_utils/src/lib/import.py b/roles/lib_utils/src/lib/import.py new file mode 100644 index 000000000..d892353a1 --- /dev/null +++ b/roles/lib_utils/src/lib/import.py @@ -0,0 +1,14 @@ +# flake8: noqa +# pylint: skip-file + +# pylint: disable=wrong-import-order,wrong-import-position,unused-import + +from __future__ import print_function # noqa: F401 +import json # noqa: F401 +import os # noqa: F401 +import re # noqa: F401 +# pylint: disable=import-error +import ruamel.yaml as yaml # noqa: F401 +import shutil # noqa: F401 + +from ansible.module_utils.basic import AnsibleModule diff --git a/roles/lib_utils/src/lib/repoquery.py b/roles/lib_utils/src/lib/repoquery.py new file mode 100644 index 000000000..91ccd9815 --- /dev/null +++ b/roles/lib_utils/src/lib/repoquery.py @@ -0,0 +1,92 @@ +# pylint: skip-file +# flake8: noqa + +''' + 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 diff --git a/roles/lib_utils/src/sources.yml b/roles/lib_utils/src/sources.yml index 9cf3a0981..053b59f77 100644 --- a/roles/lib_utils/src/sources.yml +++ b/roles/lib_utils/src/sources.yml @@ -2,7 +2,16 @@ yedit.py: - doc/generated - doc/license -- class/import.py +- lib/import.py - doc/yedit - class/yedit.py - ansible/yedit.py + +repoquery.py: +- doc/generated +- doc/license +- lib/import.py +- doc/repoquery +- lib/repoquery.py +- class/repoquery.py +- ansible/repoquery.py diff --git a/roles/lib_utils/src/test/integration/repoquery.yml b/roles/lib_utils/src/test/integration/repoquery.yml new file mode 100755 index 000000000..425324387 --- /dev/null +++ b/roles/lib_utils/src/test/integration/repoquery.yml @@ -0,0 +1,136 @@ +#!/usr/bin/ansible-playbook --module-path=../../../library/ +--- +- hosts: localhost + gather_facts: no + + tasks: + - name: basic query test - Act + repoquery: + name: bash + register: rq_out + + - name: Set a real package version to be used later + set_fact: + latest_available_bash_version: "{{ rq_out.results.versions.latest }}" + latest_available_full_bash_version: "{{ rq_out.results.versions.latest_full }}" + + - name: basic query test - Assert + assert: + that: + - "rq_out.state == 'list'" + - "rq_out.changed == False" + - "rq_out.results.returncode == 0" + - "rq_out.results.package_found == True" + - "rq_out.results.package_name == 'bash'" + - "rq_out.results.versions.available_versions | length == 1" + - "rq_out.results.versions.available_versions_full | length == 1" + - "rq_out.results.versions.latest is defined" + - "rq_out.results.versions.latest in rq_out.results.versions.available_versions" + - "rq_out.results.versions.latest_full is defined" + - "rq_out.results.versions.latest_full in rq_out.results.versions.available_versions_full" + + - name: show_duplicates query test - Act + repoquery: + name: bash + show_duplicates: True + register: rq_out + + - name: show_duplicates query test - Assert + assert: + that: + - "rq_out.state == 'list'" + - "rq_out.changed == False" + - "rq_out.results.returncode == 0" + - "rq_out.results.package_found == True" + - "rq_out.results.package_name == 'bash'" + - "rq_out.results.versions.available_versions | length >= 1" + - "rq_out.results.versions.available_versions_full | length >= 1" + - "rq_out.results.versions.latest is defined" + - "rq_out.results.versions.latest in rq_out.results.versions.available_versions" + - "rq_out.results.versions.latest_full is defined" + - "rq_out.results.versions.latest_full in rq_out.results.versions.available_versions_full" + + - name: show_duplicates verbose query test - Act + repoquery: + name: bash + show_duplicates: True + verbose: True + register: rq_out + + - name: show_duplicates verbose query test - Assert + assert: + that: + - "rq_out.state == 'list'" + - "rq_out.changed == False" + - "rq_out.results.returncode == 0" + - "rq_out.results.package_found == True" + - "rq_out.results.package_name == 'bash'" + - "rq_out.results.raw_versions | length > 0" + - "rq_out.results.versions.available_versions | length > 0" + - "rq_out.results.versions.available_versions_full | length > 0" + - "rq_out.results.versions.latest is defined" + - "rq_out.results.versions.latest in rq_out.results.versions.available_versions" + - "rq_out.results.versions.latest_full is defined" + - "rq_out.results.versions.latest_full in rq_out.results.versions.available_versions_full" + + - name: query package does not exist query test - Act + repoquery: + name: somemadeuppackagenamethatwontmatch + show_duplicates: True + register: rq_out + + - name: query package does not exist query test - Assert + assert: + that: + - "rq_out.state == 'list'" + - "rq_out.changed == False" + - "rq_out.results.returncode == 0" + - "rq_out.results.package_found == False" + - "rq_out.results.results == ''" + + + - name: query match_version does not exist query test - Act + repoquery: + name: bash + show_duplicates: True + match_version: somemadeupversionnotexist + register: rq_out + + - name: query match_version does not exist query test - Assert + assert: + that: + - "rq_out.state == 'list'" + - "rq_out.changed == False" + - "rq_out.results.returncode == 0" + - "rq_out.results.package_found == True" + - "rq_out.results.package_name == 'bash'" + - "rq_out.results.versions.matched_version_found == False" + - "rq_out.results.versions.available_versions | length > 0" + - "rq_out.results.versions.available_versions_full | length > 0" + - "rq_out.results.versions.latest is defined" + - "rq_out.results.versions.latest in rq_out.results.versions.available_versions" + - "rq_out.results.versions.latest_full is defined" + - "rq_out.results.versions.latest_full in rq_out.results.versions.available_versions_full" + + - name: query match_version exists query test - Act + repoquery: + name: bash + show_duplicates: True + match_version: "{{ latest_available_bash_version }}" + register: rq_out + + - name: query match_version exists query test - Assert + assert: + that: + - "rq_out.state == 'list'" + - "rq_out.changed == False" + - "rq_out.results.returncode == 0" + - "rq_out.results.package_found == True" + - "rq_out.results.package_name == 'bash'" + - "rq_out.results.versions.matched_version_found == True" + - "rq_out.results.versions.available_versions | length > 0" + - "rq_out.results.versions.available_versions_full | length > 0" + - "rq_out.results.versions.latest is defined" + - "rq_out.results.versions.latest in rq_out.results.versions.available_versions" + - "rq_out.results.versions.latest_full is defined" + - "rq_out.results.versions.latest_full in rq_out.results.versions.available_versions_full" diff --git a/roles/lib_utils/src/test/unit/repoquery.py b/roles/lib_utils/src/test/unit/repoquery.py new file mode 100755 index 000000000..c487ab254 --- /dev/null +++ b/roles/lib_utils/src/test/unit/repoquery.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python2 +''' + Unit tests for repoquery +''' +# To run: +# ./repoquery.py +# +# . +# Ran 1 test in 0.002s +# +# OK + +import os +import sys +import unittest +import mock + +# Removing invalid variable names for tests so that I can +# keep them brief +# pylint: disable=invalid-name,no-name-in-module +# Disable import-error b/c our libraries aren't loaded in jenkins +# pylint: disable=import-error,wrong-import-position +# place class in our python path +module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501 +sys.path.insert(0, module_path) +from repoquery import Repoquery # noqa: E402 + + +class RepoQueryTest(unittest.TestCase): + ''' + Test class for RepoQuery + ''' + + def setUp(self): + ''' setup method for other tests ''' + pass + + @mock.patch('repoquery._run') + def test_querying_a_package(self, mock_cmd): + ''' Testing querying a package ''' + + # Arrange + + # run_ansible input parameters + params = { + 'state': 'list', + 'name': 'bash', + 'query_type': 'repos', + 'verbose': False, + 'show_duplicates': False, + 'match_version': None, + } + + valid_stderr = '''Repo rhel-7-server-extras-rpms forced skip_if_unavailable=True due to: /etc/pki/entitlement/3268107132875399464-key.pem + Repo rhel-7-server-rpms forced skip_if_unavailable=True due to: /etc/pki/entitlement/4128505182875899164-key.pem''' # not real + + # Return values of our mocked function call. These get returned once per call. + mock_cmd.side_effect = [ + (0, '4.2.46|21.el7_3|x86_64|rhel-7-server-rpms|4.2.46-21.el7_3', valid_stderr), # first call to the mock + ] + + # Act + results = Repoquery.run_ansible(params, False) + + # Assert + self.assertEqual(results['state'], 'list') + self.assertFalse(results['changed']) + self.assertTrue(results['results']['package_found']) + self.assertEqual(results['results']['returncode'], 0) + self.assertEqual(results['results']['package_name'], 'bash') + self.assertEqual(results['results']['versions'], {'latest_full': '4.2.46-21.el7_3', + 'available_versions': ['4.2.46'], + 'available_versions_full': ['4.2.46-21.el7_3'], + 'latest': '4.2.46'}) + + # Making sure our mock was called as we expected + mock_cmd.assert_has_calls([ + mock.call(['/usr/bin/repoquery', '--plugins', '--quiet', '--pkgnarrow=repos', '--queryformat=%{version}|%{release}|%{arch}|%{repo}|%{version}-%{release}', 'bash']), + ]) + + def tearDown(self): + '''TearDown method''' + pass + + +if __name__ == "__main__": + unittest.main() |