Repository: ansible/ansible

Issue:
nxos_vpc_interface errors out for transport nxapi
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
 - nxos_vpc_interface

##### NXOS VERSION
7.0(3)I3(1)

##### ANSIBLE VERSION
```
ansible-playbook 2.4.0 (devel 781bb44b02) last updated 2017/06/08 15:20:16 (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-playbook
  python version = 2.7.6 (default, Oct 26 2016, 20:30:19) [GCC 4.8.4]
```

##### OS / ENVIRONMENT
running on Ubuntu 14.04

##### SUMMARY
The module errors out in latest `devel` when using transport `nxapi`.

##### STEPS TO REPRODUCE
```yaml
---
- debug: msg="START TRANSPORT:NXAPI nxos_vpc_interface sanity test"

- name: Configure vpc port channel
  nxos_vpc_interface:
    portchannel: 10
    vpc: 100
    provider: "{{ nxapi }}"

- debug: msg="END TRANSPORT:NXAPI nxos_vpc_interface sanity test"

```

##### EXPECTED RESULTS
Expect module to not error out.

##### ACTUAL RESULTS FOR TRANSPORT NXAPI

```shell
TASK [nxos_vpc_interface : debug] *************************************************************************************************************************************************************************************************************
task path: /root/agents-ci/ansible/test/integration/targets/nxos_vpc_interface/tests/nxapi/sanity.yaml:2
ok: [n9k] => {
    "failed": false, 
    "msg": "START TRANSPORT:NXAPI nxos_vpc_interface sanity test"
}

TASK [nxos_vpc_interface : Configure vpc port channel] ****************************************************************************************************************************************************************************************
task path: /root/agents-ci/ansible/test/integration/targets/nxos_vpc_interface/tests/nxapi/sanity.yaml:4
<n9k> connection transport is nxapi
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/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_vpc_interface.py
<n9k> ESTABLISH LOCAL CONNECTION FOR USER: root
<n9k> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1496954312.64-212198553137167 `" && echo ansible-tmp-1496954312.64-212198553137167="` echo $HOME/.ansible/tmp/ansible-tmp-1496954312.64-212198553137167 `" ) && sleep 0'
<n9k> PUT /tmp/tmpDoGV5P TO /root/.ansible/tmp/ansible-tmp-1496954312.64-212198553137167/nxos_vpc_interface.py
<n9k> EXEC /bin/sh -c 'chmod u+x /root/.ansible/tmp/ansible-tmp-1496954312.64-212198553137167/ /root/.ansible/tmp/ansible-tmp-1496954312.64-212198553137167/nxos_vpc_interface.py && sleep 0'
<n9k> EXEC /bin/sh -c '/usr/bin/python /root/.ansible/tmp/ansible-tmp-1496954312.64-212198553137167/nxos_vpc_interface.py; rm -rf "/root/.ansible/tmp/ansible-tmp-1496954312.64-212198553137167/" > /dev/null 2>&1 && sleep 0'
The full traceback is:
Traceback (most recent call last):
  File "/tmp/ansible_MKyy_n/ansible_module_nxos_vpc_interface.py", line 381, in <module>
    main()
  File "/tmp/ansible_MKyy_n/ansible_module_nxos_vpc_interface.py", line 288, in main
    if portchannel not in get_portchannel_list(module):
  File "/tmp/ansible_MKyy_n/ansible_module_nxos_vpc_interface.py", line 136, in get_portchannel_list
    pc_list = body[0]['TABLE_channel']['ROW_channel']
TypeError: string indices must be integers

fatal: [n9k]: FAILED! => {
    "changed": false, 
    "failed": true, 
    "module_stderr": "Traceback (most recent call last):\n  File \"/tmp/ansible_MKyy_n/ansible_module_nxos_vpc_interface.py\", line 381, in <module>\n    main()\n  File \"/tmp/ansible_MKyy_n/ansible_module_nxos_vpc_interface.py\", line 288, in main\n    if portchannel not in get_portchannel_list(module):\n  File \"/tmp/ansible_MKyy_n/ansible_module_nxos_vpc_interface.py\", line 136, in get_portchannel_list\n    pc_list = body[0]['TABLE_channel']['ROW_channel']\nTypeError: string indices must be integers\n", 
    "module_stdout": "", 
    "msg": "MODULE FAILURE", 
    "rc": 0
}
	to retry, use: --limit @/root/agents-ci/ansible/test/integration/nxos.retry

PLAY RECAP ************************************************************************************************************************************************************************************************************************************
n9k          : ok=6    changed=0    unreachable=0    failed=1   
```


Discussion:
cc @jedelman8 @GGabriele @rahushen @privateip @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_vpc_interface.py b/lib/ansible/modules/network/nxos/nxos_vpc_interface.py
--- a/lib/ansible/modules/network/nxos/nxos_vpc_interface.py
+++ b/lib/ansible/modules/network/nxos/nxos_vpc_interface.py
@@ -16,10 +16,10 @@
 # along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 #
 
-ANSIBLE_METADATA = {'metadata_version': '1.0',
-                    'status': ['preview'],
-                    'supported_by': 'community'}
-
+ANSIBLE_METADATA = {
+    'metadata_version': '1.0',
+    'status': ['preview'],
+    'supported_by': 'community'}
 
 DOCUMENTATION = '''
 ---
@@ -28,92 +28,57 @@
 version_added: "2.2"
 short_description: Manages interface VPC configuration
 description:
-    - Manages interface VPC configuration
+  - Manages interface VPC configuration
 author:
-    - Jason Edelman (@jedelman8)
-    - Gabriele Gerbino (@GGabriele)
+  - Jason Edelman (@jedelman8)
+  - Gabriele Gerbino (@GGabriele)
 notes:
-    - Either vpc or peer_link param is required, but not both.
-    - C(state=absent) removes whatever VPC config is on a port-channel
-      if one exists.
-    - Re-assigning a vpc or peerlink from one portchannel to another is not
-      supported.  The module will force the user to unconfigure an existing
-      vpc/pl before configuring the same value on a new portchannel
+  - Either vpc or peer_link param is required, but not both.
+  - C(state=absent) removes whatever VPC config is on a port-channel
+    if one exists.
+  - Re-assigning a vpc or peerlink from one portchannel to another is not
+    supported.  The module will force the user to unconfigure an existing
+    vpc/pl before configuring the same value on a new portchannel
 options:
-    portchannel:
-        description:
-            - Group number of the portchannel that will be configured.
-        required: true
-    vpc:
-        description:
-            - VPC group/id that will be configured on associated portchannel.
-        required: false
-        default: null
-    peer_link:
-        description:
-            - Set to true/false for peer link config on associated portchannel.
-        required: false
-        default: null
-    state:
-        description:
-            - Manages desired state of the resource.
-        required: true
-        choices: ['present','absent']
+  portchannel:
+    description:
+      - Group number of the portchannel that will be configured.
+    required: true
+  vpc:
+    description:
+      - VPC group/id that will be configured on associated portchannel.
+    required: false
+    default: null
+  peer_link:
+    description:
+      - Set to true/false for peer link config on associated portchannel.
+    required: false
+    default: null
+  state:
+    description:
+      - Manages desired state of the resource.
+    required: true
+    choices: ['present','absent']
 '''
 
 EXAMPLES = '''
-- nxos_vpc_portchannel:
+- nxos_vpc_interface:
     portchannel: 10
     vpc: 100
-    username: "{{ un }}"
-    password: "{{ pwd }}"
-    host: "{{ inventory_hostname }}"
 '''
 
 RETURN = '''
-proposed:
-    description: k/v pairs of parameters passed into module
-    returned: always
-    type: dict
-    sample: {"portchannel": "100", "vpc": "10"}
-existing:
-    description: k/v pairs of existing configuration
-    returned: always
-    type: dict
-    sample: {}
-end_state:
-    description: k/v pairs of configuration after module execution
-    returned: always
-    type: dict
-    sample:  {"peer-link": false, "portchannel": "100", "vpc": "10"}
-updates:
+commands:
     description: commands sent to the device
     returned: always
     type: list
     sample: ["interface port-channel100", "vpc 10"]
-changed:
-    description: check to see if a change was made on the device
-    returned: always
-    type: boolean
-    sample: true
 '''
 
-
 from ansible.module_utils.nxos import get_config, load_config, run_commands
 from ansible.module_utils.nxos import nxos_argument_spec, check_args
 from ansible.module_utils.basic import AnsibleModule
 
-def execute_show_command(command, module, command_type='cli_show'):
-    if module.params['transport'] == 'cli':
-        command += ' | json'
-        cmds = [command]
-        body = run_commands(module, cmds)
-    elif module.params['transport'] == 'nxapi':
-        cmds = [command]
-        body = run_commands(module, cmds)
-
-    return body
-
 
 def flatten_list(command_lists):
     flat_command_list = []
@@ -126,15 +91,13 @@ def flatten_list(command_lists):
 
 
 def get_portchannel_list(module):
-    command = 'show port-channel summary'
     portchannels = []
     pc_list = []
 
-    body = execute_show_command(command, module)
-
     try:
-        pc_list = body[0]['TABLE_channel']['ROW_channel']
-    except (KeyError, AttributeError):
+        body = run_commands(module, ['show port-channel summary | json'])[0]
+        pc_list = body['TABLE_channel']['ROW_channel']
+    except (KeyError, AttributeError, TypeError):
         return portchannels
 
     if pc_list:
@@ -148,13 +111,11 @@ def get_portchannel_list(module):
 
 
 def get_existing_portchannel_to_vpc_mappings(module):
-    command = 'show vpc brief'
     pc_vpc_mapping = {}
 
-    body = execute_show_command(command, module)
-
     try:
-        vpc_table = body[0]['TABLE_vpc']['ROW_vpc']
+        body = run_commands(module, ['show vpc brief | json'])[0]
+        vpc_table = body['TABLE_vpc']['ROW_vpc']
     except (KeyError, AttributeError, TypeError):
         vpc_table = None
 
@@ -170,7 +131,7 @@ def get_existing_portchannel_to_vpc_mappings(module):
 
 def peer_link_exists(module):
     found = False
-    run = get_vpc_running_config(module)
+    run = get_config(module, flags=['section vpc'])
 
     vpc_list = run.split('\n')
     for each in vpc_list:
@@ -179,38 +140,28 @@ def peer_link_exists(module):
     return found
 
 
-def get_vpc_running_config(module):
-    command = 'show running section vpc'
-    body = execute_show_command(command, module,
-                                command_type='cli_show_ascii')[0]
-
-    return body
-
-
 def get_active_vpc_peer_link(module):
-    command = 'show vpc brief'
     peer_link = None
-    body = execute_show_command(command, module)
+
     try:
-        peer_link = body[0]['TABLE_peerlink']['ROW_peerlink']['peerlink-ifindex']
-    except (KeyError, AttributeError):
+        body = run_commands(module, ['show vpc brief | json'])[0]
+        peer_link = body['TABLE_peerlink']['ROW_peerlink']['peerlink-ifindex']
+    except (KeyError, AttributeError, TypeError):
         return peer_link
 
     return peer_link
 
 
 def get_portchannel_vpc_config(module, portchannel):
-    command = 'show vpc brief'
     peer_link_pc = None
     peer_link = False
     vpc = ""
     pc = ""
     config = {}
 
-    body = execute_show_command(command, module)
-
     try:
-        table = body[0]['TABLE_peerlink']['ROW_peerlink']
+        body = run_commands(module, ['show vpc brief | json'])[0]
+        table = body['TABLE_peerlink']['ROW_peerlink']
     except (KeyError, AttributeError, TypeError):
         table = {}
 
@@ -255,33 +206,58 @@ def get_commands_to_config_vpc_interface(portchannel, delta, config_value, exist
     return commands
 
 
+def state_present(portchannel, delta, config_value, existing):
+    commands = []
+
+    command = get_commands_to_config_vpc_interface(
+        portchannel,
+        delta,
+        config_value,
+        existing
+    )
+    commands.append(command)
+
+    return commands
+
+
+def state_absent(portchannel, existing):
+    commands = []
+    if existing.get('vpc'):
+        command = 'no vpc'
+        commands.append(command)
+    elif existing.get('peer-link'):
+        command = 'no vpc peer-link'
+        commands.append(command)
+    if commands:
+        commands.insert(0, 'interface port-channel{0}'.format(portchannel))
+
+    return commands
+
+
 def main():
     argument_spec = dict(
         portchannel=dict(required=True, type='str'),
         vpc=dict(required=False, type='str'),
         peer_link=dict(required=False, type='bool'),
-        state=dict(choices=['absent', 'present'], default='present'),
-        include_defaults=dict(default=False),
-        config=dict(),
-        save=dict(type='bool', default=False)
+        state=dict(choices=['absent', 'present'], default='present')
     )
 
     argument_spec.update(nxos_argument_spec)
 
     module = AnsibleModule(argument_spec=argument_spec,
-                                mutually_exclusive=[['vpc', 'peer_link']],
-                                supports_check_mode=True)
+                           mutually_exclusive=[['vpc', 'peer_link']],
+                           supports_check_mode=True)
 
     warnings = list()
+    commands = []
     check_args(module, warnings)
-
+    results = {'changed': False, 'warnings': warnings}
 
     portchannel = module.params['portchannel']
     vpc = module.params['vpc']
     peer_link = module.params['peer_link']
     state = module.params['state']
 
-    changed = False
     args = {'portchannel': portchannel, 'vpc': vpc, 'peer-link': peer_link}
     active_peer_link = None
 
@@ -294,14 +270,14 @@ def main():
 
         if vpc in mapping and portchannel != mapping[vpc].strip('Po'):
             module.fail_json(msg="This vpc is already configured on "
-                                 "another portchannel.  Remove it first "
+                                 "another portchannel. Remove it first "
                                  "before trying to assign it here. ",
                              existing_portchannel=mapping[vpc])
 
         for vpcid, existing_pc in mapping.items():
             if portchannel == existing_pc.strip('Po') and vpcid != vpc:
                 module.fail_json(msg="This portchannel already has another"
-                                     " VPC configured.  Remove it first "
+                                     " VPC configured. Remove it first "
                                      "before assigning this one",
                                  existing_vpc=vpcid)
 
@@ -309,7 +285,7 @@ def main():
             active_peer_link = get_active_vpc_peer_link(module)
             if active_peer_link[-2:] == portchannel:
                 module.fail_json(msg="That port channel is the current "
-                                     "PEER LINK.  Remove it if you want it"
+                                     "PEER LINK. Remove it if you want it"
                                      " to be a VPC")
         config_value = vpc
 
@@ -319,64 +295,34 @@ def main():
             if active_peer_link != portchannel:
                 if peer_link:
                     module.fail_json(msg="A peer link already exists on"
-                                         " the device.  Remove it first",
-                                     current_peer_link='Po{0}'.format(
-                                         active_peer_link))
+                                         " the device. Remove it first",
+                                     current_peer_link='Po{0}'.format(active_peer_link))
         config_value = 'peer-link'
 
-
     proposed = dict((k, v) for k, v in args.items() if v is not None)
     existing = get_portchannel_vpc_config(module, portchannel)
-    end_state = existing
-    commands = []
 
     if state == 'present':
         delta = dict(set(proposed.items()).difference(existing.items()))
         if delta:
-            command = get_commands_to_config_vpc_interface(
-                portchannel,
-                delta,
-                config_value,
-                existing
-                )
-            commands.append(command)
-
-    elif state == 'absent':
-        if existing.get('vpc'):
-            command = ['no vpc']
-            commands.append(command)
-        elif existing.get('peer-link'):
-            command = ['no vpc peer-link']
-            commands.append(command)
-        if commands:
-            commands.insert(0, ['interface port-channel{0}'.format(portchannel)])
+            commands = state_present(portchannel, delta, config_value, existing)
+
+    elif state == 'absent' and existing:
+        commands = state_absent(portchannel, existing)
 
     cmds = flatten_list(commands)
     if cmds:
         if module.check_mode:
             module.exit_json(changed=True, commands=cmds)
         else:
-            changed = True
             load_config(module, cmds)
-            if module.params['transport'] == 'cli':
-                output = ' '.join(output)
-                if 'error' in output.lower():
-                    module.fail_json(msg=output.replace('\n', ''))
-            end_state = get_portchannel_vpc_config(module, portchannel)
+            results['changed'] = True
             if 'configure' in cmds:
                 cmds.pop(0)
 
-    results = {}
-    results['proposed'] = proposed
-    results['existing'] = existing
-    results['end_state'] = end_state
-    results['updates'] = cmds
-    results['changed'] = changed
-    results['warnings'] = warnings
-
+    results['commands'] = cmds
     module.exit_json(**results)
 
 
 if __name__ == '__main__':
     main()
-


Test patch:
diff --git a/test/sanity/pep8/legacy-files.txt b/test/sanity/pep8/legacy-files.txt
--- a/test/sanity/pep8/legacy-files.txt
+++ b/test/sanity/pep8/legacy-files.txt
@@ -395,7 +395,6 @@ lib/ansible/modules/network/nxos/nxos_system.py
 lib/ansible/modules/network/nxos/nxos_udld.py
 lib/ansible/modules/network/nxos/nxos_udld_interface.py
 lib/ansible/modules/network/nxos/nxos_user.py
-lib/ansible/modules/network/nxos/nxos_vpc_interface.py
 lib/ansible/modules/network/nxos/nxos_vrf.py
 lib/ansible/modules/network/nxos/nxos_vrf_interface.py
 lib/ansible/modules/network/nxos/nxos_vrrp.py
diff --git a/test/units/modules/network/nxos/fixtures/nxos_vpc_interface/show_port-channel_summary b/test/units/modules/network/nxos/fixtures/nxos_vpc_interface/show_port-channel_summary
new file mode 100644
--- /dev/null
+++ b/test/units/modules/network/nxos/fixtures/nxos_vpc_interface/show_port-channel_summary
@@ -0,0 +1,22 @@
+{
+  "TABLE_channel": {
+    "ROW_channel": [
+      {
+        "group": "10",
+        "port-channel": "port-channel10",
+        "layer": "R",
+        "status": "D",
+        "type": "Eth",
+        "prtcl": "NONE"
+      },
+      {
+        "group": "20",
+        "port-channel": "port-channel20",
+        "layer": "R",
+        "status": "D",
+        "type": "Eth",
+        "prtcl": "NONE"
+      }
+    ]
+  }
+}
diff --git a/test/units/modules/network/nxos/fixtures/nxos_vpc_interface/show_vpc_brief b/test/units/modules/network/nxos/fixtures/nxos_vpc_interface/show_vpc_brief
new file mode 100644
--- /dev/null
+++ b/test/units/modules/network/nxos/fixtures/nxos_vpc_interface/show_vpc_brief
@@ -0,0 +1,14 @@
+{
+  "TABLE_vpc": {
+    "ROW_vpc": [
+      {
+	"vpc-id": 100,
+	"vpc-ifindex": "Po10",
+	"vpc-port-state": "Up",
+	"vpc-thru-peerlink": 19,
+	"vpc-consistency": "consistent",
+	"vpc-consistency-status": "SUCCESS"
+       }
+    ]
+  }
+}
diff --git a/test/units/modules/network/nxos/test_nxos_vpc_interface.py b/test/units/modules/network/nxos/test_nxos_vpc_interface.py
new file mode 100644
--- /dev/null
+++ b/test/units/modules/network/nxos/test_nxos_vpc_interface.py
@@ -0,0 +1,69 @@
+# (c) 2016 Red Hat Inc.
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+
+from ansible.compat.tests.mock import patch
+from ansible.modules.network.nxos import nxos_vpc_interface
+from .nxos_module import TestNxosModule, load_fixture, set_module_args
+
+
+class TestNxosVpcModule(TestNxosModule):
+
+    module = nxos_vpc_interface
+
+    def setUp(self):
+        self.mock_load_config = patch('ansible.modules.network.nxos.nxos_vpc_interface.load_config')
+        self.load_config = self.mock_load_config.start()
+
+        self.mock_get_config = patch('ansible.modules.network.nxos.nxos_vpc_interface.get_config')
+        self.get_config = self.mock_get_config.start()
+
+        self.mock_run_commands = patch('ansible.modules.network.nxos.nxos_vpc_interface.run_commands')
+        self.run_commands = self.mock_run_commands.start()
+
+    def tearDown(self):
+        self.mock_load_config.stop()
+        self.mock_get_config.stop()
+        self.mock_run_commands.stop()
+
+    def load_fixtures(self, commands=None):
+        def load_from_file(*args, **kwargs):
+            module, commands = args
+            output = list()
+            for command in commands:
+                filename = str(command).split(' | ')[0].replace(' ', '_')
+                filename = os.path.join('nxos_vpc_interface', filename)
+                output.append(load_fixture(filename))
+            return output
+
+        self.run_commands.side_effect = load_from_file
+        self.load_config.return_value = None
+
+    def test_nxos_vpc_interface_absent(self):
+        set_module_args(dict(portchannel=10, vpc=100, state='absent'))
+        result = self.execute_module(changed=True)
+        self.assertEqual(result['commands'], ['interface port-channel10', 'no vpc'])
+
+    def test_nxos_vpc_interface_present(self):
+        set_module_args(dict(portchannel=20, vpc=200, state='present'))
+        result = self.execute_module(changed=True)
+        self.assertEqual(result['commands'], ['interface port-channel20', 'vpc 200'])