diff --git a/lib/ansible/modules/packaging/os/rhsm_repository_release.py b/lib/ansible/modules/packaging/os/rhsm_repository_release.py new file mode 100644 index 00000000000..e9fdc6136e5 --- /dev/null +++ b/lib/ansible/modules/packaging/os/rhsm_repository_release.py @@ -0,0 +1,130 @@ +#!/usr/bin/python + +# (c) 2018, Sean Myers +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = ''' +--- +module: rhsm_repository_release +short_description: Set or Unset RHSM Repository Release version +version_added: '2.8' +description: + - Sets or unsets the release version used by RHSM repositories. +notes: + - This module will fail on an unregistered system. + Use the C(redhat_subscription) module to register a system + prior to setting the RHSM repository release. +requirements: + - Red Hat Enterprise Linux 6+ with subscription-manager installed +options: + release: + description: + - RHSM repository release version to use (use null to unset) + required: true +author: + - Sean Myers (@seandst) +''' + +EXAMPLES = ''' +# Set release version to 7.1 +- name: Set RHSM repository release version + rhsm_repository_release: + release: "7.1" + +# Set release version to 6Server +- name: Set RHSM repository release version + rhsm_repository_release: + release: "6Server" + +# Unset release version +- name: Unset RHSM repository release release + rhsm_repository_release: + release: null +''' + +RETURN = ''' +current_release: + description: The current RHSM repository release version value + returned: success + type: str +''' + +from ansible.module_utils.basic import AnsibleModule + +import re + +# Matches release-like values such as 7.2, 6.10, 10Server, +# but rejects unlikely values, like 100Server, 100.0, 1.100, etc. +release_matcher = re.compile(r'\b\d{1,2}(?:\.\d{1,2}|Server)\b') + + +def _sm_release(module, *args): + # pass args to s-m release, e.g. _sm_release(module, '--set', '0.1') becomes + # "subscription-manager release --set 0.1" + sm_bin = module.get_bin_path('subscription-manager', required=True) + cmd = '{0} release {1}'.format(sm_bin, " ".join(args)) + # delegate nonzero rc handling to run_command + return module.run_command(cmd, check_rc=True) + + +def get_release(module): + # Get the current release version, or None if release unset + rc, out, err = _sm_release(module, '--show') + try: + match = release_matcher.findall(out)[0] + except IndexError: + # 0'th index did not exist; no matches + match = None + + return match + + +def set_release(module, release): + # Set current release version, or unset if release is None + if release is None: + args = ('--unset',) + else: + args = ('--set', release) + + return _sm_release(module, *args) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + release=dict(type='str', required=True), + ), + supports_check_mode=True + ) + + target_release = module.params['release'] + + # sanity check: the target release at least looks like a valid release + if target_release and not release_matcher.findall(target_release): + module.fail_json(msg='"{0}" does not appear to be a valid release.'.format(target_release)) + + # Will fail with useful error from s-m if system not subscribed + current_release = get_release(module) + + changed = (target_release != current_release) + if not module.check_mode and changed: + set_release(module, target_release) + # If setting the release fails, then a fail_json would have exited with + # the s-m error, e.g. "No releases match '7.20'...". If not, then the + # current release is now set to the target release (job's done) + current_release = target_release + + module.exit_json(current_release=current_release, changed=changed) + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/packaging/os/test_rhsm_repository_release.py b/test/units/modules/packaging/os/test_rhsm_repository_release.py new file mode 100644 index 00000000000..d3c7128ae53 --- /dev/null +++ b/test/units/modules/packaging/os/test_rhsm_repository_release.py @@ -0,0 +1,141 @@ +# (c) 2018, Sean Myers +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from units.compat.mock import call, patch +from ansible.modules.packaging.os import rhsm_repository_release +from units.modules.utils import ( + AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args) + + +class RhsmRepositoryReleaseModuleTestCase(ModuleTestCase): + module = rhsm_repository_release + + def setUp(self): + super(RhsmRepositoryReleaseModuleTestCase, self).setUp() + + # Mainly interested that the subscription-manager calls are right + # based on the module args, so patch out run_command in the module. + # returns (rc, out, err) structure + self.mock_run_command = patch('ansible.modules.packaging.os.rhsm_repository_release.' + 'AnsibleModule.run_command') + self.module_main_command = self.mock_run_command.start() + + # Module does a get_bin_path check before every run_command call + self.mock_get_bin_path = patch('ansible.modules.packaging.os.rhsm_repository_release.' + 'AnsibleModule.get_bin_path') + self.get_bin_path = self.mock_get_bin_path.start() + self.get_bin_path.return_value = '/testbin/subscription-manager' + + def tearDown(self): + self.mock_run_command.stop() + self.mock_get_bin_path.stop() + super(RhsmRepositoryReleaseModuleTestCase, self).tearDown() + + def module_main(self, exit_exc): + with self.assertRaises(exit_exc) as exc: + self.module.main() + return exc.exception.args[0] + + def test_release_set(self): + # test that the module attempts to change the release when the current + # release is not the same as the user-specific target release + set_module_args({'release': '7.5'}) + self.module_main_command.side_effect = [ + # first call, get_release: returns different version so set_release is called + (0, '7.4', ''), + # second call, set_release: just needs to exit with 0 rc + (0, '', ''), + ] + + result = self.module_main(AnsibleExitJson) + + self.assertTrue(result['changed']) + self.assertEqual('7.5', result['current_release']) + self.module_main_command.assert_has_calls([ + call('/testbin/subscription-manager release --show', check_rc=True), + call('/testbin/subscription-manager release --set 7.5', check_rc=True), + ]) + + def test_release_set_idempotent(self): + # test that the module does not attempt to change the release when + # the current release matches the user-specified target release + set_module_args({'release': '7.5'}) + self.module_main_command.side_effect = [ + # first call, get_release: returns same version, set_release is not called + (0, '7.5', ''), + ] + + result = self.module_main(AnsibleExitJson) + + self.assertFalse(result['changed']) + self.assertEqual('7.5', result['current_release']) + self.module_main_command.assert_has_calls([ + call('/testbin/subscription-manager release --show', check_rc=True), + ]) + + def test_release_unset(self): + # test that the module attempts to change the release when the current + # release is not the same as the user-specific target release + set_module_args({'release': None}) + self.module_main_command.side_effect = [ + # first call, get_release: returns version so set_release is called + (0, '7.5', ''), + # second call, set_release: just needs to exit with 0 rc + (0, '', ''), + ] + + result = self.module_main(AnsibleExitJson) + + self.assertTrue(result['changed']) + self.assertIsNone(result['current_release']) + self.module_main_command.assert_has_calls([ + call('/testbin/subscription-manager release --show', check_rc=True), + call('/testbin/subscription-manager release --unset', check_rc=True), + ]) + + def test_release_unset_idempotent(self): + # test that the module attempts to change the release when the current + # release is not the same as the user-specific target release + set_module_args({'release': None}) + self.module_main_command.side_effect = [ + # first call, get_release: returns no version, set_release is not called + (0, 'Release not set', ''), + ] + + result = self.module_main(AnsibleExitJson) + + self.assertFalse(result['changed']) + self.assertIsNone(result['current_release']) + self.module_main_command.assert_has_calls([ + call('/testbin/subscription-manager release --show', check_rc=True), + ]) + + def test_release_insane(self): + # test that insane values for release trigger fail_json + insane_value = 'this is an insane release value' + set_module_args({'release': insane_value}) + + result = self.module_main(AnsibleFailJson) + + # also ensure that the fail msg includes the insane value + self.assertIn(insane_value, result['msg']) + + def test_release_matcher(self): + # throw a few values at the release matcher -- only sane_values should match + sane_values = ['1Server', '10Server', '1.10', '10.0'] + insane_values = [ + '6server', # lowercase 's' + '100Server', # excessively long 'x' component + '100.0', # excessively long 'x' component + '6.100', # excessively long 'y' component + '100.100', # excessively long 'x' and 'y' components + ] + + matches = self.module.release_matcher.findall(' '.join(sane_values + insane_values)) + + # matches should be returned in the same order they were parsed, + # so sorting shouldn't be necessary here + self.assertEqual(matches, sane_values)