Repository: ansible/ansible

Issue:
nxos_bgp_af - client_to_client property is not idempotent
<!---
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/feature -->
- nxos_bgp_af

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
ansible 2.4.0 (devel 6bed831f8c) last updated 2017/06/27 15:24:19 (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
<!---
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: Nexus9000 C9508  running 7.0(3)I6(1)

##### SUMMARY
<!--- Explain the problem briefly -->
The property client_to_client is not idempotent. The same playbook applied the second time reports failed.

##### 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.
-->

<!--- Paste example playbooks or commands between quotes below -->
```yaml
---
cli:
  host: "{{ inventory_hostname }}"
  username: "{{ nxos_cli_user | default('admin') }}"
  password: "{{ nxos_cli_pass | default('admin') }}"

nxapi:
  host: "{{ inventory_hostname }}"
  username: "{{ nxos_nxapi_user | default('admin') }}"
  password: "{{ nxos_nxapi_pass | default('admin') }}"
  transport: nxapi
  user_ssl: no

---
- name: NXOS Sample Playbook
  hosts: nxos
  connection: local
  gather_facts: no

  tasks:

  - name: Ensure BGP Is Enabled
    nxos_feature:
      feature: bgp
      state: enabled
      provider: "{{ cli }}"


  - name: "Enable feature nv overlay"
    nxos_feature:
      feature: nv overlay
      state: enabled
      provider: "{{ cli }}"

  - name: "Enable nv overlay evpn"
    nxos_config:
      lines:
        - nv overlay evpn
      provider: "{{ cli }}"
    ignore_errors: yes

  - name: "Configure BGP_AF non defaults"
    nxos_bgp_af: &configure_non_default
      asn: 65535
      vrf: TESTING
      afi: ipv4
      safi: unicast
      advertise_l2vpn_evpn: true
      additional_paths_install: true
      additional_paths_receive: true
      additional_paths_selection: RouteMap
      additional_paths_send: true
      client_to_client: false
      dampen_igp_metric: 200
      dampening_half_time: 1
      dampening_max_suppress_time: 4
      dampening_reuse_time: 2
      dampening_suppress_time: 3
      default_information_originate: true
      default_metric: 50
      distance_ebgp: 30
      distance_ibgp: 60
      distance_local: 90
      maximum_paths: 9
      maximum_paths_ibgp: 9
      next_hop_route_map: RouteMap
      suppress_inactive: true
      table_map: RouteMap
      table_map_filter: true
      state: present
      provider: "{{ cli }}"
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
Expected playbook to pass a second time

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```
PLAY [NXOS Sample Playbook] *************************************************************************************************************************************************************************************************************************************

TASK [Ensure BGP Is Enabled] ************************************************************************************************************************************************************************************************************************************
changed: [nxxxx.cisco.com]

TASK [Enable feature nv overlay] ********************************************************************************************************************************************************************************************************************************
ok: [nxxxx.cisco.com]

TASK [Enable nv overlay evpn] ***********************************************************************************************************************************************************************************************************************************
changed: [nxxxx.cisco.com]

TASK [Configure BGP_AF non defaults] ****************************************************************************************************************************************************************************************************************************
 [WARNING]: The BGP process 65535 didn't exist but the task just created it.

changed: [nxxxx.cisco.com]

PLAY RECAP ******************************************************************************************************************************************************************************************************************************************************
nxxxx.cisco.com          : ok=4    changed=3    unreachable=0    failed=0   

xxxxxxxxxxxx:~/agents-ci/ansible# ansible-playbook -i ~/hosts playbook/bgp_af.yaml 

PLAY [NXOS Sample Playbook] *************************************************************************************************************************************************************************************************************************************

TASK [Ensure BGP Is Enabled] ************************************************************************************************************************************************************************************************************************************
ok: [nxxxx.cisco.com]

TASK [Enable feature nv overlay] ********************************************************************************************************************************************************************************************************************************
ok: [nxxxx.cisco.com]

TASK [Enable nv overlay evpn] ***********************************************************************************************************************************************************************************************************************************
ok: [nxxxx.cisco.com]

TASK [Configure BGP_AF non defaults] ****************************************************************************************************************************************************************************************************************************
fatal: [nxxxx.cisco.com]: FAILED! => {"changed": false, "failed": true, "msg": "no client-to-client reflection\r\r\n                                  ^\r\n% Invalid command at '^' marker.\r\n\rnxxxx(config-router-vrf)# "}
	to retry, use: --limit @/root/agents-ci/ansible/playbook/bgp_af.retry

PLAY RECAP ******************************************************************************************************************************************************************************************************************************************************
nxxxx.cisco.com          : ok=3    changed=0    unreachable=0    failed=1
```



Discussion:
cc @jedelman8 @GGabriele @privateip @rcarrillocruz @mikewiebe @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_bgp_af.py b/lib/ansible/modules/network/nxos/nxos_bgp_af.py
--- a/lib/ansible/modules/network/nxos/nxos_bgp_af.py
+++ b/lib/ansible/modules/network/nxos/nxos_bgp_af.py
@@ -279,7 +279,6 @@
     'additional_paths_receive',
     'additional_paths_send',
     'advertise_l2vpn_evpn',
-    'client_to_client',
     'dampening_state',
     'default_information_originate',
     'suppress_inactive',
@@ -420,8 +419,15 @@ def get_value(arg, config, module):
         if has_tablemap:
             value = has_tablemap.group('value')
 
+    elif arg == 'client_to_client':
+        no_command_re = re.compile(r'^\s+no\s{0}\s*$'.format(command), re.M)
+        value = True
+
+        if no_command_re.search(config):
+            value = False
+
     elif arg in BOOL_PARAMS:
-        command_re = re.compile(r'\s+{0}\s*'.format(command), re.M)
+        command_re = re.compile(r'^\s+{0}\s*$'.format(command), re.M)
         value = False
 
         if command_re.search(config):
@@ -674,14 +680,12 @@ def state_present(module, existing, proposed, candidate):
         if module.params['vrf'] != 'default':
             parents.append('vrf {0}'.format(module.params['vrf']))
 
-        if len(commands) == 1:
-            candidate.add(commands, parents=parents)
-        elif len(commands) > 1:
-            parents.append('address-family {0} {1}'.format(module.params['afi'],
-                                                           module.params['safi']))
-            if addr_family_command in commands:
-                commands.remove(addr_family_command)
-            candidate.add(commands, parents=parents)
+        addr_family_command = "address-family {0} {1}".format(module.params['afi'],
+                                                              module.params['safi'])
+        parents.append(addr_family_command)
+        if addr_family_command in commands:
+            commands.remove(addr_family_command)
+        candidate.add(commands, parents=parents)
 
 
 def state_absent(module, candidate):


Test patch:
diff --git a/test/units/modules/network/nxos/fixtures/nxos_bgp/config.cfg b/test/units/modules/network/nxos/fixtures/nxos_bgp/config.cfg
new file mode 100644
--- /dev/null
+++ b/test/units/modules/network/nxos/fixtures/nxos_bgp/config.cfg
@@ -0,0 +1,11 @@
+feature bgp
+
+router bgp 65535
+  router-id 192.168.1.1
+  event-history cli size medium
+  event-history detail
+  vrf test2
+    address-family ipv4 unicast
+    timers bgp 1 10
+  neighbor 3.3.3.5
+    address-family ipv4 unicast
diff --git a/test/units/modules/network/nxos/test_nxos_bgp_af.py b/test/units/modules/network/nxos/test_nxos_bgp_af.py
--- a/test/units/modules/network/nxos/test_nxos_bgp_af.py
+++ b/test/units/modules/network/nxos/test_nxos_bgp_af.py
@@ -42,7 +42,7 @@ def tearDown(self):
         self.mock_get_config.stop()
 
     def load_fixtures(self, commands=None, device=''):
-        self.get_config.return_value = load_fixture('', 'nxos_bgp_config.cfg')
+        self.get_config.return_value = load_fixture('nxos_bgp', 'config.cfg')
         self.load_config.return_value = None
 
     def test_nxos_bgp_af(self):
@@ -89,3 +89,12 @@ def test_nxos_bgp_af_dampening_mix(self):
                              dampening_reuse_time=1900, dampening_max_suppress_time=10))
         result = self.execute_module(failed=True)
         self.assertEqual(result['msg'], 'dampening_routemap cannot be used with the dampening_half_time param')
+
+    def test_nxos_bgp_af_client(self):
+        set_module_args(dict(asn=65535, afi='ipv4', safi='unicast',
+                             client_to_client=False))
+        self.execute_module(
+            changed=True,
+            commands=['router bgp 65535', 'address-family ipv4 unicast',
+                      'no client-to-client reflection']
+        )