Repository: ansible/ansible

Issue:
nxos_pim issues
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master 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_pim
##### 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)I7(1)

##### SUMMARY
<!--- Explain the problem briefly -->

##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used.
-->
There are multiple issues with nxos_pim module.

1. ssm_range is actually a list. The platform allows multiple ssm ranges to be configured at the same time. However, the code has no such support and only one ssm range can be configured.

2. Once ssm_range is configured, it cannot be removed by using 'default' keyword. Please note that 'none' is totally different from 'default'
 
3. No integration tests exist for nxos_pim module. 

Note: I will open a PR shortly to address these above issues


Discussion:

Resolving patch:
diff --git a/lib/ansible/modules/network/nxos/nxos_pim.py b/lib/ansible/modules/network/nxos/nxos_pim.py
--- a/lib/ansible/modules/network/nxos/nxos_pim.py
+++ b/lib/ansible/modules/network/nxos/nxos_pim.py
@@ -34,7 +34,8 @@
   ssm_range:
     description:
       - Configure group ranges for Source Specific Multicast (SSM).
-        Valid values are multicast addresses or the keyword 'none'.
+        Valid values are multicast addresses or the keyword 'none'
+        or keyword 'default'
     required: true
 '''
 EXAMPLES = '''
@@ -75,7 +76,9 @@ def get_existing(module, args):
         value = ''
         if has_command:
             value = has_command.group('value')
-        existing[arg] = value
+            if value == '232.0.0.0/8':
+                value = ''  # remove the reserved value
+        existing[arg] = value.split()
     return existing
 
 
@@ -93,8 +96,13 @@ def get_commands(module, existing, proposed, candidate):
     proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed)
 
     for key, value in proposed_commands.items():
-        command = '{0} {1}'.format(key, value)
-        commands.append(command)
+        if key == 'ip pim ssm range' and value == 'default':
+            # no cmd needs a value but the actual value does not matter
+            command = 'no ip pim ssm range none'
+            commands.append(command)
+        else:
+            command = '{0} {1}'.format(key, value)
+            commands.append(command)
 
     if commands:
         candidate.add(commands, parents=[])
@@ -102,7 +110,7 @@ def get_commands(module, existing, proposed, candidate):
 
 def main():
     argument_spec = dict(
-        ssm_range=dict(required=True, type='str'),
+        ssm_range=dict(required=True, type='list'),
     )
 
     argument_spec.update(nxos_argument_spec)
@@ -114,16 +122,29 @@ def main():
     check_args(module, warnings)
     result = {'changed': False, 'commands': [], 'warnings': warnings}
 
-    splitted_ssm_range = module.params['ssm_range'].split('.')
-    if len(splitted_ssm_range) != 4 and module.params['ssm_range'] != 'none':
-        module.fail_json(msg="Valid ssm_range values are multicast addresses "
-                             "or the keyword 'none'.")
+    ssm_range_list = module.params['ssm_range']
+    for item in ssm_range_list:
+        splitted_ssm_range = item.split('.')
+        if len(splitted_ssm_range) != 4 and item != 'none' and item != 'default':
+            module.fail_json(msg="Valid ssm_range values are multicast addresses "
+                                 "or the keyword 'none' or the keyword 'default'.")
 
     args = PARAM_TO_COMMAND_KEYMAP.keys()
 
     existing = get_existing(module, args)
-    proposed = dict((k, v) for k, v in module.params.items()
-                    if k in args and v != existing[k])
+    proposed_args = dict((k, v) for k, v in module.params.items() if k in args)
+
+    proposed = {}
+    for key, value in proposed_args.items():
+        if key == 'ssm_range':
+            if value[0] == 'default':
+                if existing.get(key):
+                    proposed[key] = 'default'
+            else:
+                v = sorted(set([str(i) for i in value]))
+                ex = sorted(set([str(i) for i in existing.get(key)]))
+                if v != ex:
+                    proposed[key] = ' '.join(str(s) for s in v)
 
     candidate = CustomNetworkConfig(indent=3)
     get_commands(module, existing, 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
@@ -518,6 +518,15 @@
             failed_modules: "{{ failed_modules }} + [ 'nxos_overlay_global' ]"
             test_failed: true
 
+    - block:
+      - include_role:
+          name: nxos_pim
+        when: "limit_to in ['*', 'nxos_pim']"
+      rescue:
+        - set_fact:
+            failed_modules: "{{ failed_modules }} + [ 'nxos_pim' ]"
+            test_failed: true
+
     - block:
       - include_role:
           name: nxos_pim_interface
diff --git a/test/integration/targets/nxos_pim/defaults/main.yaml b/test/integration/targets/nxos_pim/defaults/main.yaml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/nxos_pim/defaults/main.yaml
@@ -0,0 +1,2 @@
+---
+testcase: "*"
diff --git a/test/integration/targets/nxos_pim/meta/main.yml b/test/integration/targets/nxos_pim/meta/main.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/nxos_pim/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+  - prepare_nxos_tests
diff --git a/test/integration/targets/nxos_pim/tasks/cli.yaml b/test/integration/targets/nxos_pim/tasks/cli.yaml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/nxos_pim/tasks/cli.yaml
@@ -0,0 +1,33 @@
+---
+- name: collect common cli test cases
+  find:
+    paths: "{{ role_path }}/tests/common"
+    patterns: "{{ testcase }}.yaml"
+  connection: local
+  register: test_cases
+
+- name: collect cli test cases
+  find:
+    paths: "{{ role_path }}/tests/cli"
+    patterns: "{{ testcase }}.yaml"
+  connection: local
+  register: cli_cases
+
+- set_fact:
+    test_cases:
+      files: "{{ test_cases.files }} + {{ cli_cases.files }}"
+
+- name: set test_items
+  set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
+
+- name: run test cases (connection=network_cli)
+  include: "{{ test_case_to_run }} ansible_connection=network_cli connection={}"
+  with_items: "{{ test_items }}"
+  loop_control:
+    loop_var: test_case_to_run
+
+- name: run test case (connection=local)
+  include: "{{ test_case_to_run }} ansible_connection=local connection={{ cli }}"
+  with_first_found: "{{ test_items }}"
+  loop_control:
+    loop_var: test_case_to_run
diff --git a/test/integration/targets/nxos_pim/tasks/main.yaml b/test/integration/targets/nxos_pim/tasks/main.yaml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/nxos_pim/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_pim/tasks/nxapi.yaml b/test/integration/targets/nxos_pim/tasks/nxapi.yaml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/nxos_pim/tasks/nxapi.yaml
@@ -0,0 +1,27 @@
+---
+- name: collect common nxapi test cases
+  find:
+    paths: "{{ role_path }}/tests/common"
+    patterns: "{{ testcase }}.yaml"
+  connection: local
+  register: test_cases
+
+- name: collect nxapi test cases
+  find:
+    paths: "{{ role_path }}/tests/nxapi"
+    patterns: "{{ testcase }}.yaml"
+  connection: local
+  register: nxapi_cases
+
+- set_fact:
+    test_cases:
+      files: "{{ test_cases.files }} + {{ nxapi_cases.files }}"
+
+- name: set test_items
+  set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
+
+- name: run test cases (connection=local)
+  include: "{{ test_case_to_run }} ansible_connection=local connection={{ nxapi }}"
+  with_items: "{{ test_items }}"
+  loop_control:
+    loop_var: test_case_to_run
diff --git a/test/integration/targets/nxos_pim/tests/common/sanity.yaml b/test/integration/targets/nxos_pim/tests/common/sanity.yaml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/nxos_pim/tests/common/sanity.yaml
@@ -0,0 +1,74 @@
+---
+- debug: msg="START connection={{ ansible_connection }} nxos_pim sanity test"
+- debug: msg="Using provider={{ connection.transport }}"
+  when: ansible_connection == "local"
+
+- name: "Setup: Disable feature PIM"
+  nxos_feature: &disable_feature
+    feature: pim
+    state: disabled
+    provider: "{{ connection }}"
+
+- name: "Setup: Enable feature PIM"
+  nxos_feature:
+    feature: pim
+    state: enabled
+    provider: "{{ connection }}"
+
+- name: "Setup: Configure ssm_range none"
+  nxos_pim: &none
+    ssm_range: "none"
+    provider: "{{ connection }}"
+
+- block:
+  - name: Configure ssm_range
+    nxos_pim: &configure
+      ssm_range: 
+        - "239.128.1.0/24"
+        - "224.0.0.0/8"
+      provider: "{{ connection }}"
+    register: result
+
+  - assert: &true
+      that:
+        - "result.changed == true"
+
+  - name: Check idempotence
+    nxos_pim: *configure
+    register: result
+
+  - assert: &false
+      that:
+        - "result.changed == false"
+
+  - name: Configure ssm_range default
+    nxos_pim: &conf_default
+      ssm_range: "default"
+      provider: "{{ connection }}"
+    register: result
+
+  - assert: *true
+
+  - name: Check idempotence
+    nxos_pim: *conf_default
+    register: result
+
+  - assert: *false
+
+  - name: Configure ssm_range none
+    nxos_pim: *none
+    register: result
+
+  - assert: *true
+
+  - name: Check idempotence
+    nxos_pim: *none
+    register: result
+
+  - assert: *false
+
+  always:
+  - name: "Disable feature PIM"
+    nxos_feature: *disable_feature
+
+- debug: msg="END connection={{ ansible_connection }} nxos_pim sanity test"