Repository: ansible/ansible

Issue:
(P1) Attempting to set ipv6 interface errors out for nxos_ip_interface module
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
 - nxos_ip_interface

##### NXOS VERSION
7.3(0)I5(1)

##### ANSIBLE VERSION
```
ansible 2.4.0 (devel 85e7e342b0) last updated 2017/07/20 14:17:47 (GMT -400)
  config file = None
  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
Ubuntu 14.04

##### SUMMARY
Attempt to set the ipv6 address using the `nxos_ip_interface` module.  The module errors out.

Failure occurs for both `cli` and `nxapi` transports.

```yaml
---
- debug: msg="START TRANSPORT:CLI nxos_ip_interface sanity test"

- set_fact: testint1="{{ nxos_int1 }}"
- set_fact: testint2="{{ nxos_int2 }}"

- name: "Make {{testint1}} a layer3 interface"
  nxos_interface: &l3int1
    interface: "{{ testint1 }}"
    mode: layer3
    description: 'Configured by Ansible - Layer3'
    admin_state: 'up'
    state: present
    provider: "{{ cli }}"

- name: "Make {{testint2}} a layer3 interface"
  nxos_interface: &l3int2
    interface: "{{ testint2 }}"
    mode: layer3
    description: 'Configured by Ansible - Layer3'
    admin_state: 'up'
    state: present
    provider: "{{ cli }}"

- name: Ensure ipv4 address is configured
  nxos_ip_interface:
    interface: "{{ testint1 }}"
    version: v4
    state: present
    addr: 20.20.20.20
    mask: 24
    provider: "{{ cli }}"

- name: Ensure ipv6 address is configured
  nxos_ip_interface:
    interface: "{{ testint2 }}"
    version: v6
    state: present
    addr: 'fd56:31f7:e4ad:5585::1'
    mask: 64
    provider: "{{ cli }}"

- debug: msg="END TRANSPORT:CLI nxos_ip_interface sanity test"
```
##### STEPS TO REPRODUCE

```shell
TASK [nxos_ip_interface : Ensure ipv4 address is configured] ********************************************************************************************************************************************************************************************
task path: /root/agents-ci/ansible/test/integration/targets/nxos_ip_interface/tests/cli/sanity.yaml:25
<n9k.example.com> connection transport is cli
<n9k.example.com> using connection plugin network_cli
<n9k.example.com> socket_path: /root/.ansible/pc/2ead40b205
Using module_utils file /root/agents-ci/ansible/lib/ansible/module_utils/basic.py
Using module_utils file /root/agents-ci/ansible/lib/ansible/module_utils/nxos.py
Using module_utils file /root/agents-ci/ansible/lib/ansible/module_utils/_text.py
Using module_utils file /root/agents-ci/ansible/lib/ansible/module_utils/parsing/convert_bool.py
Using module_utils file /root/agents-ci/ansible/lib/ansible/module_utils/parsing/__init__.py
Using module_utils file /root/agents-ci/ansible/lib/ansible/module_utils/pycompat24.py
Using module_utils file /root/agents-ci/ansible/lib/ansible/module_utils/six/__init__.py
Using module_utils file /root/agents-ci/ansible/lib/ansible/module_utils/six/_six.py
Using module_utils file /root/agents-ci/ansible/lib/ansible/module_utils/connection.py
Using module_utils file /root/agents-ci/ansible/lib/ansible/module_utils/network_common.py
Using module_utils file /root/agents-ci/ansible/lib/ansible/module_utils/urls.py
Using module file /root/agents-ci/ansible/lib/ansible/modules/network/nxos/nxos_ip_interface.py
<n9k.example.com> ESTABLISH LOCAL CONNECTION FOR USER: root
<n9k.example.com> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1500659143.81-186913983097485 `" && echo ansible-tmp-1500659143.81-186913983097485="` echo $HOME/.ansible/tmp/ansible-tmp-1500659143.81-186913983097485 `" ) && sleep 0'
<n9k.example.com> PUT /tmp/tmp8cidoa TO /root/.ansible/tmp/ansible-tmp-1500659143.81-186913983097485/nxos_ip_interface.py
<n9k.example.com> EXEC /bin/sh -c 'chmod u+x /root/.ansible/tmp/ansible-tmp-1500659143.81-186913983097485/ /root/.ansible/tmp/ansible-tmp-1500659143.81-186913983097485/nxos_ip_interface.py && sleep 0'
<n9k.example.com> EXEC /bin/sh -c '/usr/bin/python /root/.ansible/tmp/ansible-tmp-1500659143.81-186913983097485/nxos_ip_interface.py; rm -rf "/root/.ansible/tmp/ansible-tmp-1500659143.81-186913983097485/" > /dev/null 2>&1 && sleep 0'
ok: [n9k.example.com] => {
    "changed": false, 
    "commands": [], 
    "end_state": {
        "addresses": [
            {
                "addr": "20.20.20.20", 
                "mask": "24", 
                "secondary": false, 
                "tag": 0
            }
        ], 
        "interface": "ethernet1/1", 
        "prefixes": [
            "20.20.20.0/24"
        ], 
        "type": "ethernet", 
        "vrf": "\"default\"(1)"
    }, 
    "existing": {
        "addresses": [
            {
                "addr": "20.20.20.20", 
                "mask": "24", 
                "secondary": false, 
                "tag": 0
            }
        ], 
        "interface": "ethernet1/1", 
        "prefixes": [
            "20.20.20.0/24"
        ], 
        "type": "ethernet", 
        "vrf": "\"default\"(1)"
    }, 
    "failed": false, 
    "invocation": {
        "module_args": {
            "addr": "20.20.20.20", 
            "allow_secondary": false, 
            "config": null, 
            "host": null, 
            "include_defaults": "True", 
            "interface": "Ethernet1/1", 
            "mask": "24", 
            "password": null, 
            "port": null, 
            "provider": null, 
            "save": false, 
            "ssh_keyfile": null, 
            "state": "present", 
            "tag": 0, 
            "timeout": 10, 
            "transport": "cli", 
            "use_ssl": null, 
            "username": null, 
            "validate_certs": null, 
            "version": "v4"
        }
    }, 
    "proposed": {
        "addr": "20.20.20.20", 
        "allow_secondary": false, 
        "interface": "ethernet1/1", 
        "mask": "24", 
        "tag": 0
    }
}

TASK [nxos_ip_interface : Ensure ipv6 address is configured] ********************************************************************************************************************************************************************************************
task path: /root/agents-ci/ansible/test/integration/targets/nxos_ip_interface/tests/cli/sanity.yaml:34
<n9k.example.com> connection transport is cli
<n9k.example.com> using connection plugin network_cli
<n9k.example.com> socket_path: /root/.ansible/pc/2ead40b205
Using module file /root/agents-ci/ansible/lib/ansible/modules/network/nxos/nxos_ip_interface.py
<n9k.example.com> ESTABLISH LOCAL CONNECTION FOR USER: root
<n9k.example.com> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1500659145.55-36881611223138 `" && echo ansible-tmp-1500659145.55-36881611223138="` echo $HOME/.ansible/tmp/ansible-tmp-1500659145.55-36881611223138 `" ) && sleep 0'
<n9k.example.com> PUT /tmp/tmp9zxIn8 TO /root/.ansible/tmp/ansible-tmp-1500659145.55-36881611223138/nxos_ip_interface.py
<n9k.example.com> EXEC /bin/sh -c 'chmod u+x /root/.ansible/tmp/ansible-tmp-1500659145.55-36881611223138/ /root/.ansible/tmp/ansible-tmp-1500659145.55-36881611223138/nxos_ip_interface.py && sleep 0'
<n9k.example.com> EXEC /bin/sh -c '/usr/bin/python /root/.ansible/tmp/ansible-tmp-1500659145.55-36881611223138/nxos_ip_interface.py; rm -rf "/root/.ansible/tmp/ansible-tmp-1500659145.55-36881611223138/" > /dev/null 2>&1 && sleep 0'
The full traceback is:
Traceback (most recent call last):
  File "/tmp/ansible_fnFZPX/ansible_module_nxos_ip_interface.py", line 542, in <module>
    main()
  File "/tmp/ansible_fnFZPX/ansible_module_nxos_ip_interface.py", line 502, in main
    existing = get_ip_interface(interface, version, module)
  File "/tmp/ansible_fnFZPX/ansible_module_nxos_ip_interface.py", line 321, in get_ip_interface
    interface = parse_unstructured_data(body, interface_name, version, module)
  File "/tmp/ansible_fnFZPX/ansible_module_nxos_ip_interface.py", line 277, in parse_unstructured_data
    interface['prefixes'].append(str(ipaddress.ip_interface(address).network))
  File "/usr/local/lib/python2.7/dist-packages/ipaddress.py", line 239, in ip_interface
    address)
ValueError: '2001::db8:800:200c:cccb/64' does not appear to be an IPv4 or IPv6 interface

fatal: [n9k.example.com]: FAILED! => {
    "changed": false, 
    "failed": true, 
    "module_stderr": "Traceback (most recent call last):\n  File \"/tmp/ansible_fnFZPX/ansible_module_nxos_ip_interface.py\", line 542, in <module>\n    main()\n  File \"/tmp/ansible_fnFZPX/ansible_module_nxos_ip_interface.py\", line 502, in main\n    existing = get_ip_interface(interface, version, module)\n  File \"/tmp/ansible_fnFZPX/ansible_module_nxos_ip_interface.py\", line 321, in get_ip_interface\n    interface = parse_unstructured_data(body, interface_name, version, module)\n  File \"/tmp/ansible_fnFZPX/ansible_module_nxos_ip_interface.py\", line 277, in parse_unstructured_data\n    interface['prefixes'].append(str(ipaddress.ip_interface(address).network))\n  File \"/usr/local/lib/python2.7/dist-packages/ipaddress.py\", line 239, in ip_interface\n    address)\nValueError: '2001::db8:800:200c:cccb/64' does not appear to be an IPv4 or IPv6 interface\n", 
    "module_stdout": "", 
    "msg": "MODULE FAILURE", 
    "rc": 0
}
	to retry, use: --limit @/root/agents-ci/ansible/test/integration/nxos.retry

PLAY RECAP **********************************************************************************************************************************************************************************************************************************************
n9k.example.com   : ok=18   changed=0    unreachable=0    failed=1   
```



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_ip_interface.py b/lib/ansible/modules/network/nxos/nxos_ip_interface.py
--- a/lib/ansible/modules/network/nxos/nxos_ip_interface.py
+++ b/lib/ansible/modules/network/nxos/nxos_ip_interface.py
@@ -37,6 +37,7 @@
     - C(mask) must be inserted in decimal format (i.e. 24) for
       both IPv6 and IPv4.
     - A single interface can have multiple IPv6 configured.
+    - C(tag) is not idempotent for IPv6 addresses and I2 system image.
 options:
     interface:
         description:
@@ -168,9 +169,10 @@ def find_same_addr(existing, addr, mask, full=False, **kwargs):
             if full:
                 if kwargs['version'] == 'v4' and int(address['tag']) == kwargs['tag']:
                     return address
-                elif kwargs['version'] == 'v6':
+                elif kwargs['version'] == 'v6' and kwargs['tag'] == 0:
                     # Currently we don't get info about IPv6 address tag
-                    return False
+                    # But let's not break idempotence for the default case
+                    return address
             else:
                 return address
     return False
@@ -274,7 +276,7 @@ def parse_unstructured_data(body, interface_name, version, module):
                 address = each_line.strip().split(' ')[0]
                 if address not in address_list:
                     address_list.append(address)
-                    interface['prefixes'].append(str(ipaddress.ip_interface(address).network))
+                    interface['prefixes'].append(str(ipaddress.ip_interface(u"%s" % address).network))
 
             if address_list:
                 for ipv6 in address_list:
@@ -289,7 +291,7 @@ def parse_unstructured_data(body, interface_name, version, module):
             if "IP address" in splitted_body[index]:
                 regex = '.*IP\saddress:\s(?P<addr>\d{1,3}(?:\.\d{1,3}){3}),\sIP\ssubnet:' + \
                         '\s\d{1,3}(?:\.\d{1,3}){3}\/(?P<mask>\d+)(?:\s(?P<secondary>secondary)\s)?' + \
-                        '.+?tag:\s(?P<tag>\d+).*'
+                        '(.+?tag:\s(?P<tag>\d+).*)?'
                 match = re.match(regex, splitted_body[index])
                 if match:
                     match_dict = match.groupdict()
@@ -297,7 +299,10 @@ def parse_unstructured_data(body, interface_name, version, module):
                         match_dict['secondary'] = False
                     else:
                         match_dict['secondary'] = True
-                    match_dict['tag'] = int(match_dict['tag'])
+                    if match_dict['tag'] is None:
+                        match_dict['tag'] = 0
+                    else:
+                        match_dict['tag'] = int(match_dict['tag'])
                     interface['addresses'].append(match_dict)
                     prefix = str(ipaddress.ip_interface(u"%(addr)s/%(mask)s" % match_dict).network)
                     interface['prefixes'].append(prefix)
@@ -349,7 +354,9 @@ def get_remove_ip_config_commands(interface, addr, mask, existing, version):
                     commands.append('no ip address {0}/{1}'.format(addr, mask))
                 break
     else:
-        commands.append('no ipv6 address {0}/{1}'.format(addr, mask))
+        for address in existing['addresses']:
+            if address['addr'] == addr:
+                commands.append('no ipv6 address {0}/{1}'.format(addr, mask))
 
     return commands
 


Test patch:
diff --git a/test/integration/nxos.yaml b/test/integration/nxos.yaml
--- a/test/integration/nxos.yaml
+++ b/test/integration/nxos.yaml
@@ -258,6 +258,15 @@
             failed_modules: "{{ failed_modules }} + [ 'nxos_interface_ospf' ]"
             test_failed: true
 
+    - block:
+      - include_role:
+          name: nxos_ip_interface
+        when: "limit_to in ['*', 'nxos_ip_interface']"
+      rescue:
+        - set_fact:
+            failed_modules: "{{ failed_modules }} + [ 'nxos_ip_interface' ]"
+            test_failed: true
+
     - block:
       - include_role:
           name: nxos_logging
diff --git a/test/integration/targets/nxos_ip_interface/defaults/main.yaml b/test/integration/targets/nxos_ip_interface/defaults/main.yaml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/nxos_ip_interface/defaults/main.yaml
@@ -0,0 +1,2 @@
+---
+testcase: "*"
diff --git a/test/integration/targets/nxos_ip_interface/meta/main.yml b/test/integration/targets/nxos_ip_interface/meta/main.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/nxos_ip_interface/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+  - prepare_nxos_tests
diff --git a/test/integration/targets/nxos_ip_interface/tasks/cli.yaml b/test/integration/targets/nxos_ip_interface/tasks/cli.yaml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/nxos_ip_interface/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_ip_interface/tasks/main.yaml b/test/integration/targets/nxos_ip_interface/tasks/main.yaml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/nxos_ip_interface/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_ip_interface/tasks/nxapi.yaml b/test/integration/targets/nxos_ip_interface/tasks/nxapi.yaml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/nxos_ip_interface/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_ip_interface/tests/cli/sanity.yaml b/test/integration/targets/nxos_ip_interface/tests/cli/sanity.yaml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/nxos_ip_interface/tests/cli/sanity.yaml
@@ -0,0 +1,99 @@
+---
+- debug: msg="START TRANSPORT:CLI nxos_ip_interface sanity test"
+
+- set_fact: testint1="{{ nxos_int1 }}"
+- set_fact: testint2="{{ nxos_int2 }}"
+
+- name: "Put interface {{testint1}} into default state"
+  nxos_config: &intdefault1
+    lines:
+      - "default interface {{ testint1 }}"
+    provider: "{{ cli }}"
+  ignore_errors: yes
+
+- name: "Put interface {{testint2}} into default state"
+  nxos_config: &intdefault2
+    lines:
+      - "default interface {{ testint2 }}"
+    provider: "{{ cli }}"
+  ignore_errors: yes
+
+- name: "Make {{testint1}} a layer3 interface"
+  nxos_interface: &l3int1
+    interface: "{{ testint1 }}"
+    mode: layer3
+    description: 'Configured by Ansible - Layer3'
+    admin_state: 'up'
+    state: present
+    provider: "{{ cli }}"
+
+- name: "Make {{testint2}} a layer3 interface"
+  nxos_interface: &l3int2
+    interface: "{{ testint2 }}"
+    mode: layer3
+    description: 'Configured by Ansible - Layer3'
+    admin_state: 'up'
+    state: present
+    provider: "{{ cli }}"
+
+# For titanium
+- name: Clear interface v4
+  nxos_ip_interface:
+    interface: "{{ testint1 }}"
+    version: v4
+    state: absent
+    addr: 20.20.20.20
+    mask: 24
+    provider: "{{ cli }}"
+
+# For titanium
+- name: Clear interface v6
+  nxos_ip_interface:
+    interface: "{{ testint2 }}"
+    version: v6
+    state: absent
+    addr: 'fd56:31f7:e4ad:5585::1'
+    mask: 64
+    provider: "{{ cli }}"
+
+- name: Ensure ipv4 address is configured
+  nxos_ip_interface: &ipv4
+    interface: "{{ testint1 }}"
+    version: v4
+    state: present
+    addr: 20.20.20.20
+    mask: 24
+    provider: "{{ cli }}"
+  register: result
+
+- assert: &true
+    that:
+      - "result.changed == true"
+
+- name: "Check Idempotence"
+  nxos_ip_interface: *ipv4
+  register: result
+
+- assert: &false
+    that:
+      - "result.changed == false"
+
+- name: Ensure ipv6 address is configured
+  nxos_ip_interface: &ipv6
+    interface: "{{ testint2 }}"
+    version: v6
+    state: present
+    addr: 'fd56:31f7:e4ad:5585::1'
+    mask: 64
+    provider: "{{ cli }}"
+  register: result
+
+- assert: *true
+
+- name: "Check Idempotence"
+  nxos_ip_interface: *ipv6
+  register: result
+
+- assert: *false
+
+- debug: msg="END TRANSPORT:CLI nxos_ip_interface sanity test"
diff --git a/test/integration/targets/nxos_ip_interface/tests/nxapi/sanity.yaml b/test/integration/targets/nxos_ip_interface/tests/nxapi/sanity.yaml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/nxos_ip_interface/tests/nxapi/sanity.yaml
@@ -0,0 +1,99 @@
+---
+- debug: msg="START TRANSPORT:NXAPI nxos_ip_interface sanity test"
+
+- set_fact: testint1="{{ nxos_int1 }}"
+- set_fact: testint2="{{ nxos_int2 }}"
+
+- name: "Put interface {{testint1}} into default state"
+  nxos_config: &intdefault1
+    lines:
+      - "default interface {{ testint1 }}"
+    provider: "{{ nxapi }}"
+  ignore_errors: yes
+
+- name: "Put interface {{testint2}} into default state"
+  nxos_config: &intdefault2
+    lines:
+      - "default interface {{ testint2 }}"
+    provider: "{{ nxapi }}"
+  ignore_errors: yes
+
+- name: "Make {{testint1}} a layer3 interface"
+  nxos_interface: &l3int1
+    interface: "{{ testint1 }}"
+    mode: layer3
+    description: 'Configured by Ansible - Layer3'
+    admin_state: 'up'
+    state: present
+    provider: "{{ nxapi }}"
+
+- name: "Make {{testint2}} a layer3 interface"
+  nxos_interface: &l3int2
+    interface: "{{ testint2 }}"
+    mode: layer3
+    description: 'Configured by Ansible - Layer3'
+    admin_state: 'up'
+    state: present
+    provider: "{{ nxapi }}"
+
+# For titanium
+- name: Clear interface v4
+  nxos_ip_interface:
+    interface: "{{ testint1 }}"
+    version: v4
+    state: absent
+    addr: 20.20.20.20
+    mask: 24
+    provider: "{{ nxapi }}"
+
+# For titanium
+- name: Clear interface v6
+  nxos_ip_interface:
+    interface: "{{ testint2 }}"
+    version: v6
+    state: absent
+    addr: 'fd56:31f7:e4ad:5585::1'
+    mask: 64
+    provider: "{{ nxapi }}"
+
+- name: Ensure ipv4 address is configured
+  nxos_ip_interface: &ipv4
+    interface: "{{ testint1 }}"
+    version: v4
+    state: present
+    addr: 20.20.20.20
+    mask: 24
+    provider: "{{ nxapi }}"
+  register: result
+
+- assert: &true
+    that:
+      - "result.changed == true"
+
+- name: "Check Idempotence"
+  nxos_ip_interface: *ipv4
+  register: result
+
+- assert: &false
+    that:
+      - "result.changed == false"
+
+- name: Ensure ipv6 address is configured
+  nxos_ip_interface: &ipv6
+    interface: "{{ testint2 }}"
+    version: v6
+    state: present
+    addr: 'fd56:31f7:e4ad:5585::1'
+    mask: 64
+    provider: "{{ nxapi }}"
+  register: result
+
+- assert: *true
+
+- name: "Check Idempotence"
+  nxos_ip_interface: *ipv6
+  register: result
+
+- assert: *false
+
+- debug: msg="END TRANSPORT:NXAPI nxos_ip_interface sanity test"