From 5a6f3ebed12dbde59f657f023fb03d39266eee7d Mon Sep 17 00:00:00 2001
From: Ricardo Carrillo Cruz <ricardo.carrillo.cruz@gmail.com>
Date: Thu, 17 Aug 2017 09:45:50 +0200
Subject: [PATCH] WIP Implement declarative intent arguments on eos_vlan
 (#28270)

Implement declarative intent arguments on eos_vlan
---
 lib/ansible/modules/network/eos/eos_vlan.py   | 199 +++++++++++-----
 .../targets/eos_vlan/tests/cli/basic.yaml     | 222 +++++++++++++++++-
 2 files changed, 353 insertions(+), 68 deletions(-)

diff --git a/lib/ansible/modules/network/eos/eos_vlan.py b/lib/ansible/modules/network/eos/eos_vlan.py
index f98790e3379..9e4b75ad7e1 100644
--- a/lib/ansible/modules/network/eos/eos_vlan.py
+++ b/lib/ansible/modules/network/eos/eos_vlan.py
@@ -43,8 +43,11 @@ options:
     required: true
   interfaces:
     description:
-      - List of interfaces to check the VLAN has been
-        configured correctly.
+      - List of interfaces that should be associated to the VLAN.
+  delay:
+    description:
+      - Delay the play should wait to check for declaratie intent params values.
+    default: 10
   aggregate:
     description: List of VLANs definitions
   purge:
@@ -76,88 +79,169 @@ from ansible.module_utils.eos import eos_argument_spec, check_args
 from ansible.module_utils.six import iteritems
 
 import re
+import time
+
+
+def search_obj_in_list(vlan_id, lst):
+    for o in lst:
+        if o['vlan_id'] == vlan_id:
+            return o
 
 
 def map_obj_to_commands(updates, module):
     commands = list()
     want, have = updates
-    state = module.params['state']
+    purge = module.params['purge']
 
-    if state == 'absent':
-        if have:
-            commands.append('no vlan %s' % want['vlan_id'])
-    elif state == 'present':
-        if not have or want['name'] != have['name']:
-            commands.append('vlan %s' % want['vlan_id'])
-            commands.append('name %s' % want['name'])
-    else:
-        if not have:
-            commands.append('vlan %s' % want['vlan_id'])
-            commands.append('name %s' % want['name'])
-            commands.append('state %s' % want['state'])
-        elif have['name'] != want['name'] or have['state'] != want['state']:
-            commands.append('vlan %s' % want['vlan_id'])
+    for w in want:
+        vlan_id = w['vlan_id']
+        name = w['name']
+        state = w['state']
+        interfaces = w['interfaces']
 
-            if have['name'] != want['name']:
-                commands.append('name %s' % want['name'])
+        obj_in_have = search_obj_in_list(vlan_id, have)
 
-            if have['state'] != want['state']:
-                commands.append('state %s' % want['state'])
+        if state == 'absent':
+            if obj_in_have:
+                commands.append('no vlan %s' % w['vlan_id'])
+        elif state == 'present':
+            if not obj_in_have:
+                commands.append('vlan %s' % w['vlan_id'])
+                commands.append('name %s' % w['name'])
+
+                if w['interfaces']:
+                    for i in w['interfaces']:
+                        commands.append('interface %s' % i)
+                        commands.append('switchport access vlan %s' % w['vlan_id'])
+            else:
+                if w['name'] and w['name'] != obj_in_have['name']:
+                    commands.append('vlan %s' % w['vlan_id'])
+                    commands.append('name %s' % w['name'])
+
+                if w['interfaces']:
+                    if not obj_in_have['interfaces']:
+                        for i in w['interfaces']:
+                            commands.append('vlan %s' % w['vlan_id'])
+                            commands.append('interface %s' % i)
+                            commands.append('switchport access vlan %s' % w['vlan_id'])
+                    elif set(w['interfaces']) != obj_in_have['interfaces']:
+                        missing_interfaces = list(set(w['interfaces']) - set(obj_in_have['interfaces']))
+                        for i in missing_interfaces:
+                            commands.append('vlan %s' % w['vlan_id'])
+                            commands.append('interface %s' % i)
+                            commands.append('switchport access vlan %s' % w['vlan_id'])
+
+                        superfluous_interfaces = list(set(obj_in_have['interfaces']) - set(w['interfaces']))
+                        for i in superfluous_interfaces:
+                            commands.append('vlan %s' % w['vlan_id'])
+                            commands.append('interface %s' % i)
+                            commands.append('no switchport access vlan %s' % w['vlan_id'])
+        else:
+            if not obj_in_have:
+                commands.append('vlan %s' % w['vlan_id'])
+                commands.append('name %s' % w['name'])
+                commands.append('state %s' % w['state'])
+            elif obj_in_have['name'] != w['name'] or obj_in_have['state'] != w['state']:
+                commands.append('vlan %s' % w['vlan_id'])
+
+                if obj_in_have['name'] != w['name']:
+                    commands.append('name %s' % w['name'])
+
+                if obj_in_have['state'] != w['state']:
+                    commands.append('state %s' % w['state'])
+
+    if purge:
+        for h in have:
+            obj_in_want = search_obj_in_list(h['vlan_id'], want)
+            if not obj_in_want and h['vlan_id'] != '1':
+                commands.append('no vlan %s' % h['vlan_id'])
 
     return commands
 
 
 def map_config_to_obj(module):
-    obj = {}
+    objs = []
     output = run_commands(module, ['show vlan'])
+    lines = output[0].strip().splitlines()[2:]
 
-    if isinstance(output[0], str):
-        for l in output[0].strip().splitlines()[2:]:
-            split_line = l.split()
-            vlan_id = split_line[0]
-            name = split_line[1]
-            status = split_line[2]
+    for l in lines:
+        splitted_line = re.split(r'\s{2,}', l.strip())
+        obj = {}
+        obj['vlan_id'] = splitted_line[0]
+        obj['name'] = splitted_line[1]
+        obj['state'] = splitted_line[2]
 
-            if vlan_id == str(module.params['vlan_id']):
-                obj['vlan_id'] = vlan_id
-                obj['name'] = name
-                obj['state'] = status
-                if obj['state'] == 'suspended':
-                    obj['state'] = 'suspend'
-                break
+        if obj['state'] == 'suspended':
+            obj['state'] = 'suspend'
+
+        obj['interfaces'] = []
+        if len(splitted_line) > 3:
+
+            for i in splitted_line[3].split(','):
+                obj['interfaces'].append(i.strip().replace('Et', 'Ethernet'))
+
+        objs.append(obj)
+
+    return objs
+
+
+def map_params_to_obj(module):
+    obj = []
+
+    if 'aggregate' in module.params and module.params['aggregate']:
+        for v in module.params['aggregate']:
+            d = v.copy()
+
+            d['vlan_id'] = str(d['vlan_id'])
+
+            if 'state' not in d:
+                d['state'] = module.params['state']
+
+            if 'name' not in d:
+                d['name'] = None
+
+            if 'interfaces' not in d:
+                d['interfaces'] = []
+
+            obj.append(d)
     else:
-        for k, v in iteritems(output[0]['vlans']):
-            vlan_id = k
-            name = v['name']
-            status = v['status']
+        vlan_id = str(module.params['vlan_id'])
+        name = module.params['name']
+        state = module.params['state']
+        interfaces = module.params['interfaces']
 
-            if vlan_id == str(module.params['vlan_id']):
-                obj['vlan_id'] = vlan_id
-                obj['name'] = name
-                obj['state'] = status
-                if obj['state'] == 'suspended':
-                    obj['state'] = 'suspend'
-                break
+        obj.append({
+            'vlan_id': vlan_id,
+            'name': name,
+            'state': state,
+            'interfaces': interfaces
+        })
 
     return obj
 
 
-def map_params_to_obj(module):
-    return {
-        'vlan_id': str(module.params['vlan_id']),
-        'name': module.params['name'],
-        'state': module.params['state']
-    }
+def check_declarative_intent_params(want, module):
+    if module.params['interfaces']:
+        time.sleep(module.params['delay'])
+        have = map_config_to_obj(module)
+
+        for w in want:
+            for i in w['interfaces']:
+                obj_in_have = search_obj_in_list(w['vlan_id'], have)
+
+                if obj_in_have and 'interfaces' in obj_in_have and i not in obj_in_have['interfaces']:
+                    module.fail_json(msg="Interface %s not configured on vlan %s" % (i, w['vlan_id']))
 
 
 def main():
     """ main entry point for module execution
     """
     argument_spec = dict(
-        vlan_id=dict(required=True, type='int'),
+        vlan_id=dict(type='int'),
         name=dict(),
-        interfaces=dict(),
-        aggregate=dict(),
+        interfaces=dict(type='list'),
+        delay=dict(default=10, type='int'),
+        aggregate=dict(type='list'),
         purge=dict(default=False, type='bool'),
         state=dict(default='present',
                    choices=['present', 'absent', 'active', 'suspend'])
@@ -165,6 +249,8 @@ def main():
 
     argument_spec.update(eos_argument_spec)
 
+    required_one_of = [['vlan_id', 'aggregate']]
+    mutually_exclusive = [['vlan_id', 'aggregate']]
     module = AnsibleModule(argument_spec=argument_spec,
                            supports_check_mode=True)
 
@@ -190,6 +276,9 @@ def main():
         result['session_name'] = response.get('session')
         result['changed'] = True
 
+    if result['changed']:
+        check_declarative_intent_params(want, module)
+
     module.exit_json(**result)
 
 if __name__ == '__main__':
diff --git a/test/integration/targets/eos_vlan/tests/cli/basic.yaml b/test/integration/targets/eos_vlan/tests/cli/basic.yaml
index fb44fd5495d..f22d57715ec 100644
--- a/test/integration/targets/eos_vlan/tests/cli/basic.yaml
+++ b/test/integration/targets/eos_vlan/tests/cli/basic.yaml
@@ -1,37 +1,53 @@
 ---
 
-- name: setup - remove vlan
-  eos_vlan:
-    vlan_id: 4000
-    name: test-vlan
-    state: absent
+- name: setup - remove vlans used in test
+  eos_config:
+    lines:
+      - no vlan 4000
+      - no vlan 4001
+      - no vlan 4002
+    authorize: yes
+    provider: "{{ cli }}"
+
+- name: setup - remove switchport settings on interface Ethernet1 used in test
+  eos_config:
+    lines:
+      - switchport
+      - no switchport access vlan 4000
+    parents: interface Ethernet1
+    authorize: yes
+    provider: "{{ cli }}"
+
+- name: setup - remove switchport settings on interface Ethernet2 used in test
+  eos_config:
+    lines:
+      - switchport
+      - no switchport access vlan 4000
+    parents: interface Ethernet2
     authorize: yes
     provider: "{{ cli }}"
 
 - name: Create vlan
   eos_vlan:
     vlan_id: 4000
-    name: test-vlan
+    name: vlan-4000
     state: present
     authorize: yes
     provider: "{{ cli }}"
   register: result
 
-- debug:
-    msg: "{{ result }}"
-
 - assert:
     that:
       - "result.changed == true"
       - "'vlan 4000' in result.commands"
-      - "'name test-vlan' in result.commands"
+      - "'name vlan-4000' in result.commands"
       # Ensure sessions contains epoc. Will fail after 18th May 2033
       - "'ansible_1' in result.session_name"
 
 - name: Create vlan again (idempotent)
   eos_vlan:
     vlan_id: 4000
-    name: test-vlan
+    name: vlan-4000
     state: present
     authorize: yes
     provider: "{{ cli }}"
@@ -47,7 +63,7 @@
 - name: Change vlan name and state
   eos_vlan:
     vlan_id: 4000
-    name: test-vlan2
+    name: vlan-4000-new
     state: suspend
     authorize: yes
     provider: "{{ cli }}"
@@ -57,12 +73,192 @@
     that:
       - "result.changed == true"
       - "'vlan 4000' in result.commands"
-      - "'name test-vlan2' in result.commands"
+      - "'name vlan-4000-new' in result.commands"
       - "'state suspend' in result.commands"
       # Ensure sessions contains epoc. Will fail after 18th May 2033
       - "'ansible_1' in result.session_name"
 
+- name: Change vlan name and state again (idempotent)
+  eos_vlan:
+    vlan_id: 4000
+    name: vlan-4000-new
+    state: suspend
+    authorize: yes
+    provider: "{{ cli }}"
+  register: result
 
+- assert:
+    that:
+      - "result.changed == false"
+      - "result.commands | length == 0"
+      # Ensure sessions contains epoc. Will fail after 18th May 2033
+      - "result.session_name is not defined"
+
+- name: Unsuspend vlan
+  eos_vlan:
+    vlan_id: 4000
+    state: active
+    authorize: yes
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - "result.changed == true"
+      - "'vlan 4000' in result.commands"
+      - "'state active' in result.commands"
+      # Ensure sessions contains epoc. Will fail after 18th May 2033
+      - "'ansible_1' in result.session_name"
+
+- name: Add interfaces to vlan
+  eos_vlan:
+    vlan_id: 4000
+    state: present
+    interfaces:
+      - Ethernet1
+      - Ethernet2
+    authorize: yes
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - "result.changed == true"
+      - "'vlan 4000' in result.commands"
+      - "'interface Ethernet1' in result.commands"
+      - "'switchport access vlan 4000' in result.commands"
+      - "'interface Ethernet2' in result.commands"
+      - "'switchport access vlan 4000' in result.commands"
+      # Ensure sessions contains epoc. Will fail after 18th May 2033
+      - "'ansible_1' in result.session_name"
+
+- name: Add interfaces to vlan again (idempotent)
+  eos_vlan:
+    vlan_id: 4000
+    state: present
+    interfaces:
+      - Ethernet1
+      - Ethernet2
+    authorize: yes
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - "result.changed == false"
+      - "result.commands | length == 0"
+      # Ensure sessions contains epoc. Will fail after 18th May 2033
+      - "result.session_name is not defined"
+
+- name: Remove interface from vlan
+  eos_vlan:
+    vlan_id: 4000
+    state: present
+    interfaces:
+      - Ethernet1
+    authorize: yes
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - "result.changed == true"
+      - "'vlan 4000' in result.commands"
+      - "'interface Ethernet2' in result.commands"
+      - "'no switchport access vlan 4000' in result.commands"
+      # Ensure sessions contains epoc. Will fail after 18th May 2033
+      - "'ansible_1' in result.session_name"
+
+- name: Remove interface from vlan again (idempotent)
+  eos_vlan:
+    vlan_id: 4000
+    state: present
+    interfaces:
+      - Ethernet1
+    authorize: yes
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - "result.changed == false"
+      - "result.commands | length == 0"
+      # Ensure sessions contains epoc. Will fail after 18th May 2033
+      - "result.session_name is not defined"
+
+- name: Create aggregate of vlans
+  eos_vlan:
+    aggregate:
+      - {vlan_id: 4000, state: absent}
+      - {vlan_id: 4001, name: vlan-4001}
+    state: present
+    authorize: yes
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - "result.changed == true"
+      - "'no vlan 4000' in result.commands"
+      - "'vlan 4001' in result.commands"
+      - "'name vlan-4001' in result.commands"
+      # Ensure sessions contains epoc. Will fail after 18th May 2033
+      - "'ansible_1' in result.session_name"
+
+- name: Create aggregate of vlans again (idempotent)
+  eos_vlan:
+    aggregate:
+      - {vlan_id: 4000, state: absent}
+      - {vlan_id: 4001, name: vlan-4001}
+    state: present
+    authorize: yes
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - "result.changed == false"
+      - "result.commands | length == 0"
+      # Ensure sessions contains epoc. Will fail after 18th May 2033
+      - "result.session_name is not defined"
+
+- name: Create vlan with purge
+  eos_vlan:
+    aggregate:
+      - {vlan_id: 4002, name: vlan-4002}
+    name: vlan-4002
+    state: present
+    purge: yes
+    authorize: yes
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - "result.changed == true"
+      - "'no vlan 4001' in result.commands"
+      - "'vlan 4002' in result.commands"
+      - "'name vlan-4002' in result.commands"
+      # Ensure sessions contains epoc. Will fail after 18th May 2033
+      - "'ansible_1' in result.session_name"
+
+- name: Create vlan with purge
+  eos_vlan:
+    aggregate:
+      - {vlan_id: 4002, name: vlan-4002}
+    name: vlan-4002
+    state: present
+    purge: yes
+    authorize: yes
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - "result.changed == false"
+      - "result.commands | length == 0"
+      # Ensure sessions contains epoc. Will fail after 18th May 2033
+      - "result.session_name is not defined"
 # FIXME add in tests for everything defined in docs
 # FIXME Test state:absent + test:
 # FIXME Without powers ensure "privileged mode required"