Repository: ansible/ansible

Issue:
Enable/disable/list repos using redhat_subscription module
_From @ghost on March 22, 2016 11:7_

##### Issue Type:
- Feature Idea
##### Plugin Name:

redhat_subscription

##### COMPONENT NAME
lib/ansible/modules/packaging/os/redhat_subscription.py

##### Ansible Version:

N/A
##### Ansible Configuration:

N/A
##### Environment:

N/A
##### Summary:

redhat_subscription module does not contains options to enable/disable/list repos.
When connecting a server to the rhn or satellite and assign a subscription without activation keys, its necessary to enable or disable repos.
##### Steps To Reproduce:

subscription-manager command provides those options:
- Enable: subscription-manager repos --enable repo [--enable repo] [--enable regexp]
- Disable: subscription-manager repos --disable repo [--disable repo] [--disable regexpr]
- List all: subscription-manager repos --list
- List enabled: subscription-manager repos --list-enabled
- List disabled: subscription-manager repos --list-disabled


_Copied from original issue: ansible/ansible-modules-core#3300_


Discussion:
_From @jfenal on June 8, 2016 23:27_

As for needs:
- get list of repos currently enabled on the machine
- enable if not yet enabled a repos or a list of repos (in one command, as subscription-manager is slooooow)
- same for disabling a list of repos, with special '*' to disable all repos (and build on a clean state)

_From @samdoran on June 13, 2016 19:55_

I would like this functionality as well. I'm wondering about the best way to go about implementing it.

Would it make sense to add this to the existing `redhat_subscription` module given the `state:` parameter would then be ambiguous, i.e., is the _repo_ enabled ( `repos --enable` vs `repos --disable`) or is the _subscription_ enabled (`register` vs. `unregister`)? Adding a `repo_state` parameter would remove the amibiguity, but seems a little gross.

Or would it be better to split this out into a separate `redhat_repository` module so it's clear exactly what is being enabled/disabled ([singular name](https://github.com/ansible/proposals/issues/10) to make @abadger happy)?

_From @minusdavid on July 22, 2016 1:2_

Since the modules "rhn_channel" and "rhn_register" already exist, I think it would be reasonable and consistent to create a new module called "redhat_repository" to go alongside "redhat_subscription". 

By the way, I'd also like this functionality. I wonder how people are managing their repos using subscription-manager in Ansible at the moment. Using the command/shell modules I'm guessing?

_From @minusdavid on July 22, 2016 7:11_

As a short-term measure, I'm writing a custom "site_facts" module which will parse redhat.repo, and then I can use that with some conditionals to do "command: subscription-manager repos...".

It's not really that different from the concept behind http://docs.ansible.com/ansible/include_vars_module.html I imagine... except instead of YAML/JSON it's the INI type format that Python uses with ConfigParser.

I'm going to look at it again on Monday, and if I can make it nice and clean, I'll post it on my own Github account for anyone else who wants to use it. 

_From @jfenal on July 22, 2016 14:56_

I've started writing such a module, but was interrupted before I could finish a first version I could test.
Have a look at : https://github.com/jfenal/ansible-modules-jfenal
The logic is mostly there, BUT not tested at all, no JSON output, no error handling. It clearly lacks some more code to be functional, but close.

Please note my disclaimer : my first ansible module, and moreover, my first attempt to write (with lots of copy/pasting) python code. Cave canem :)

_From @samdoran on July 22, 2016 19:41_

I have a [role](https://galaxy.ansible.com/samdoran/redhat-subscription/) that's using `command` to manage subscriptions and repos.

_From @minusdavid on July 23, 2016 14:55_

Kudos for your work, guys!

@samdoran I take it that's not idempotent, so you use tags to just run it once during initial setup?

@jfenal Looks ambitious! I opted for a local custom module, which you can see at this [Gist](https://gist.github.com/minusdavid/2774a8d6df5ded46b1b63e05f214da01) 

_From @minusdavid on July 23, 2016 14:57_

Btw, it works well enough. I like the idea of a core redhat_repository module best, but this will certainly do the job for now. 

_From @minusdavid on July 23, 2016 15:3_

Just realized that you both appear to work at Red Hat.

I'm super curious about if/how RH is developing Ansible to help manage RHEL...

I've only been using RHEL for a couple of weeks, but I've used Ansible for a couple years. So far so good using it to setup RHEL servers. The only real sticking point so far has been this issue. Well, and this [one](https://github.com/ansible/ansible/issues/16006), but perhaps this comment section isn't the best place for discussing such things.

_From @detiber on September 8, 2016 17:10_

@barnabycourt not sure if you've seen this bug

_From @ansibot on October 6, 2016 14:57_

@barnabycourt, @alikins, ping. This issue is still waiting on your response.
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)

<!--- boilerplate: issue_renotify_maintainer --->

_From @jfenal on November 28, 2016 22:43_

https://github.com/ansible/ansible-modules-extras/pull/3559
_From @ansibot on December 7, 2016 18:6_

This repository has been locked. All new issues and pull requests should be filed in https://github.com/ansible/ansible

Please read through the <a href="http://docs.ansible.com/ansible/dev_guide/repomerge.html#move-issues-and-prs-to-new-repo">repomerge</a> page in the dev guide. The guide contains links to tools which automatically move your issue or pull request to the ansible/ansible repo.

<!-- boilerplate: repomerge -->
_From @jfenal on December 27, 2016 17:8_

For followers of this thread, please have a look at https://github.com/jfenal/ansible-modules-jfenal, all feedbacks are welcome before I submit it to ansible core.
_From @detiber on January 3, 2017 19:46_

@barnabycourt might be of interest to you.
@rvalente Greetings! Thanks for taking the time to open this issue. In order for the community to handle your issue effectively, we need a bit more information. 

Here are the items we could not find in your description:
- component name

Please set the description of this issue with this template:
https://raw.githubusercontent.com/ansible/ansible/devel/.github/ISSUE_TEMPLATE.md

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: issue_missing_data --->
ping @alikins, @barnabycourt, @kahowell 
I submitted a PR for a new module redhat_subscription based on the needs described in this comment and on my sysadmin experience on subscription-manager. I'm glad to receive suggestions and review to improve it.

Resolving patch:
diff --git a/lib/ansible/modules/packaging/os/rhsm_repository.py b/lib/ansible/modules/packaging/os/rhsm_repository.py
new file mode 100644
--- /dev/null
+++ b/lib/ansible/modules/packaging/os/rhsm_repository.py
@@ -0,0 +1,227 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2017, Giovanni Sciortino (@giovannisciortino)
+# 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
+short_description: Manage RHSM repositories using the subscription-manager command
+description:
+  - Manage(List/Enable/Disable) RHSM repositories to the Red Hat Subscription
+    Management entitlement platform using the C(subscription-manager) command.
+version_added: '2.5'
+author: Giovanni Sciortino (@giovannisciortino)
+notes:
+  - In order to manage RHSM repositories the system must be already registered
+    to RHSM manually or using the ansible module redhat_subscription.
+
+requirements:
+  - subscription-manager
+options:
+  state:
+    description:
+      - If state is equal to present or disabled, indicates the desired
+        repository state.
+    choices: [present, absent]
+    required: True
+    default: "present"
+  name:
+    description:
+      - The ID of repositories to enable.
+      - To operate on several repositories this can accept a comma separated
+        list or a YAML list.
+    required: True
+'''
+
+EXAMPLES = '''
+- name: Enable a RHSM repository
+  rhsm_repository:
+    name: rhel-7-server-rpms
+
+- name: Disable all RHSM repositories
+  rhsm_repository:
+    name: '*'
+    state: disabled
+
+- name: Enable all repositories starting with rhel-6-server
+  rhsm_repository:
+    name: rhel-6-server*
+    state: enabled
+
+- name: Disable all repositories except rhel-7-server-rpms
+  rhsm_repository:
+    name: "{{ item }}"
+    state: disabled
+  with_items: "{{
+    rhsm_repository.repositories |
+    map(attribute='id') |
+    difference(['rhel-7-server-rpms']) }}"
+'''
+
+RETURN = '''
+repositories:
+  description:
+    - The list of RHSM repositories with their states.
+    - When this module is used to change the repositories states, this list contains the updated states after the changes.
+  returned: success
+  type: list
+'''
+
+import re
+import os
+from fnmatch import fnmatch
+from copy import deepcopy
+from ansible.module_utils.basic import AnsibleModule
+
+
+def run_subscription_manager(module, arguments):
+    # Execute subuscription-manager with arguments and manage common errors
+    rhsm_bin = module.get_bin_path('subscription-manager')
+    if not rhsm_bin:
+        module.fail_json(msg='The executable file subscription-manager was not found in PATH')
+
+    rc, out, err = module.run_command("%s %s" % (rhsm_bin, " ".join(arguments)))
+
+    if rc == 1 and (err == 'The password you typed is invalid.\nPlease try again.\n' or os.getuid() != 0):
+        module.fail_json(msg='The executable file subscription-manager must be run using root privileges')
+    elif rc == 0 and out == 'This system has no repositories available through subscriptions.\n':
+        module.fail_json(msg='This system has no repositories available through subscriptions')
+    elif rc == 1:
+        module.fail_json(msg='subscription-manager failed with the following error: %s' % err)
+    else:
+        return rc, out, err
+
+
+def get_repository_list(module, list_parameter):
+    # Generate RHSM repository list and return a list of dict
+    if list_parameter == 'list_enabled':
+        rhsm_arguments = ['repos', '--list-enabled']
+    elif list_parameter == 'list_disabled':
+        rhsm_arguments = ['repos', '--list-disabled']
+    elif list_parameter == 'list':
+        rhsm_arguments = ['repos', '--list']
+    rc, out, err = run_subscription_manager(module, rhsm_arguments)
+
+    skip_lines = [
+        '+----------------------------------------------------------+',
+        '    Available Repositories in /etc/yum.repos.d/redhat.repo'
+    ]
+    repo_id_re_str = r'Repo ID:   (.*)'
+    repo_name_re_str = r'Repo Name: (.*)'
+    repo_url_re_str = r'Repo URL:  (.*)'
+    repo_enabled_re_str = r'Enabled:   (.*)'
+
+    repo_id = ''
+    repo_name = ''
+    repo_url = ''
+    repo_enabled = ''
+
+    repo_result = []
+
+    for line in out.split('\n'):
+        if line in skip_lines:
+            continue
+
+        repo_id_re = re.match(repo_id_re_str, line)
+        if repo_id_re:
+            repo_id = repo_id_re.group(1)
+            continue
+
+        repo_name_re = re.match(repo_name_re_str, line)
+        if repo_name_re:
+            repo_name = repo_name_re.group(1)
+            continue
+
+        repo_url_re = re.match(repo_url_re_str, line)
+        if repo_url_re:
+            repo_url = repo_url_re.group(1)
+            continue
+
+        repo_enabled_re = re.match(repo_enabled_re_str, line)
+        if repo_enabled_re:
+            repo_enabled = repo_enabled_re.group(1)
+
+        repo = {
+            "id": repo_id,
+            "name": repo_name,
+            "url": repo_url,
+            "enabled": True if repo_enabled == '1' else False
+        }
+        repo_result.append(repo)
+    return repo_result
+
+
+def repository_modify(module, state, name):
+    name = set(name)
+    current_repo_list = get_repository_list(module, 'list')
+    updated_repo_list = deepcopy(current_repo_list)
+    matched_existing_repo = {}
+    for repoid in name:
+        matched_existing_repo[repoid] = []
+        for idx, repo in enumerate(current_repo_list):
+            if fnmatch(repo['id'], repoid):
+                matched_existing_repo[repoid].append(repo)
+                # Update current_repo_list to return it as result variable
+                updated_repo_list[idx]['enabled'] = True if state == 'enabled' else False
+
+    changed = False
+    results = []
+    diff_before = ""
+    diff_after = ""
+    rhsm_arguments = ['repos']
+
+    for repoid in matched_existing_repo:
+        if len(matched_existing_repo[repoid]) == 0:
+            results.append("%s is not a valid repository ID" % repoid)
+            module.fail_json(results=results, msg="%s is not a valid repository ID" % repoid)
+        for repo in matched_existing_repo[repoid]:
+            if state == 'disabled':
+                if repo['enabled']:
+                    changed = True
+                    diff_before += "Repository '%s' is enabled for this system\n" % repo['id']
+                    diff_after += "Repository '%s' is disabled for this system\n" % repo['id']
+                results.append("Repository '%s' is disabled for this system" % repo['id'])
+                rhsm_arguments += ['--disable', repo['id']]
+            elif state == 'enabled':
+                if not repo['enabled']:
+                    changed = True
+                    diff_before += "Repository '%s' is disabled for this system\n" % repo['id']
+                    diff_after += "Repository '%s' is enabled for this system\n" % repo['id']
+                results.append("Repository '%s' is enabled for this system" % repo['id'])
+                rhsm_arguments += ['--enable', repo['id']]
+
+    diff = {'before': diff_before,
+            'after': diff_after,
+            'before_header': "RHSM repositories",
+            'after_header': "RHSM repositories"}
+
+    if not module.check_mode:
+        rc, out, err = run_subscription_manager(module, rhsm_arguments)
+        results = out.split('\n')
+    module.exit_json(results=results, changed=changed, repositories=updated_repo_list, diff=diff)
+
+
+def main():
+    module = AnsibleModule(
+        argument_spec=dict(
+            name=dict(type='list', required=True),
+            state=dict(choices=['enabled', 'disabled'], default='enabled'),
+        ),
+        supports_check_mode=True,
+    )
+    name = module.params['name']
+    state = module.params['state']
+
+    repository_modify(module, state, name)
+
+
+if __name__ == '__main__':
+    main()


Test patch: