Repository: ansible/ansible

Issue:
nxos_snmp_user issues
<!---
Verify first that your issue/request is not already reported on GitHub.
THIS FORM WILL BE READ BY A MACHINE, COMPLETE ALL SECTIONS AS DESCRIBED.
Also test if the latest release, and devel branch are affected too.
ALWAYS add information AFTER (OUTSIDE) these html comments.
Otherwise it may end up being automatically closed by our bot. -->

##### ISSUE TYPE
<!--- Pick one below and delete the rest -->
 - Bug Report

##### COMPONENT NAME
<!--- Insert, BELOW THIS COMMENT, the name of the module, plugin, task or feature.
Do not include extra details here, e.g. "vyos_command" not "the network module vyos_command" or the full path-->
nxos_snmp_user

##### ANSIBLE VERSION
<!--- Paste, BELOW THIS COMMENT, verbatim output from "ansible --version" between quotes below -->
```
ansible 2.6.0 (devel fed20b825f) last updated 2018/02/15 12:51:12 (GMT -400)
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /root/agents-ci/ansible/lib/ansible
  executable location = /root/agents-ci/ansible/bin/ansible
  python version = 2.7.6 (default, Oct 26 2016, 20:30:19) [GCC 4.8.4]
```

##### OS / ENVIRONMENT
<!--- Mention, BELOW THIS COMMENT, the OS you are running Ansible from, and the OS you are
managing, or say "N/A" for anything that is not platform-specific.
Also mention the specific version of what you are trying to control,
e.g. if this is a network bug the version of firmware on the network device.-->
Ansible Server : Ubuntu 14.04
Device: N7K running 7.0(3)D1(1)
##### SUMMARY
<!--- Explain the problem briefly -->

##### 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. -->

There are few issues with nxos_snmp_user module
1. group is not a required parameter. When group is not specified, the platform does accept the CLI and assigns the default group (usually network-operator).
2. more than one group cannot be added properly
3. group cannot be removed after adding without removing the user itself.
4. There are also platform bugs where the 'show snmp user | json' output is not consistent across older platforms and the code fails for these old platforms.
5. dead code

Note: I will open a PR shortly to address these issues.


Discussion:

Resolving patch:
diff --git a/lib/ansible/modules/network/nxos/nxos_snmp_user.py b/lib/ansible/modules/network/nxos/nxos_snmp_user.py
--- a/lib/ansible/modules/network/nxos/nxos_snmp_user.py
+++ b/lib/ansible/modules/network/nxos/nxos_snmp_user.py
@@ -42,7 +42,15 @@
     group:
         description:
             - Group to which the user will belong to.
-        required: true
+              If state = present, and the user is existing,
+              the group is added to the user. If the user
+              is not existing, user entry is created with this
+              group argument.
+              If state = absent, only the group is removed from the
+              user entry. However, to maintain backward compatibility,
+              if the existing user belongs to only one group, and if
+              group argument is same as the existing user's group,
+              then the user entry also is deleted.
     authentication:
         description:
             - Authentication parameters for the user.
@@ -50,9 +58,11 @@
     pwd:
         description:
             - Authentication password when using md5 or sha.
+              This is not idempotent
     privacy:
         description:
             - Privacy password for the user.
+              This is not idempotent
     encrypt:
         description:
             - Enables AES-128 bit encryption when using privacy password.
@@ -148,8 +158,18 @@ def get_snmp_user(user, module):
             privkey = 'priv'
             grpkey = 'group'
 
-        resource_table = body[0][tablekey][rowkey]
-        resource['user'] = str(resource_table['user'])
+        rt = body[0][tablekey][rowkey]
+        # on some older platforms, all groups except the 1st one
+        # are in list elements by themselves and they are
+        # indexed by 'user'. This is due to a platform bug.
+        # Get first element if rt is a list due to the bug
+        # or if there is no bug, parse rt directly
+        if isinstance(rt, list):
+            resource_table = rt[0]
+        else:
+            resource_table = rt
+
+        resource['user'] = user
         resource['authentication'] = str(resource_table[authkey]).strip()
         encrypt = str(resource_table[privkey]).strip()
         if encrypt.startswith('aes'):
@@ -166,6 +186,15 @@ def get_snmp_user(user, module):
         except TypeError:
             groups.append(str(group_table[grpkey]).strip())
 
+        # Now for the platform bug case, get the groups
+        if isinstance(rt, list):
+            # remove 1st element from the list as this is parsed already
+            rt.pop(0)
+            # iterate through other elements indexed by
+            # 'user' and add it to groups.
+            for each in rt:
+                groups.append(each['user'].strip())
+
         resource['group'] = groups
 
     except (KeyError, AttributeError, IndexError, TypeError):
@@ -174,22 +203,23 @@ def get_snmp_user(user, module):
     return resource
 
 
-def remove_snmp_user(user):
-    return ['no snmp-server user {0}'.format(user)]
+def remove_snmp_user(user, group=None):
+    if group:
+        return ['no snmp-server user {0} {1}'.format(user, group)]
+    else:
+        return ['no snmp-server user {0}'.format(user)]
 
 
-def config_snmp_user(proposed, user, reset, new):
-    if reset and not new:
+def config_snmp_user(proposed, user, reset):
+    if reset:
         commands = remove_snmp_user(user)
     else:
         commands = []
 
-    group = proposed.get('group', None)
-
-    cmd = ''
-
-    if group:
+    if proposed.get('group'):
         cmd = 'snmp-server user {0} {group}'.format(user, **proposed)
+    else:
+        cmd = 'snmp-server user {0}'.format(user)
 
     auth = proposed.get('authentication', None)
     pwd = proposed.get('pwd', None)
@@ -214,7 +244,7 @@ def config_snmp_user(proposed, user, reset, new):
 def main():
     argument_spec = dict(
         user=dict(required=True, type='str'),
-        group=dict(type='str', required=True),
+        group=dict(type='str'),
         pwd=dict(type='str'),
         privacy=dict(type='str'),
         authentication=dict(choices=['md5', 'sha']),
@@ -251,19 +281,28 @@ def main():
 
     existing = get_snmp_user(user, module)
 
-    if existing:
-        if group not in existing['group']:
-            existing['group'] = None
+    if state == 'present' and existing:
+        if group:
+            if group not in existing['group']:
+                existing['group'] = None
+            else:
+                existing['group'] = group
         else:
-            existing['group'] = group
+            existing['group'] = None
 
     commands = []
 
     if state == 'absent' and existing:
-        commands.append(remove_snmp_user(user))
+        if group:
+            if group in existing['group']:
+                if len(existing['group']) == 1:
+                    commands.append(remove_snmp_user(user))
+                else:
+                    commands.append(remove_snmp_user(user, group))
+        else:
+            commands.append(remove_snmp_user(user))
 
     elif state == 'present':
-        new = False
         reset = False
 
         args = dict(user=user, pwd=pwd, group=group, privacy=privacy,
@@ -273,7 +312,7 @@ def main():
         if not existing:
             if encrypt:
                 proposed['encrypt'] = 'aes-128'
-            commands.append(config_snmp_user(proposed, user, reset, new))
+            commands.append(config_snmp_user(proposed, user, reset))
 
         elif existing:
             if encrypt and not existing['encrypt'].startswith('aes'):
@@ -285,14 +324,12 @@ def main():
             if delta.get('pwd'):
                 delta['authentication'] = authentication
 
-            if delta:
-                delta['group'] = group
-
             if delta and encrypt:
                 delta['encrypt'] = 'aes-128'
 
-            command = config_snmp_user(delta, user, reset, new)
-            commands.append(command)
+            if delta:
+                command = config_snmp_user(delta, user, reset)
+                commands.append(command)
 
     cmds = flatten_list(commands)
     if cmds:


Test patch:
diff --git a/test/integration/targets/nxos_snmp_user/tests/common/sanity.yaml b/test/integration/targets/nxos_snmp_user/tests/common/sanity.yaml
--- a/test/integration/targets/nxos_snmp_user/tests/common/sanity.yaml
+++ b/test/integration/targets/nxos_snmp_user/tests/common/sanity.yaml
@@ -3,41 +3,88 @@
 - debug: msg="Using provider={{ connection.transport }}"
   when: ansible_connection == "local"
 
-- name: Create snmp user
-  nxos_snmp_user: &create
-    user: ntc
-    group: network-operator
-    authentication: md5
-    pwd: N$tOpe%1
-    privacy: HelloU$er1
-    encrypt: true
-    provider: "{{ connection }}"
-  register: result
-
-- assert: &true
-    that:
-      - "result.changed == true"
-
-- name: delete snmp user
+- name: Remove snmp user
   nxos_snmp_user: &remove
     user: ntc
-    group: network-operator
-    authentication: md5
-    pwd: Testing1%
-    privacy: HelloU$er1
-    encrypt: true
     state: absent
     provider: "{{ connection }}"
-  register: result
 
-- assert: *true
+- pause:
+    seconds: 5
+
+- block:
+  - name: Create snmp user
+    nxos_snmp_user: &create
+      user: ntc
+      group: network-operator
+      authentication: md5
+      pwd: N$tOpe%1
+      privacy: HelloU$er1
+      encrypt: true
+      provider: "{{ connection }}"
+    register: result
+
+  - assert: &true
+      that:
+        - "result.changed == true"
+
+  - name: Add another group to user
+    nxos_snmp_user: &chg
+      user: ntc
+      group: network-admin
+      provider: "{{ connection }}"
+    register: result
+
+  - assert: *true
+
+  - name: "Check Idempotence"
+    nxos_snmp_user: *chg
+    register: result
+
+  - assert: &false
+      that:
+        - "result.changed == false"
+
+  - name: Remove group from user
+    nxos_snmp_user: &remg
+      user: ntc
+      group: network-admin
+      state: absent
+      provider: "{{ connection }}"
+    register: result
+
+  - assert: *true
+
+  - pause:
+      seconds: 5
+
+  - name: "Check Idempotence"
+    nxos_snmp_user: *remg
+    register: result
+
+  - assert: *false
+
+  - name: delete snmp user
+    nxos_snmp_user: &remove1
+      user: ntc
+      group: network-operator
+      state: absent
+      provider: "{{ connection }}"
+    register: result
+
+  - assert: *true
+
+  - pause:
+      seconds: 5
+
+  - name: "Remove Idempotence"
+    nxos_snmp_user: *remove1
+    register: result
 
-- name: "Remove Idempotence"
-  nxos_snmp_user: *remove
-  register: result
+  - assert: *false
 
-- assert: &false
-    that:
-      - "result.changed == false"
+  always:
+    - name: delete snmp user
+      nxos_snmp_user: *remove
 
 - debug: msg="END connection={{ ansible_connection }} nxos_snmp_user sanity test"