Repository: ansible/ansible

Issue:
nxos_vpc 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.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest -->
 - Bug Report

##### COMPONENT NAME
<!---
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_vpc
##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
ansible 2.5.0 (devel 788010d0f0) last updated 2018/01/08 11:49:21 (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)I5(2)

##### SUMMARY
There are multiple issues with nxos_vpc module

1. Setting parameters to default is not supported
2. Most of the parameters are not idempotent
3. auto_recovery and peer_gw are optional parameters but they were taken as required.
4. auto_recovery default is different between different platforms and this is not taken care.
5. pkl_vrf and pkl_src are optional when setting peer-keepalive but pkl_src is forced as a requirement.
6. Integration test cases are not complete

Note: I will open a PR shortly to address these above issues


Discussion:

Resolving patch:
diff --git a/lib/ansible/modules/network/nxos/nxos_vpc.py b/lib/ansible/modules/network/nxos/nxos_vpc.py
--- a/lib/ansible/modules/network/nxos/nxos_vpc.py
+++ b/lib/ansible/modules/network/nxos/nxos_vpc.py
@@ -74,12 +74,12 @@
   peer_gw:
     description:
       - Enables/Disables peer gateway
-    required: true
+    required: false
     choices: ['true','false']
   auto_recovery:
     description:
       - Enables/Disables auto recovery
-    required: true
+    required: false
     choices: ['true','false']
   delay_restore:
     description:
@@ -125,6 +125,7 @@
             "auto-recovery", "peer-gateway"]
 '''
 
+import re
 from ansible.module_utils.network.nxos.nxos import get_config, load_config, run_commands
 from ansible.module_utils.network.nxos.nxos import nxos_argument_spec, check_args
 from ansible.module_utils.basic import AnsibleModule
@@ -138,6 +139,13 @@
     'auto_recovery': '{auto_recovery} auto-recovery',
 }
 
+PARAM_TO_DEFAULT_KEYMAP = {
+    'delay_restore': '60',
+    'role_priority': '32667',
+    'system_priority': '32667',
+    'peer_gw': False,
+}
+
 
 def flatten_list(command_lists):
     flat_command_list = []
@@ -165,65 +173,60 @@ def get_vrf_list(module):
     return vrf_list
 
 
+def get_auto_recovery_default(module):
+    auto = False
+    data = run_commands(module, ['show inventory | json'])[0]
+    pid = data['TABLE_inv']['ROW_inv'][0]['productid']
+    if re.search(r'N7K', pid):
+        auto = True
+    elif re.search(r'N9K', pid):
+        data = run_commands(module, ['show hardware | json'])[0]
+        ver = data['kickstart_ver_str']
+        if re.search(r'7.0\(3\)F', ver):
+            auto = True
+
+    return auto
+
+
 def get_vpc(module):
     body = run_commands(module, ['show vpc | json'])[0]
-
     domain = str(body['vpc-domain-id'])
-    auto_recovery = 'enabled' in str(body['vpc-auto-recovery-status']).lower()
-
     vpc = {}
     if domain != 'not configured':
-        delay_restore = None
-        pkl_src = None
-        role_priority = '32667'
-        system_priority = None
-        pkl_dest = None
-        pkl_vrf = None
-        peer_gw = False
-
         run = get_config(module, flags=['vpc'])
         if run:
+            vpc['domain'] = domain
+            for key in PARAM_TO_DEFAULT_KEYMAP.keys():
+                vpc[key] = PARAM_TO_DEFAULT_KEYMAP.get(key)
+            vpc['auto_recovery'] = get_auto_recovery_default(module)
             vpc_list = run.split('\n')
             for each in vpc_list:
-                if 'delay restore' in each:
-                    line = each.split()
-                    if len(line) == 3:
-                        delay_restore = line[-1]
-                if 'peer-keepalive destination' in each:
-                    line = each.split()
-                    pkl_dest = line[2]
-                    for word in line:
-                        if 'source' in word:
-                            index = line.index(word)
-                            pkl_src = line[index + 1]
                 if 'role priority' in each:
                     line = each.split()
-                    role_priority = line[-1]
+                    vpc['role_priority'] = line[-1]
                 if 'system-priority' in each:
                     line = each.split()
-                    system_priority = line[-1]
+                    vpc['system_priority'] = line[-1]
+                if 'delay restore' in each:
+                    line = each.split()
+                    vpc['delay_restore'] = line[-1]
+                if 'no auto-recovery' in each:
+                    vpc['auto_recovery'] = False
+                elif 'auto-recovery' in each:
+                    vpc['auto_recovery'] = True
                 if 'peer-gateway' in each:
-                    peer_gw = True
-
-        body = run_commands(module, ['show vpc peer-keepalive | json'])[0]
-
-        if body:
-            pkl_dest = body['vpc-keepalive-dest']
-            if 'N/A' in pkl_dest:
-                pkl_dest = None
-            elif len(pkl_dest) == 2:
-                pkl_dest = pkl_dest[0]
-            pkl_vrf = str(body['vpc-keepalive-vrf'])
-
-        vpc['domain'] = domain
-        vpc['auto_recovery'] = auto_recovery
-        vpc['delay_restore'] = delay_restore
-        vpc['pkl_src'] = pkl_src
-        vpc['role_priority'] = role_priority
-        vpc['system_priority'] = system_priority
-        vpc['pkl_dest'] = pkl_dest
-        vpc['pkl_vrf'] = pkl_vrf
-        vpc['peer_gw'] = peer_gw
+                    vpc['peer_gw'] = True
+                if 'peer-keepalive destination' in each:
+                    line = each.split()
+                    vpc['pkl_dest'] = line[2]
+                    vpc['pkl_vrf'] = 'management'
+                    if 'source' in each:
+                        vpc['pkl_src'] = line[4]
+                        if 'vrf' in each:
+                            vpc['pkl_vrf'] = line[6]
+                    else:
+                        if 'vrf' in each:
+                            vpc['pkl_vrf'] = line[4]
 
     return vpc
 
@@ -232,40 +235,24 @@ def get_commands_to_config_vpc(module, vpc, domain, existing):
     vpc = dict(vpc)
 
     domain_only = vpc.get('domain')
-    pkl_src = vpc.get('pkl_src')
-    pkl_dest = vpc.get('pkl_dest')
-    pkl_vrf = vpc.get('pkl_vrf') or existing.get('pkl_vrf')
-    vpc['pkl_vrf'] = pkl_vrf
 
     commands = []
-    if pkl_src or pkl_dest:
-        if pkl_src is None:
-            vpc['pkl_src'] = existing.get('pkl_src')
-        elif pkl_dest is None:
-            vpc['pkl_dest'] = existing.get('pkl_dest')
-        pkl_command = 'peer-keepalive destination {pkl_dest}'.format(**vpc) \
-                      + ' source {pkl_src} vrf {pkl_vrf}'.format(**vpc)
+    if 'pkl_dest' in vpc:
+        pkl_command = 'peer-keepalive destination {pkl_dest}'.format(**vpc)
+        if 'pkl_src' in vpc:
+            pkl_command += ' source {pkl_src}'.format(**vpc)
+        if 'pkl_vrf' in vpc and vpc['pkl_vrf'] != 'management':
+            pkl_command += ' vrf {pkl_vrf}'.format(**vpc)
         commands.append(pkl_command)
-    elif pkl_vrf:
-        pkl_src = existing.get('pkl_src')
-        pkl_dest = existing.get('pkl_dest')
-        if pkl_src and pkl_dest:
-            pkl_command = ('peer-keepalive destination {0}'
-                           ' source {1} vrf {2}'.format(pkl_dest, pkl_src, pkl_vrf))
-            commands.append(pkl_command)
-
-    if vpc.get('auto_recovery') is False:
-        vpc['auto_recovery'] = 'no'
-    else:
-        vpc['auto_recovery'] = ''
 
-    if 'peer_gw' in vpc:
-        if vpc.get('peer_gw') is False:
-            vpc['peer_gw'] = 'no'
+    if 'auto_recovery' in vpc:
+        if not vpc.get('auto_recovery'):
+            vpc['auto_recovery'] = 'no'
         else:
-            vpc['peer_gw'] = ''
-    else:
-        if existing.get('peer_gw') is False:
+            vpc['auto_recovery'] = ''
+
+    if 'peer_gw' in vpc:
+        if not vpc.get('peer_gw'):
             vpc['peer_gw'] = 'no'
         else:
             vpc['peer_gw'] = ''
@@ -283,14 +270,6 @@ def get_commands_to_config_vpc(module, vpc, domain, existing):
     return commands
 
 
-def get_commands_to_remove_vpc_interface(portchannel, config_value):
-    commands = []
-    command = 'no vpc {0}'.format(config_value)
-    commands.append(command)
-    commands.insert(0, 'interface port-channel{0}'.format(portchannel))
-    return commands
-
-
 def main():
     argument_spec = dict(
         domain=dict(required=True, type='str'),
@@ -298,9 +277,9 @@ def main():
         system_priority=dict(required=False, type='str'),
         pkl_src=dict(required=False),
         pkl_dest=dict(required=False),
-        pkl_vrf=dict(required=False, default='management'),
-        peer_gw=dict(required=True, type='bool'),
-        auto_recovery=dict(required=True, type='bool'),
+        pkl_vrf=dict(required=False),
+        peer_gw=dict(required=False, type='bool'),
+        auto_recovery=dict(required=False, type='bool'),
         delay_restore=dict(required=False, type='str'),
         state=dict(choices=['absent', 'present'], default='present'),
     )
@@ -331,18 +310,17 @@ def main():
                 auto_recovery=auto_recovery,
                 delay_restore=delay_restore)
 
-    if not (pkl_src and pkl_dest and pkl_vrf):
-        # if only the source or dest is set, it'll fail and ask to set the
-        # other
-        if pkl_src or pkl_dest:
-            module.fail_json(msg='source AND dest IP for pkl are required at '
-                                 'this time (although source is technically not '
-                                 ' required by the device.)')
-
-        args.pop('pkl_src')
-        args.pop('pkl_dest')
-        args.pop('pkl_vrf')
-
+    if not pkl_dest:
+        if pkl_src:
+            module.fail_json(msg='dest IP for peer-keepalive is required'
+                                 ' when src IP is present')
+        elif pkl_vrf:
+            if pkl_vrf != 'management':
+                module.fail_json(msg='dest and src IP for peer-keepalive are required'
+                                     ' when vrf is present')
+            else:
+                module.fail_json(msg='dest IP for peer-keepalive is required'
+                                     ' when vrf is present')
     if pkl_vrf:
         if pkl_vrf.lower() not in get_vrf_list(module):
             module.fail_json(msg='The VRF you are trying to use for the peer '
@@ -353,7 +331,12 @@ def main():
 
     commands = []
     if state == 'present':
-        delta = set(proposed.items()).difference(existing.items())
+        delta = {}
+        for key, value in proposed.items():
+            if str(value).lower() == 'default':
+                value = PARAM_TO_DEFAULT_KEYMAP.get(key)
+            if existing.get(key) != value:
+                delta[key] = value
         if delta:
             command = get_commands_to_config_vpc(module, delta, domain, existing)
             commands.append(command)


Test patch:
diff --git a/test/integration/targets/nxos_vpc/tests/common/sanity.yaml b/test/integration/targets/nxos_vpc/tests/common/sanity.yaml
--- a/test/integration/targets/nxos_vpc/tests/common/sanity.yaml
+++ b/test/integration/targets/nxos_vpc/tests/common/sanity.yaml
@@ -19,14 +19,9 @@
     nxos_vpc: &conf_vpc
       state: present
       domain: 100
-      role_priority: 500
-      system_priority: 2000
       pkl_dest: 192.168.100.4
       pkl_src: 10.1.100.20
       pkl_vrf: ntc
-      peer_gw: true
-      delay_restore: 5
-      auto_recovery: true
       provider: "{{ connection }}"
     register: result
 
@@ -42,18 +37,134 @@
       that:
         - "result.changed == false"
 
+  - name: Configure vpc1
+    nxos_vpc: &conf_vpc1
+      state: present
+      domain: 100
+      role_priority: 500
+      system_priority: 2000
+      peer_gw: True
+      delay_restore: 5
+      provider: "{{ connection }}"
+    register: result
+
+  - assert: *true
+
+  - name: "Conf Idempotence"
+    nxos_vpc: *conf_vpc1
+    register: result
+
+  - assert: *false
+
+  - block:
+    - name: Configure auto1
+      nxos_vpc: &auto_false
+        state: present
+        domain: 100
+        auto_recovery: False
+        provider: "{{ connection }}"
+      register: result
+
+    - assert: *true
+
+    - name: "Conf Idempotence"
+      nxos_vpc: *auto_false
+      register: result
+
+    - assert: *false
+
+    - name: Configure auto2
+      nxos_vpc: &auto_true
+        state: present
+        domain: 100
+        auto_recovery: True
+        provider: "{{ connection }}"
+      register: result
+
+    - assert: *true
+
+    - name: "Conf Idempotence"
+      nxos_vpc: *auto_true
+      register: result
+
+    - assert: *false
+
+    when: (platform is search("N7K|N9K-F"))
+
+  - block:
+    - name: Configure auto1
+      nxos_vpc: &auto_true1
+        state: present
+        domain: 100
+        auto_recovery: True
+        provider: "{{ connection }}"
+      register: result
+
+    - assert: *true
+
+    - name: "Conf Idempotence"
+      nxos_vpc: *auto_true1
+      register: result
+
+    - assert: *false
+
+    - name: Configure auto2
+      nxos_vpc: &auto_false1
+        state: present
+        domain: 100
+        auto_recovery: False
+        provider: "{{ connection }}"
+      register: result
+
+    - assert: *true
+
+    - name: "Conf Idempotence"
+      nxos_vpc: *auto_false1
+      register: result
+
+    - assert: *false
+
+    when: not (platform is search("N7K|N9K-F"))
+
+  - name: Configure vpc2
+    nxos_vpc: &conf_vpc2
+      state: present
+      domain: 100
+      role_priority: default
+      system_priority: default
+      peer_gw: True
+      delay_restore: default
+      provider: "{{ connection }}"
+    register: result
+
+  - assert: *true
+
+  - name: "Conf Idempotence"
+    nxos_vpc: *conf_vpc2
+    register: result
+
+  - assert: *false
+
+  - name: Configure vpc3
+    nxos_vpc: &conf_vpc3
+      state: present
+      domain: 100
+      peer_gw: False
+      provider: "{{ connection }}"
+    register: result
+
+  - assert: *true
+
+  - name: "Conf Idempotence"
+    nxos_vpc: *conf_vpc3
+    register: result
+
+  - assert: *false
+
   - name: remove vpc
     nxos_vpc: &rem_vpc
       state: absent
       domain: 100
-      role_priority: 32667
-      system_priority: 2000
-      pkl_dest: 192.168.100.4
-      pkl_src: 10.1.100.20
-      pkl_vrf: ntc
-      peer_gw: true
-      delay_restore: 5
-      auto_recovery: false
       provider: "{{ connection }}"
     register: result
 
diff --git a/test/units/modules/network/nxos/test_nxos_vpc.py b/test/units/modules/network/nxos/test_nxos_vpc.py
--- a/test/units/modules/network/nxos/test_nxos_vpc.py
+++ b/test/units/modules/network/nxos/test_nxos_vpc.py
@@ -61,6 +61,6 @@ def test_nxos_vpc_present(self):
                              peer_gw=True, auto_recovery=True))
         self.execute_module(changed=True, commands=[
             'vpc domain 100', 'terminal dont-ask', 'role priority 32667', 'system-priority 2000',
-            'peer-keepalive destination 192.168.100.4 source 10.1.100.20 vrf management',
+            'peer-keepalive destination 192.168.100.4 source 10.1.100.20',
             'peer-gateway', 'auto-recovery',
         ])