Repository: ansible/ansible

Issue:
Arista EOS module doesn't properly parse vrfs
<!---
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
eos_vrf
eos_eapi

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
 19179 1505254263.46765: starting run
ansible 2.5.0
  config file = /home/user/ansible-network/ansible.cfg
  configured module search path = [u'/home/user/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.5 (default, Nov  6 2016, 00:28:07) [GCC 4.8.5 20150623 (Red Hat 4.8.5-11)]
```

##### CONFIGURATION
ANSIBLE_DEBUG=True
ANSIBLE_LOG_PATH=~/.ansible.log

##### OS / ENVIRONMENT
CentOS 7, I've tried ansible 2.5.0 and v2.3.2.0, EOS 4.16.7M

##### SUMMARY
When trying to specify a vrf in the eos_eapi task, I get the error below:
```
{"msg": "vrf `vrf-mgmt` is not configured on the system", "failed": true,
```
Except the vrf does exist and shows when I run show vrf manually which is what I see is run by the module https://github.com/ansible/ansible/blob/d8b1cb9a634d0e828a7be39d4019a6215410e643/lib/ansible/modules/network/eos/eos_eapi.py#L210

Manual show vrf output:
```
#show vrf
Maximum number of vrfs allowed: 15
  Vrf          RD           Protocols     State            Interfaces
------------ ------------ ------------- ------------------ --------------------
  vrf-ext      65001:1      ipv4,ipv6     v4:routing,      Ethernet45,
                                          v6:no routing    Ethernet46, Vlan100,
                                                           Vlan1213, Vlan136,
                                                           Vlan137, Vlan138,
                                                           Vlan139, Vlan172,
                                                           Vlan174, Vlan200,
                                                           Vlan201, Vlan202,
                                                           Vlan203, Vlan204,
                                                           Vlan205, Vlan206,
                                                           Vlan207, Vlan208,
                                                           Vlan221, Vlan90,
                                                           Vlan96
  vrf-mgmt     65001:10     ipv4,ipv6     v4:routing,      Management1
                                          v6:no routing

```

I imagine something in the "show vrf" output from Arista changed?
##### 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
---
- name: Arista
  hosts: all
  connection: local
  gather_facts: no

  vars_prompt:
    - name: "username"
      prompt: "username? "
      private: no

    - name: "password"
      prompt: "Password? "
      private: yes

  tasks:
    - name: Enable eAPI with no HTTP, HTTPS at port 443
      eos_eapi:
       host: "{{ inventory_hostname }}"
       vrf: "vrf-mgmt"
       state: started
       http: false
       https: true
       https_port: 443
       local_http: false
       socket: false
       provider: "{{ provider }}"
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
I expected it to detect my vrf exists which it does and run the task properly.

##### ACTUAL RESULTS
It returns the above error saying my vrf doesn't exist. DEBUG information shows the same error


<!--- Paste verbatim command output between quotes below -->
```
ansible-playbook -C --limit DEVICE1 -i inventories/site/hosts eapi.yml
fatal: [DEVICE1]: FAILED! => {"changed": false, "failed": true, "msg": "vrf `vrf-ext` is not configured on the system"}
```

##### MORE INFORMATION
The code to parse the "show vrf' output to json in eos_vrf parses the vrf names correctly but it has issues parsing the routing domain and interfaces when there is a large number.

https://github.com/ansible/ansible/blob/340f52ae9646f605310345f875df7985ee978019/lib/ansible/modules/network/eos/eos_vrf.py#L176

```json
[
    {
        "rd": "65001:1",
        "interfaces": [
            "Ethernet45",
            ""
        ],
        "name": "vrf-ext"
    },
    {
        "rd": "Ethernet46, Vlan100,",
        "interfaces": null,
        "name": "v6:no routing"
    },
    {
        "rd": "65001:10",
        "interfaces": [
            "Management1"
        ],
        "name": "vrf-mgmt"
    }
]
```

The code in eos_eapi to parse the vrfs is very different to eos_vrf and something with the regex in configured_vrfs under the validate_vrf function is very wrong.  Debugging shows the only valid VRFs being detected are: ['Vrf', 'Vlan96', 'default']

Testing the regex against my output shows thats is indeed all thats being matched: https://regexr.com/3go3j

I imagine something in the "show vrf" output from Arista maybe changed? I am on version 4.16.7M


Discussion:
cc @privateip @rcarrillocruz @trishnaguha
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@Gelob
The eos_eapi doc for vrf argument mentions.
```
  vrf:
    description:
      - The C(vrf) argument will configure eAPI to listen for connections
        in the specified VRF.  By default, eAPI transports will listen
        for connections in the global table.  This value requires the
        VRF to already be created otherwise the task will fail.
```
So I think this is expected behavior. Before running `eos_epai` you need to create a `vrf` either by using `eos_vrf` or `eos_config`.
@ganeshrn, I don't see how its expected behavior? I assume that wording means I have to have the VRF created on my device, which I do. That would seem in line because the vrf validation is running 'show vrf' against my device. It isn't validating any other modules or tasks in my playbook.

Also even if I didn't have the VRF created the parsing of the 'show vrf' output is incorrect.

Notice in my output my vrf names are "vrf-ext" and "vrf-mgmt". The module parses incorrectly and thinks they are "Vrf" "Vlan96" and "default" (which it adds automatically)
@ganeshrn @Gelob this is a bug due to a command output change from 4.15 to 4.16.  We can work around the bug by doing a version check in the module code to account for the change in output

Resolving patch:
diff --git a/lib/ansible/modules/network/eos/eos_eapi.py b/lib/ansible/modules/network/eos/eos_eapi.py
--- a/lib/ansible/modules/network/eos/eos_eapi.py
+++ b/lib/ansible/modules/network/eos/eos_eapi.py
@@ -213,7 +213,15 @@ def validate_local_http_port(value, module):
 
 def validate_vrf(value, module):
     out = run_commands(module, ['show vrf'])
-    configured_vrfs = re.findall(r'^\s+(\w+)(?=\s)', out[0], re.M)
+    configured_vrfs = []
+    lines = out[0].strip().splitlines()[3:]
+    for l in lines:
+        if not l:
+            continue
+        splitted_line = re.split(r'\s{2,}', l.strip())
+        if len(splitted_line) > 2:
+            configured_vrfs.append(splitted_line[0])
+
     configured_vrfs.append('default')
     if value not in configured_vrfs:
         module.fail_json(msg='vrf `%s` is not configured on the system' % value)
diff --git a/lib/ansible/modules/network/eos/eos_vrf.py b/lib/ansible/modules/network/eos/eos_vrf.py
--- a/lib/ansible/modules/network/eos/eos_vrf.py
+++ b/lib/ansible/modules/network/eos/eos_vrf.py
@@ -184,29 +184,44 @@ def map_obj_to_commands(updates, module):
 def map_config_to_obj(module):
     objs = []
     output = run_commands(module, ['show vrf'])
-    lines = output[0].strip().splitlines()[2:]
 
-    for l in lines:
-        if not l:
+    lines = output[0].strip().splitlines()[3:]
+
+    out_len = len(lines)
+    index = 0
+    while out_len > index:
+        line = lines[index]
+        if not line:
             continue
 
-        splitted_line = re.split(r'\s{2,}', l.strip())
+        splitted_line = re.split(r'\s{2,}', line.strip())
 
         if len(splitted_line) == 1:
+            index += 1
             continue
         else:
-            obj = {}
+            obj = dict()
             obj['name'] = splitted_line[0]
             obj['rd'] = splitted_line[1]
             obj['interfaces'] = []
 
             if len(splitted_line) > 4:
                 obj['interfaces'] = []
-
-                for i in splitted_line[4].split(','):
+                interfaces = splitted_line[4]
+                if interfaces.endswith(','):
+                    while interfaces.endswith(','):
+                        # gather all comma separated interfaces
+                        if out_len <= index:
+                            break
+                        index += 1
+                        line = lines[index]
+                        vrf_line = re.split(r'\s{2,}', line.strip())
+                        interfaces += vrf_line[-1]
+
+                for i in interfaces.split(','):
                     obj['interfaces'].append(i.strip().lower())
-
-            objs.append(obj)
+        index += 1
+        objs.append(obj)
 
     return objs
 
@@ -308,5 +323,6 @@ def main():
 
     module.exit_json(**result)
 
+
 if __name__ == '__main__':
     main()


Test patch:
diff --git a/test/integration/targets/eos_vrf/tests/cli/basic.yaml b/test/integration/targets/eos_vrf/tests/cli/basic.yaml
--- a/test/integration/targets/eos_vrf/tests/cli/basic.yaml
+++ b/test/integration/targets/eos_vrf/tests/cli/basic.yaml
@@ -2,27 +2,18 @@
 
 - name: setup - remove vrf
   eos_vrf:
-    name: test
-    state: absent
-    authorize: yes
-    provider: "{{ cli }}"
-  become: yes
-
-- name: setup - remove vrf
-  eos_vrf:
-    name: test2
-    state: absent
-    authorize: yes
-    provider: "{{ cli }}"
-  become: yes
-
-- name: setup - remove vrf
-  eos_vrf:
-    name: test3
+    name: "{{ item }}"
     state: absent
     authorize: yes
     provider: "{{ cli }}"
   become: yes
+  with_items:
+  - test
+  - test1
+  - test2
+  - test3
+  - test4
+  - test5
 
 - name: Create vrf
   eos_vrf:
@@ -133,6 +124,66 @@
       # Ensure sessions contains epoc. Will fail after 18th May 2033
       - "'session_name' not in result.commands"
 
+- name: Add multiple interfaces to vrf
+  eos_vrf:
+    name: test1
+    rd: 1:202
+    state: present
+    authorize: yes
+    interfaces:
+      - loopback10
+      - loopback11
+      - loopback12
+      - loopback13
+      - loopback14
+      - loopback15
+      - loopback1000
+    provider: "{{ cli }}"
+  become: yes
+  register: result
+
+- assert:
+    that:
+      - "result.changed == true"
+      - "'interface loopback10' in result.commands"
+      - "'vrf forwarding test1' in result.commands"
+      - "'interface loopback11' in result.commands"
+      - "'vrf forwarding test1' in result.commands"
+      - "'interface loopback12' in result.commands"
+      - "'vrf forwarding test1' in result.commands"
+      - "'interface loopback13' in result.commands"
+      - "'vrf forwarding test1' in result.commands"
+      - "'interface loopback14' in result.commands"
+      - "'vrf forwarding test1' in result.commands"
+      - "'interface loopback15' in result.commands"
+      - "'vrf forwarding test1' in result.commands"
+      - "'interface loopback1000' in result.commands"
+      - "'vrf forwarding test1' in result.commands"
+      - "'ansible_1' in result.session_name"
+
+- name: Add multiple interfaces to vrf (idempotent)
+  eos_vrf:
+    name: test1
+    rd: 1:202
+    state: present
+    authorize: yes
+    interfaces:
+      - loopback10
+      - loopback11
+      - loopback12
+      - loopback13
+      - loopback14
+      - loopback15
+      - loopback1000
+    provider: "{{ cli }}"
+  become: yes
+  register: result
+
+- assert:
+    that:
+      - "result.changed == false"
+      - "result.commands | length == 0"
+
 - name: Create aggregate of VRFs
   eos_vrf:
     aggregate:
@@ -216,6 +267,7 @@
 - name: Delete aggregate of VRFs
   eos_vrf:
     aggregate:
+      - { name: test1 }
       - { name: test2 }
       - { name: test3 }
       - { name: test4 }
@@ -228,6 +280,7 @@
 - name: Delete VRFs again (idempotent)
   eos_vrf:
     aggregate:
+      - { name: test1 }
       - { name: test2 }
       - { name: test3 }
       - { name: test4 }