Repository: ansible/ansible-modules-core

Issue:
nxos_nxapi isn't idempotent (in some cases)
##### ISSUE TYPE
- Bug Report
##### COMPONENT NAME

nxos_nxapi
##### ANSIBLE VERSION

```
ansible 2.2.0 (devel 547cea556f) last updated 2016/09/20 12:12:18 (GMT +100)
  lib/ansible/modules/core: (devel 12a7027c49) last updated 2016/09/20 15:11:43 (GMT +100)
  lib/ansible/modules/extras: (devel db7a3f48e1) last updated 2016/09/20 11:53:00 (GMT +100)
```
##### CONFIGURATION
##### OS / ENVIRONMENT
##### SUMMARY

Running the following twice isn't idempotent

``` yaml
- name: Disable NXAPI again
  nxos_nxapi:
      state: absent
      provider: "{{ cli }}"
  register: result
```
##### STEPS TO REPRODUCE

``` yaml
- name: Assert NXAPI is disabled
  assert:
      that: result.stdout[0] | search('disabled')

- name: Disable NXAPI again
  nxos_nxapi:
      state: absent
      provider: "{{ cli }}"
  register: result
  # delegate_to: "{{ delegate_to }}"

- name: Disable NXAPI again AGAIN
  nxos_nxapi:
      state: absent
      provider: "{{ cli }}"
  register: result
  # delegate_to: "{{ delegate_to }}"

- name: Assert idempotence
  assert:
    that:
      result.changed == false

```
##### EXPECTED RESULTS
##### ACTUAL RESULTS

``` yaml
TASK [test_nxos_nxapi : Assert NXAPI is disabled] ******************************
task path: /home/johnb/git/ansible-inc/test-network-modules/roles/test_nxos_nxapi/tests/cli/disable.yaml:25
ok: [nxos01] => {
    "changed": false, 
    "invocation": {
        "module_args": {
            "that": "result.stdout[0] | search('disabled')"
        }, 
        "module_name": "assert"
    }, 
    "msg": "all assertions passed"
}

TASK [test_nxos_nxapi : Disable NXAPI again] ***********************************
task path: /home/johnb/git/ansible-inc/test-network-modules/roles/test_nxos_nxapi/tests/cli/disable.yaml:29
Using module file /home/johnb/git/ansible-inc/ansible/lib/ansible/modules/core/network/nxos/nxos_nxapi.py
<nxos01> ESTABLISH LOCAL CONNECTION FOR USER: johnb
<nxos01> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1474463677.89-271821526274115 `" && echo ansible-tmp-1474463677.89-271821526274115="` echo $HOME/.ansible/tmp/ansible-tmp-1474463677.89-271821526274115 `" ) && sleep 0'
<nxos01> PUT /tmp/tmpXDhMFT TO /home/johnb/.ansible/tmp/ansible-tmp-1474463677.89-271821526274115/nxos_nxapi.py
<nxos01> EXEC /bin/sh -c 'chmod u+x /home/johnb/.ansible/tmp/ansible-tmp-1474463677.89-271821526274115/ /home/johnb/.ansible/tmp/ansible-tmp-1474463677.89-271821526274115/nxos_nxapi.py && sleep 0'
<nxos01> EXEC /bin/sh -c 'python /home/johnb/.ansible/tmp/ansible-tmp-1474463677.89-271821526274115/nxos_nxapi.py; rm -rf "/home/johnb/.ansible/tmp/ansible-tmp-1474463677.89-271821526274115/" > /dev/null 2>&1 && sleep 0'
changed: [nxos01] => {
    "changed": true, 
    "invocation": {
        "module_args": {
            "auth_pass": null, 
            "authorize": false, 
            "config": null, 
            "host": "nxos01", 
            "http": true, 
            "http_port": 80, 
            "https": false, 
            "https_port": 443, 
            "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
            "port": null, 
            "provider": {
                "host": "nxos01", 
                "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
                "transport": "cli", 
                "username": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
            }, 
            "sandbox": false, 
            "ssh_keyfile": null, 
            "state": "absent", 
            "timeout": 10, 
            "transport": "cli", 
            "use_ssl": false, 
            "username": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
            "validate_certs": true
        }, 
        "module_name": "nxos_nxapi"
    }, 
    "updates": [
        "no feature nxapi"
    ], 
    "warnings": []
}

TASK [test_nxos_nxapi : Disable NXAPI again AGAIN] ***********************************
task path: /home/johnb/git/ansible-inc/test-network-modules/roles/test_nxos_nxapi/tests/cli/disable.yaml:35
Using module file /home/johnb/git/ansible-inc/ansible/lib/ansible/modules/core/network/nxos/nxos_nxapi.py
<nxos01> ESTABLISH LOCAL CONNECTION FOR USER: johnb
<nxos01> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1474463690.75-462285203393 `" && echo ansible-tmp-1474463690.75-462285203393="` echo $HOME/.ansible/tmp/ansible-tmp-1474463690.75-462285203393 `" ) && sleep 0'
<nxos01> PUT /tmp/tmp6dFds6 TO /home/johnb/.ansible/tmp/ansible-tmp-1474463690.75-462285203393/nxos_nxapi.py
<nxos01> EXEC /bin/sh -c 'chmod u+x /home/johnb/.ansible/tmp/ansible-tmp-1474463690.75-462285203393/ /home/johnb/.ansible/tmp/ansible-tmp-1474463690.75-462285203393/nxos_nxapi.py && sleep 0'
<nxos01> EXEC /bin/sh -c 'python /home/johnb/.ansible/tmp/ansible-tmp-1474463690.75-462285203393/nxos_nxapi.py; rm -rf "/home/johnb/.ansible/tmp/ansible-tmp-1474463690.75-462285203393/" > /dev/null 2>&1 && sleep 0'
changed: [nxos01] => {
    "changed": true, 
    "invocation": {
        "module_args": {
            "auth_pass": null, 
            "authorize": false, 
            "config": null, 
            "host": "nxos01", 
            "http": true, 
            "http_port": 80, 
            "https": false, 
            "https_port": 443, 
            "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
            "port": null, 
            "provider": {
                "host": "nxos01", 
                "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
                "transport": "cli", 
                "username": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
            }, 
            "sandbox": false, 
            "ssh_keyfile": null, 
            "state": "absent", 
            "timeout": 10, 
            "transport": "cli", 
            "use_ssl": false, 
            "username": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
            "validate_certs": true
        }, 
        "module_name": "nxos_nxapi"
    }, 
    "updates": [
        "no feature nxapi"
    ], 
    "warnings": []
}

TASK [test_nxos_nxapi : Assert idempotence] ************************************
task path: /home/johnb/git/ansible-inc/test-network-modules/roles/test_nxos_nxapi/tests/cli/disable.yaml:42
fatal: [nxos01]: FAILED! => {
    "assertion": "result.changed == false", 
    "changed": false, 
    "evaluated_to": false, 
    "failed": true, 
    "invocation": {
        "module_args": {
            "that": "result.changed == false"
        }, 
        "module_name": "assert"
    }
}
    to retry, use: --limit @/home/johnb/git/ansible-inc/test-network-modules/nxos.retry

PLAY RECAP *********************************************************************
nxos01                     : ok=14   changed=4    unreachable=0    failed=1   

```



Discussion:
Also getting the following idempotent issues

``` yaml
- debug: msg="START cli/config.yaml"


#----
- name: Setup
  nxos_config:
    lines: no feature nxapi
    match: none
    provider: "{{ cli }}"
  connection: local

- name: Get running-config
  nxos_command:
    commands: show running-config
    provider: "{{ cli }}"
  register: config
  connection: local

- name: Set config
  nxos_nxapi:
    config: "{{ config.stdout[0] }}"
    provider: "{{ cli }}"
  register: config
  connection: local

# Triggers the following changes:
#    "updates": [
#        "feature nxapi",
#        "nxapi http port 80",
#        "no nxapi https",
#        "no nxapi sandbox"
#    ],

- name: Ensure that this triggered a change
  assert:
    that:
      - "config.changed == true"

#---
- name: Get running-config again
  nxos_command:
    commands: show running-config
    provider: "{{ cli }}"
  register: runningconfig
  connection: local

- name: Set config
  nxos_nxapi:
    config: "{{ runningconfig.stdout[0] }}"
    provider: "{{ cli }}"
  register: config
  connection: local

# BUG: The above shouldn't trigger the following updates
# Triggers the following changes:
#    "updates": [
#        "nxapi http port 80",
#        "no nxapi https",
#        "no nxapi sandbox"
#    ],



# DEBUG: Try another time
- name: Set config - FIXME, shouldn't need this
  nxos_nxapi:
    config: "{{ runningconfig.stdout[0] }}"
    provider: "{{ cli }}"
  register: config
  connection: local

# BUG: We are still getting changes
# Triggers the following changes:
#    "updates": [
#        "nxapi http port 80",
#        "no nxapi https",
#        "no nxapi sandbox"
#    ],

- name: Idempotency check
  assert:
    that:
      - "config.changed == false"

- debug: msg="END cli/config.yaml"
```

Once fixed test cases need updating (search for `FIXME`)


Resolving patch:
diff --git a/network/nxos/nxos_nxapi.py b/network/nxos/nxos_nxapi.py
--- a/network/nxos/nxos_nxapi.py
+++ b/network/nxos/nxos_nxapi.py
@@ -65,7 +65,7 @@
         enable the use of the HTTPS transport, set the value of this
         argument to True.
     required: false
-    default: yes
+    default: no
     choices: ['yes', 'no']
     aliases: ['enable_https']
   sandbox:
@@ -151,7 +151,26 @@ def invoke(name, *args, **kwargs):
     if func:
         return func(*args, **kwargs)
 
-def present(module, commands):
+def get_instance(module):
+    instance = dict(state='absent')
+    try:
+        resp = module.cli('show nxapi', 'json')
+    except NetworkError:
+        return instance
+
+    instance['state'] = 'present'
+
+    instance['http'] = 'http_port' in resp[0]
+    instance['http_port'] = resp[0].get('http_port') or 80
+
+    instance['https'] = 'https_port' in resp[0]
+    instance['https_port'] = resp[0].get('https_port') or 443
+
+    instance['sandbox'] = resp[0]['sandbox_status']
+
+    return instance
+
+def present(module, instance, commands):
     commands.append('feature nxapi')
     setters = set()
     for key, value in module.argument_spec.iteritems():
@@ -159,12 +178,13 @@ def present(module, commands):
         if setter not in setters:
             setters.add(setter)
             if module.params[key] is not None:
-                invoke(setter, module, commands)
+                invoke(setter, module, instance, commands)
 
-def absent(module, commands):
-    commands.append('no feature nxapi')
+def absent(module, instance, commands):
+    if instance['state'] != 'absent':
+        commands.append('no feature nxapi')
 
-def set_http(module, commands):
+def set_http(module, instance, commands):
     port = module.params['http_port']
     if not 0 <= port <= 65535:
         module.fail_json(msg='http_port must be between 1 and 65535')
@@ -173,7 +193,7 @@ def set_http(module, commands):
     elif module.params['http'] is False:
         commands.append('no nxapi http')
 
-def set_https(module, commands):
+def set_https(module, instance, commands):
     port = module.params['https_port']
     if not 0 <= port <= 65535:
         module.fail_json(msg='https_port must be between 1 and 65535')
@@ -182,7 +202,7 @@ def set_https(module, commands):
     elif module.params['https'] is False:
         commands.append('no nxapi https')
 
-def set_sandbox(module, commands):
+def set_sandbox(module, instance, commands):
     if module.params['sandbox'] is True:
         commands.append('nxapi sandbox')
     elif module.params['sandbox'] is False:
@@ -287,7 +307,9 @@ def main():
                         'a future release.  Please use state=absent instead')
 
     commands = list()
-    invoke(state, module, commands)
+    instance = get_instance(module)
+
+    invoke(state, module, instance, commands)
 
     try:
         load(module, commands, result)


Test patch: