Repository: ansible/ansible

Issue:
nxos_aaa_server issues
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and devel branch are affected too.
Always add information AFTER of these html comments.
-->

##### 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_aaa_server
##### ANSIBLE VERSION
<!--- Paste 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 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: N9K running 7.0(3)I2(5)
##### 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_aaa_server
1. On some platforms under some conditions, the 'directed_request' show command shows empty and this is causing the playbook run to throw errors. Basically, the show commands output needs to be checked if it is empty before doing any operations on it
2. For global key, the code is making the key as 'unknown' in some conditions and this is no cmds to fail. This is because the current config global key is taken as 'unknown' and the no cmd is running as 'no radius-server key 7 unknown' which is not correct as unknown is not the current key string. It should be None
3. some parameters have defaults defined properly in the document but if those values are given in the playbook, the code does not treat them as defaults and this breaks idempotence.

Note: I will open a PR shortly to address the above issues


Discussion:

Resolving patch:
diff --git a/lib/ansible/modules/network/nxos/nxos_aaa_server.py b/lib/ansible/modules/network/nxos/nxos_aaa_server.py
--- a/lib/ansible/modules/network/nxos/nxos_aaa_server.py
+++ b/lib/ansible/modules/network/nxos/nxos_aaa_server.py
@@ -39,7 +39,6 @@
       stored as encrypted (type 7).
     - Changes to the global AAA server key with encrypt_type=0
       are not idempotent.
-    - If global AAA server key is not found, it's shown as "unknown"
     - state=default will set the supplied parameters to their default values.
       The parameters that you want to default must also be set to default.
       If global_key=default, the global key will be removed.
@@ -51,7 +50,7 @@
         choices: ['radius', 'tacacs']
     global_key:
         description:
-            - Global AAA shared secret.
+            - Global AAA shared secret or keyword 'default'.
     encrypt_type:
         description:
             - The state of encryption applied to the entered global key.
@@ -60,14 +59,15 @@
     deadtime:
         description:
             - Duration for which a non-reachable AAA server is skipped,
-              in minutes. Range is 1-1440. Device default is 0.
+              in minutes or keyword 'default.
+              Range is 1-1440. Device default is 0.
     server_timeout:
         description:
-            - Global AAA server timeout period, in seconds. Range is 1-60.
-              Device default is 5.
+            - Global AAA server timeout period, in seconds or keyword 'default.
+              Range is 1-60. Device default is 5.
     directed_request:
         description:
-            - Enables direct authentication requests to AAA server.
+            - Enables direct authentication requests to AAA server or keyword 'default'
               Device default is disabled.
         choices: ['enabled', 'disabled']
     state:
@@ -116,7 +116,14 @@
 from ansible.module_utils.basic import AnsibleModule
 
 
-def execute_show_command(command, module, command_type='cli_show'):
+PARAM_TO_DEFAULT_KEYMAP = {
+    'server_timeout': '5',
+    'deadtime': '0',
+    'directed_request': 'disabled',
+}
+
+
+def execute_show_command(command, module):
     command = {
         'command': command,
         'output': 'text',
@@ -142,8 +149,7 @@ def get_aaa_server_info(server_type, module):
     global_key_command = 'show run | sec {0}'.format(server_type)
     aaa_regex = r'.*{0}-server\skey\s\d\s+(?P<key>\S+).*'.format(server_type)
 
-    server_body = execute_show_command(
-        server_command, module, command_type='cli_show_ascii')[0]
+    server_body = execute_show_command(server_command, module)[0]
 
     split_server = server_body.splitlines()
 
@@ -154,30 +160,25 @@ def get_aaa_server_info(server_type, module):
         elif line.startswith('deadtime'):
             aaa_server_info['deadtime'] = line.split(':')[1]
 
-    request_body = execute_show_command(
-        request_command, module, command_type='cli_show_ascii')[0]
-    aaa_server_info['directed_request'] = request_body.replace('\n', '')
+    request_body = execute_show_command(request_command, module)[0]
 
-    key_body = execute_show_command(
-        global_key_command, module, command_type='cli_show_ascii')[0]
+    if bool(request_body):
+        aaa_server_info['directed_request'] = request_body.replace('\n', '')
+    else:
+        aaa_server_info['directed_request'] = 'disabled'
+
+    key_body = execute_show_command(global_key_command, module)[0]
 
     try:
         match_global_key = re.match(aaa_regex, key_body, re.DOTALL)
         group_key = match_global_key.groupdict()
         aaa_server_info['global_key'] = group_key["key"].replace('\"', '')
     except (AttributeError, TypeError):
-        aaa_server_info['global_key'] = 'unknown'
+        aaa_server_info['global_key'] = None
 
     return aaa_server_info
 
 
-def set_aaa_server_global_key(encrypt_type, key, server_type):
-    if not encrypt_type:
-        encrypt_type = ''
-    return '{0}-server key {1} {2}'.format(
-        server_type, encrypt_type, key)
-
-
 def config_aaa_server(params, server_type):
     cmds = []
 
@@ -215,13 +216,13 @@ def default_aaa_server(existing, params, server_type):
     global_key = params.get('global_key')
     existing_key = existing.get('global_key')
 
-    if deadtime is not None:
+    if deadtime is not None and existing.get('deadtime') != PARAM_TO_DEFAULT_KEYMAP['deadtime']:
         cmds.append('no {0}-server deadtime 1'.format(server_type))
 
-    if server_timeout is not None:
+    if server_timeout is not None and existing.get('server_timeout') != PARAM_TO_DEFAULT_KEYMAP['server_timeout']:
         cmds.append('no {0}-server timeout 1'.format(server_type))
 
-    if directed_request is not None:
+    if directed_request is not None and existing.get('directed_request') != PARAM_TO_DEFAULT_KEYMAP['directed_request']:
         cmds.append('no {0}-server directed-request'.format(server_type))
 
     if global_key is not None and existing_key is not None:


Test patch:
diff --git a/test/integration/targets/nxos_aaa_server/tests/common/radius.yaml b/test/integration/targets/nxos_aaa_server/tests/common/radius.yaml
--- a/test/integration/targets/nxos_aaa_server/tests/common/radius.yaml
+++ b/test/integration/targets/nxos_aaa_server/tests/common/radius.yaml
@@ -73,7 +73,7 @@
   - assert: *false
 
   - name: "Remove radius server configuration"
-    nxos_aaa_server:
+    nxos_aaa_server: &rad_def
       server_type: radius
       deadtime: default
       server_timeout: default
@@ -85,6 +85,12 @@
 
   - assert: *true
 
+  - name: "Check Idempotence"
+    nxos_aaa_server: *rad_def
+    register: result
+
+  - assert: *false
+
   rescue:
 
   - debug: msg="connection={{ ansible_connection }} nxos_aaa_server failure detected"
@@ -94,4 +100,4 @@
     nxos_aaa_server: *remove
     register: result
 
-  - debug: msg="END connection={{ ansible_connection }} nxos_aaa_server radius.yaml sanity test"  
+- debug: msg="END connection={{ ansible_connection }} nxos_aaa_server radius.yaml sanity test"
diff --git a/test/integration/targets/nxos_aaa_server/tests/common/tacacs.yaml b/test/integration/targets/nxos_aaa_server/tests/common/tacacs.yaml
--- a/test/integration/targets/nxos_aaa_server/tests/common/tacacs.yaml
+++ b/test/integration/targets/nxos_aaa_server/tests/common/tacacs.yaml
@@ -79,11 +79,24 @@
   - assert: *false
 
   - name: "Remove tacacs server configuration"
-    nxos_aaa_server: *remove
+    nxos_aaa_server: &tac_def
+      server_type: tacacs
+      deadtime: default
+      server_timeout: default
+      global_key: default
+      directed_request: default
+      state: default
+      provider: "{{ connection }}"
     register: result
 
   - assert: *true
 
+  - name: "Check Idempotence"
+    nxos_aaa_server: *tac_def
+    register: result
+
+  - assert: *false
+
   rescue:
 
   - debug: msg="connection={{ ansible_connection }} nxos_aaa_server failure detected"
@@ -100,4 +113,4 @@
       state: disabled
       provider: "{{ connection }}"
 
-  - debug: msg="END connection={{ ansible_connection }} nxos_aaa_server tacacs.yaml sanity test"  
+- debug: msg="END connection={{ ansible_connection }} nxos_aaa_server tacacs.yaml sanity test"