Repository: ansible/ansible

Issue:
(P1) nxos_ospf is not idempotent
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
 - nxos_ospf

##### NXOS VERSION
7.0(3)I5(3) All version tested failed idempotence test.

##### ANSIBLE VERSION
```
ansible 2.4.0 (devel bb5b1680f8) last updated 2017/07/26 08:18:32 (GMT -400)
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/usr/share/ansible']
  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
Ubuntu 14.04

##### SUMMARY
The following playbook is not idempotent

```yaml
---
- debug: msg="START TRANSPORT:{{ connection.transport }} nxos_ospf sanity test"

- name: "Enable feature OSPF"
  nxos_feature:
    feature: ospf
    state: enabled
    provider: "{{ connection }}"
  ignore_errors: yes

- block:
  - name: Configure ospf
    nxos_ospf: &config
      ospf: 1
      state: present
      provider: "{{ connection }}"
    register: result

  - assert: &true
      that:
        - "result.changed == true"

  - name: "Check Idempotence"
    nxos_ospf: *config
    register: result

  - assert: &false
      that:
        - "result.changed == false"

  rescue:
  - name: "Disable feature OSPF"
    nxos_feature:
      feature: ospf
      state: disabled
      provider: "{{ connection }}"
    ignore_errors: yes

  always:
  - name: Unconfigure ospf
    nxos_ospf: &unconfig
      ospf: 1
      state: absent
      provider: "{{ connection }}"
    register: result

  - assert: *true

  - name: "Check Idempotence"
    nxos_ospf: *unconfig
    register: result

  - assert: *false

  - debug: msg="END TRANSPORT:{{ connection.transport }} nxos_ospf sanity test"
```

```shell
TASK [nxos_ospf : debug] ********************************************************************************************************************************************************************************************************************************
ok: [n9k.example.com] => {
    "msg": "START TRANSPORT:nxapi nxos_ospf sanity test"
}

TASK [nxos_ospf : Enable feature OSPF] ******************************************************************************************************************************************************************************************************************
changed: [n9k.example.com]

TASK [nxos_ospf : Configure ospf] ***********************************************************************************************************************************************************************************************************************
changed: [n9k.example.com]

TASK [nxos_ospf : assert] *******************************************************************************************************************************************************************************************************************************
ok: [n9k.example.com] => {
    "changed": false, 
    "failed": false, 
    "msg": "All assertions passed"
}

TASK [nxos_ospf : Check Idempotence] ********************************************************************************************************************************************************************************************************************
changed: [n9k.example.com]

TASK [nxos_ospf : assert] *******************************************************************************************************************************************************************************************************************************
fatal: [n9k.example.com]: FAILED! => {
    "assertion": "result.changed == false", 
    "changed": false, 
    "evaluated_to": false, 
    "failed": true
}

TASK [nxos_ospf : Disable feature OSPF] *****************************************************************************************************************************************************************************************************************
changed: [n9k.example.com]

TASK [nxos_ospf : Unconfigure ospf] *********************************************************************************************************************************************************************************************************************
ok: [n9k.example.com]

TASK [nxos_ospf : assert] *******************************************************************************************************************************************************************************************************************************
fatal: [n9k.example.com]: FAILED! => {
    "assertion": "result.changed == true", 
    "changed": false, 
    "evaluated_to": false, 
    "failed": true
}

TASK [nxos_ospf : disable nxapi] ************************************************************************************************************************************************************************************************************************
changed: [n9k.example.com]

PLAY RECAP **********************************************************************************************************************************************************************************************************************************************
n9k.example.com          : ok=36   changed=10   unreachable=0    failed=4   
```


Discussion:
cc @GGabriele @jedelman8 @privateip @rahushen @rcarrillocruz @trishnaguha
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/lib/ansible/modules/network/nxos/nxos_ospf.py b/lib/ansible/modules/network/nxos/nxos_ospf.py
--- a/lib/ansible/modules/network/nxos/nxos_ospf.py
+++ b/lib/ansible/modules/network/nxos/nxos_ospf.py
@@ -139,7 +139,7 @@ def main():
         existing_list = existing['ospf']
 
     candidate = CustomNetworkConfig(indent=3)
-    if state == 'present':
+    if state == 'present' and ospf not in existing_list:
         state_present(module, proposed, candidate)
     if state == 'absent' and ospf in existing_list:
         state_absent(module, proposed, candidate)


Test patch:
diff --git a/test/integration/nxos.yaml b/test/integration/nxos.yaml
--- a/test/integration/nxos.yaml
+++ b/test/integration/nxos.yaml
@@ -240,6 +240,15 @@
             failed_modules: "{{ failed_modules }} + [ 'nxos_vxlan_vtep' ]"
             test_failed: true
 
+    - block:
+      - include_role:
+          name: nxos_ospf
+        when: "limit_to in ['*', 'nxos_ospf']"
+      rescue:
+        - set_fact:
+            failed_modules: "{{ failed_modules }} + [ 'nxos_ospf' ]"
+            test_failed: true
+
     - block:
       - include_role:
           name: nxos_interface_ospf
diff --git a/test/integration/targets/nxos_ospf/defaults/main.yaml b/test/integration/targets/nxos_ospf/defaults/main.yaml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/nxos_ospf/defaults/main.yaml
@@ -0,0 +1,2 @@
+---
+testcase: "*"
diff --git a/test/integration/targets/nxos_ospf/meta/main.yml b/test/integration/targets/nxos_ospf/meta/main.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/nxos_ospf/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+  - prepare_nxos_tests
diff --git a/test/integration/targets/nxos_ospf/tasks/cli.yaml b/test/integration/targets/nxos_ospf/tasks/cli.yaml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/nxos_ospf/tasks/cli.yaml
@@ -0,0 +1,15 @@
+---
+- name: collect all cli test cases
+  find:
+    paths: "{{ role_path }}/tests/cli"
+    patterns: "{{ testcase }}.yaml"
+  register: test_cases
+
+- name: set test_items
+  set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
+
+- name: run test case
+  include: "{{ test_case_to_run }}"
+  with_items: "{{ test_items }}"
+  loop_control:
+    loop_var: test_case_to_run
diff --git a/test/integration/targets/nxos_ospf/tasks/main.yaml b/test/integration/targets/nxos_ospf/tasks/main.yaml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/nxos_ospf/tasks/main.yaml
@@ -0,0 +1,7 @@
+---
+# Use block to ensure that both cli and nxapi tests
+# will run even if there are failures or errors.
+- block:
+  - { include: cli.yaml, tags: ['cli'] }
+  always:
+  - { include: nxapi.yaml, tags: ['nxapi'] }
diff --git a/test/integration/targets/nxos_ospf/tasks/nxapi.yaml b/test/integration/targets/nxos_ospf/tasks/nxapi.yaml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/nxos_ospf/tasks/nxapi.yaml
@@ -0,0 +1,28 @@
+---
+- name: collect all nxapi test cases
+  find:
+    paths: "{{ role_path }}/tests/nxapi"
+    patterns: "{{ testcase }}.yaml"
+  register: test_cases
+
+- name: set test_items
+  set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
+
+- name: enable nxapi
+  nxos_config:
+    lines:
+      - feature nxapi
+      - nxapi http port 80
+    provider: "{{ cli }}"
+
+- name: run test case
+  include: "{{ test_case_to_run }}"
+  with_items: "{{ test_items }}"
+  loop_control:
+    loop_var: test_case_to_run
+
+- name: disable nxapi
+  nxos_config:
+    lines:
+      - no feature nxapi
+    provider: "{{ cli }}"
diff --git a/test/integration/targets/nxos_ospf/tests/cli/sanity.yaml b/test/integration/targets/nxos_ospf/tests/cli/sanity.yaml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/nxos_ospf/tests/cli/sanity.yaml
@@ -0,0 +1,4 @@
+---
+- set_fact: connection="{{ cli }}"
+
+- import_tasks: targets/nxos_ospf/tests/common/sanity.yaml
diff --git a/test/integration/targets/nxos_ospf/tests/common/sanity.yaml b/test/integration/targets/nxos_ospf/tests/common/sanity.yaml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/nxos_ospf/tests/common/sanity.yaml
@@ -0,0 +1,55 @@
+---
+- debug: msg="START TRANSPORT:{{ connection.transport }} nxos_ospf sanity test"
+
+- name: "Enable feature OSPF"
+  nxos_feature:
+    feature: ospf
+    state: enabled
+    provider: "{{ connection }}"
+  ignore_errors: yes
+
+- block:
+  - name: Configure ospf
+    nxos_ospf: &config
+      ospf: 1
+      state: present
+      provider: "{{ connection }}"
+    register: result
+
+  - assert: &true
+      that:
+        - "result.changed == true"
+
+  - name: "Check Idempotence"
+    nxos_ospf: *config
+    register: result
+
+  - assert: &false
+      that:
+        - "result.changed == false"
+
+  rescue:
+  - name: "Disable feature OSPF"
+    nxos_feature:
+      feature: ospf
+      state: disabled
+      provider: "{{ connection }}"
+    ignore_errors: yes
+
+  always:
+  - name: Unconfigure ospf
+    nxos_ospf: &unconfig
+      ospf: 1
+      state: absent
+      provider: "{{ connection }}"
+    register: result
+
+  - assert: *true
+
+  - name: "Check Idempotence"
+    nxos_ospf: *unconfig
+    register: result
+
+  - assert: *false
+
+  - debug: msg="END TRANSPORT:{{ connection.transport }} nxos_ospf sanity test"
diff --git a/test/integration/targets/nxos_ospf/tests/nxapi/sanity.yaml b/test/integration/targets/nxos_ospf/tests/nxapi/sanity.yaml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/nxos_ospf/tests/nxapi/sanity.yaml
@@ -0,0 +1,4 @@
+---
+- set_fact: connection="{{ nxapi }}"
+
+- import_tasks: targets/nxos_ospf/tests/common/sanity.yaml