Repository: ansible/ansible

Issue:
redhat_subscription should not allow attaching subscriptions by name
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report

##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
redhat_subscription

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
ansible 2.2.0.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides

```

##### CONFIGURATION
<!---
Mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->

##### OS / ENVIRONMENT
<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say “N/A” for anything that is not platform-specific.
-->
RHEL 7.3

##### SUMMARY
<!--- Explain the problem briefly -->
redhat_subscription manager is overloading the value type of the original subscription manager.  This causes confusion and over-consumption of entitlements.

The official Red Hat Subscription Manager allows attaching subscriptions by Pool ID, NOT by Pool Name.
```
[root@rhel7 ~]# subscription-manager attach --help
Usage: subscription-manager attach [OPTIONS]

Attach a specified subscription to the registered system

Options:
  -h, --help            show this help message and exit
  --proxy=PROXY_URL     proxy URL in the form of proxy_hostname:proxy_port
  --proxyuser=PROXY_USER
                        user for HTTP proxy with basic authentication
  --proxypassword=PROXY_PASSWORD
                        password for HTTP proxy with basic authentication
  --pool=POOL           the ID of the pool to attach (can be specified more
                        than once)
  --quantity=QUANTITY   number of subscriptions to attach
  --auto                Automatically attach compatible subscriptions to this
                        system. This is the default action.
  --servicelevel=SERVICE_LEVEL
                        service level to apply to this system
  --file=FILE           A file from which to read pool IDs. If a hyphen is
                        provided, pool IDs will be read from stdin.
[root@rhel7 ~]# 
```

Because this module is allowing a user to specify a name and trying to do its own logic and parsing of available Pool IDs, it is incorrectly attempting to extend the capabilities of the official subscription manager.  It also does it poorly, allowing multiple subscriptions to be consumed when they were not desired.

Large customers will have many duplicate subscriptions of the same name due to growth and purchases made at different times.  This could have a very large impact to large customers.

The feature to enable using the Pool ID is a good start, but we also must remove the capability of adding by Name.  This Ansible module should not attempt to do more than the original tool allows.
https://github.com/ansible/ansible-modules-core/pull/4603

##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used.
-->

<!--- Paste example playbooks or commands between quotes below -->
```yaml
- redhat_subscription:
    state: present
    username: "{{ rhn_username }}"
    password: "{{ rhn_password }}"
    pool: '^Employee SKU$'
#    pool: "{{ POOL_ID }}"
  when: ansible_distribution == 'RedHat'
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
Only a single Pool ID attached, presumably the first one matched.  But certainly not ALL matches.

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->
I have altered the values below to protect our actual Pool IDs (XXX, YYY, ZZZ).  I can provide actual examples privately.

<!--- Paste verbatim command output between quotes below -->
```
<util7vm> SSH: EXEC ssh -C -o ControlMaster=auto -o ControlPersist=60s -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o ConnectTimeout=10 -o ControlPath=/root/.ansible/cp/ansible-ssh-%h-%p-%r -tt util7vm '/bin/sh -c '"'"'sudo -H -S -n -u root /bin/sh -c '"'"'"'"'"'"'"'"'echo BECOME-SUCCESS-pyeuamdvwtzvyzuxvbczhjotjfyacqxn; /usr/bin/python /root/.ansible/tmp/ansible-tmp-1481978149.12-197063747409681/redhat_subscription.py; rm -rf "/root/.ansible/tmp/ansible-tmp-1481978149.12-197063747409681/" > /dev/null 2>&1'"'"'"'"'"'"'"'"' && sleep 0'"'"''
changed: [util7vm] => {
    "changed": true, 
    "invocation": {
        "module_args": {
            "activationkey": null, 
            "autosubscribe": false, 
            "consumer_id": null, 
            "consumer_name": null, 
            "consumer_type": null, 
            "environment": null, 
            "force_register": false, 
            "org_id": null, 
            "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
            "pool": "^Employee SKU$", 
            "rhsm_baseurl": "https://cdn.redhat.com", 
            "server_hostname": "subscription.rhn.redhat.com", 
            "server_insecure": "0", 
            "state": "present", 
            "username": "my-rhn-support-username"
        }, 
        "module_name": "redhat_subscription"
    }, 
    "subscribed_pool_ids": [
        "XXXXXXXXXXXXXXXXXXXXXXXXXXX",   
        "YYYYYYYYYYYYYYYYYYYYYYYY", 
        "ZZZZZZZZZZZZZZZZZZZZZZZZZZZ"
    ], 
    "unsubscribed_serials": []
}

PLAY RECAP *********************************************************************
util7vm                    : ok=2    changed=1    unreachable=0    failed=0   
```


Discussion:
cc @barnabycourt, @alikins 
I didn't even managed to make it work by specifying the alphanumeric pool ID, system just gets registered but not attached to pool.

Running subscription-manager attach --pool=$ID with the same from CLI works fine.

Thanks
Not sure how to fix this. The 'pool' argument is currently used as a catch all glob for pools and products, which is mostly wrong (matches too eagerly against product name and now also pool id). 

Making it just match against pool id is a closer match to subscription-manager itself, but it would also break matching on "pool: 'Some Product Name From The Pool Blurb'"

subscription-manager itself can do some of the pool filtering/globbing, but not on older versions.
Working on a patch to change the logic to:

    If the pools regex matches any pool ids, then subscribe only to those pools. 
    If there are no pool id matches, then attempt to match the regexp and pool names.
    If there are pool name matches, subscribe to those pools.

That would allow:

``` yaml
- redhat_subscription:
    state: present
    username: "{{ rhn_username }}"
    password: "{{ rhn_password }}"
    pool: 'd8b224dd-c003-4cfe-a51e-5017d336f302'
  when: ansible_distribution == 'RedHat'
```

to be specific, and also allow:

``` yaml
- redhat_subscription:
    state: present
    username: "{{ rhn_username }}"
    password: "{{ rhn_password }}"
    pool: '^Red Hat Enterprise Super Awesome OS Turbo.*'
  when: ansible_distribution == 'RedHat'
```

The latter can still match lots of stuff though. Subscribing to a product name is kind of an hard case to support across all versions of subscription-manager.

Resolving patch:
diff --git a/lib/ansible/modules/packaging/os/redhat_subscription.py b/lib/ansible/modules/packaging/os/redhat_subscription.py
--- a/lib/ansible/modules/packaging/os/redhat_subscription.py
+++ b/lib/ansible/modules/packaging/os/redhat_subscription.py
@@ -359,6 +359,42 @@ def unregister(self):
         rc, stderr, stdout = self.module.run_command(args, check_rc=True)
 
     def subscribe(self, regexp):
+        '''
+            Subscribe current system to available pools matching the specified
+            regular expression. It matches regexp against available pool ids first.
+            If any pool ids match, subscribe to those pools and return.
+
+            If no pool ids match, then match regexp against available pool product
+            names. Note this can still easily match many many pools. Then subscribe
+            to those pools.
+
+            Since a pool id is a more specific match, we only fallback to matching
+            against names if we didnt match pool ids.
+
+            Raises:
+              * Exception - if error occurs while running command
+        '''
+        # See https://github.com/ansible/ansible/issues/19466
+
+        # subscribe to pools whose pool id matches regexp (and only the pool id)
+        subscribed_pool_ids = self.subscribe_pool(regexp)
+
+        # If we found any matches, we are done
+        # Don't attempt to match pools by product name
+        if subscribed_pool_ids:
+            return subscribed_pool_ids
+
+        # We didn't match any pool ids.
+        # Now try subscribing to pools based on product name match
+        # Note: This can match lots of product names.
+        subscribed_by_product_pool_ids = self.subscribe_product(regexp)
+        if subscribed_by_product_pool_ids:
+            return subscribed_by_product_pool_ids
+
+        # no matches
+        return []
+
+    def subscribe_pool(self, regexp):
         '''
             Subscribe current system to available pools matching the specified
             regular expression
@@ -370,7 +406,24 @@ def subscribe(self, regexp):
         available_pools = RhsmPools(self.module)
 
         subscribed_pool_ids = []
-        for pool in available_pools.filter(regexp):
+        for pool in available_pools.filter_pools(regexp):
+            pool.subscribe()
+            subscribed_pool_ids.append(pool.get_pool_id())
+        return subscribed_pool_ids
+
+    def subscribe_product(self, regexp):
+        '''
+            Subscribe current system to available pools matching the specified
+            regular expression
+            Raises:
+              * Exception - if error occurs while running command
+        '''
+
+        # Available pools ready for subscription
+        available_pools = RhsmPools(self.module)
+
+        subscribed_pool_ids = []
+        for pool in available_pools.filter_products(regexp):
             pool.subscribe()
             subscribed_pool_ids.append(pool.get_pool_id())
         return subscribed_pool_ids
@@ -378,7 +431,8 @@ def subscribe(self, regexp):
     def update_subscriptions(self, regexp):
         changed = False
         consumed_pools = RhsmPools(self.module, consumed=True)
-        pool_ids_to_keep = [p.get_pool_id() for p in consumed_pools.filter(regexp)]
+        pool_ids_to_keep = [p.get_pool_id() for p in consumed_pools.filter_pools(regexp)]
+        pool_ids_to_keep.extend([p.get_pool_id() for p in consumed_pools.filter_products(regexp)])
 
         serials_to_remove = [p.Serial for p in consumed_pools if p.get_pool_id() not in pool_ids_to_keep]
         serials = self.unsubscribe(serials=serials_to_remove)
@@ -464,15 +518,22 @@ def _load_product_list(self, consumed=False):
                     # warnings.warn("Unhandled subscription key/value: %s/%s" % (key,value))
         return products
 
-    def filter(self, regexp='^$'):
+    def filter_pools(self, regexp='^$'):
         '''
-            Return a list of RhsmPools whose name matches the provided regular expression
+            Return a list of RhsmPools whose pool id matches the provided regular expression
         '''
         r = re.compile(regexp)
         for product in self.products:
-            if r.search(product._name):
+            if r.search(product.get_pool_id()):
                 yield product
-            if r.search(product.PoolID):
+
+    def filter_products(self, regexp='^$'):
+        '''
+            Return a list of RhsmPools whose product name matches the provided regular expression
+        '''
+        r = re.compile(regexp)
+        for product in self.products:
+            if r.search(product._name):
                 yield product
 
 


Test patch: