Lenovo cnos vrf (#54188)

* Adding module cnos_vrf to manage VRF Configurations.

* Update cnos_vrf.py

* Adding Functional Tests, Unit Tests and Bug Fixes.

* Fixing discrepancy in description against sample

* Review comments incorporated

* Review comments 2 Done

* Update basic.yaml

* Update test_cnos_vrf.py

* Review comments 3
This commit is contained in:
Anil Kumar Muraleedharan 2019-03-28 19:19:37 +05:30 committed by Sumit Jaiswal
parent c007422d05
commit 82d26c8c93
17 changed files with 889 additions and 7 deletions

View file

@ -0,0 +1,334 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
#
# Copyright (C) 2019 Lenovo.
# (c) 2017, Ansible by Red Hat, inc
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
# Module to work on management of local users on Lenovo CNOS Switches
# Lenovo Networking
#
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = """
---
module: cnos_vrf
version_added: "2.8"
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
short_description: Manage VRFs on Lenovo CNOS network devices
description:
- This module provides declarative management of VRFs
on Lenovo CNOS network devices.
notes:
- Tested against CNOS 10.9.1
options:
name:
description:
- Name of the VRF.
required: true
rd:
description:
- Route distinguisher of the VRF
interfaces:
description:
- Identifies the set of interfaces that
should be configured in the VRF. Interfaces must be routed
interfaces in order to be placed into a VRF. The name of interface
should be in expanded format and not abbreviated.
associated_interfaces:
description:
- This is a intent option and checks the operational state of the for
given vrf C(name) for associated interfaces. If the value in the
C(associated_interfaces) does not match with the operational state of
vrf interfaces on device it will result in failure.
aggregate:
description: List of VRFs contexts
purge:
description:
- Purge VRFs not defined in the I(aggregate) parameter.
default: no
type: bool
delay:
description:
- Time in seconds to wait before checking for the operational state on
remote device. This wait is applicable for operational state arguments.
default: 10
state:
description:
- State of the VRF configuration.
default: present
choices: ['present', 'absent']
"""
EXAMPLES = """
- name: Create vrf
cnos_vrf:
name: test
rd: 1:200
interfaces:
- Ethernet1/33
state: present
- name: Delete VRFs
cnos_vrf:
name: test
state: absent
- name: Create aggregate of VRFs with purge
cnos_vrf:
aggregate:
- { name: test4, rd: "1:204" }
- { name: test5, rd: "1:205" }
state: present
purge: yes
- name: Delete aggregate of VRFs
cnos_vrf:
aggregate:
- name: test2
- name: test3
- name: test4
- name: test5
state: absent
"""
RETURN = """
commands:
description: The list of configuration mode commands to send to the device
returned: always
type: list
sample:
- vrf context test
- rd 1:100
- interface Ethernet1/44
- vrf member test
"""
import re
import time
from copy import deepcopy
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.common.utils import remove_default_spec
from ansible.module_utils.network.cnos.cnos import load_config, run_commands
from ansible.module_utils.network.cnos.cnos import cnos_argument_spec, check_args
def search_obj_in_list(name, lst):
for o in lst:
if o['name'] == name:
return o
def map_obj_to_commands(updates, module):
commands = list()
want, have = updates
state = module.params['state']
purge = module.params['purge']
for w in want:
name = w['name']
rd = w['rd']
interfaces = w['interfaces']
obj_in_have = search_obj_in_list(name, have)
if name == 'default':
module.fail_json(msg='VRF context default is reserved')
elif len(name) > 63:
module.fail_json(msg='VRF name is too long')
if state == 'absent':
if name == 'management':
module.fail_json(msg='Management VRF context cannot be deleted')
if obj_in_have:
commands.append('no vrf context %s' % name)
elif state == 'present':
if not obj_in_have:
commands.append('vrf context %s' % name)
if rd is not None:
commands.append('rd %s' % rd)
if w['interfaces']:
for i in w['interfaces']:
commands.append('interface %s' % i)
commands.append('vrf member %s' % w['name'])
else:
if w['rd'] is not None and w['rd'] != obj_in_have['rd']:
commands.append('vrf context %s' % w['name'])
commands.append('rd %s' % w['rd'])
if w['interfaces']:
if not obj_in_have['interfaces']:
for i in w['interfaces']:
commands.append('interface %s' % i)
commands.append('vrf member %s' % w['name'])
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('interface %s' % i)
commands.append('vrf member %s' % w['name'])
if purge:
for h in have:
obj_in_want = search_obj_in_list(h['name'], want)
if not obj_in_want:
commands.append('no vrf context %s' % h['name'])
return commands
def map_config_to_obj(module):
objs = []
output = run_commands(module, {'command': 'show vrf'})
if output is None:
module.fail_json(msg='Could not fetch VRF details from device')
vrfText = output[0].strip()
vrfList = vrfText.split('VRF')
for vrfItem in vrfList:
if 'FIB ID' in vrfItem:
obj = dict()
list_of_words = vrfItem.split()
vrfName = list_of_words[0]
obj['name'] = vrfName[:-1]
obj['rd'] = list_of_words[list_of_words.index('RD') + 1]
start = False
obj['interfaces'] = []
for intName in list_of_words:
if 'Interfaces' in intName:
start = True
if start is True:
if '!' not in intName and 'Interfaces' not in intName:
obj['interfaces'].append(intName.strip().lower())
objs.append(obj)
return objs
def map_params_to_obj(module):
obj = []
aggregate = module.params.get('aggregate')
if aggregate:
for item in aggregate:
for key in item:
if item.get(key) is None:
item[key] = module.params[key]
if item.get('interfaces'):
item['interfaces'] = [intf.replace(" ", "").lower() for intf in item.get('interfaces') if intf]
if item.get('associated_interfaces'):
item['associated_interfaces'] = [intf.replace(" ", "").lower() for intf in item.get('associated_interfaces') if intf]
obj.append(item.copy())
else:
obj.append({
'name': module.params['name'],
'state': module.params['state'],
'rd': module.params['rd'],
'interfaces': [intf.replace(" ", "").lower() for intf in module.params['interfaces']] if module.params['interfaces'] else [],
'associated_interfaces': [intf.replace(" ", "").lower() for intf in
module.params['associated_interfaces']] if module.params['associated_interfaces'] else []
})
return obj
def check_declarative_intent_params(want, module, result):
have = None
is_delay = False
for w in want:
if w.get('associated_interfaces') is None:
continue
if result['changed'] and not is_delay:
time.sleep(module.params['delay'])
is_delay = True
if have is None:
have = map_config_to_obj(module)
for i in w['associated_interfaces']:
obj_in_have = search_obj_in_list(w['name'], have)
if obj_in_have:
interfaces = obj_in_have.get('interfaces')
if interfaces is not None and i not in interfaces:
module.fail_json(msg="Interface %s not configured on vrf %s" % (i, w['name']))
def main():
""" main entry point for module execution
"""
element_spec = dict(
name=dict(),
interfaces=dict(type='list'),
associated_interfaces=dict(type='list'),
delay=dict(default=10, type='int'),
rd=dict(),
state=dict(default='present', choices=['present', 'absent'])
)
aggregate_spec = deepcopy(element_spec)
# remove default in aggregate spec, to handle common arguments
remove_default_spec(aggregate_spec)
argument_spec = dict(
aggregate=dict(type='list', elements='dict', options=aggregate_spec),
purge=dict(default=False, type='bool')
)
argument_spec.update(element_spec)
required_one_of = [['name', 'aggregate']]
mutually_exclusive = [['name', 'aggregate']]
module = AnsibleModule(argument_spec=argument_spec,
required_one_of=required_one_of,
mutually_exclusive=mutually_exclusive,
supports_check_mode=True)
warnings = list()
check_args(module, warnings)
result = {'changed': False}
if warnings:
result['warnings'] = warnings
want = map_params_to_obj(module)
have = map_config_to_obj(module)
commands = map_obj_to_commands((want, have), module)
result['commands'] = commands
if commands:
if not module.check_mode:
load_config(module, commands)
result['changed'] = True
check_declarative_intent_params(want, module, result)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -10,5 +10,5 @@
# Following you should specify IP Addresses details
# Please change <username> and <password> with appropriate value for your switch.
[cnos_command]
[cnos_command_sample]
10.241.107.39 ansible_network_os=cnos ansible_ssh_user=<username> ansible_ssh_pass=<password> deviceType=g8272_cnos

View file

@ -10,5 +10,5 @@
# Following you should specify IP Addresses details
# Please change <username> and <password> with appropriate value for your switch.
[cnos_config]
[cnos_config_sample]
10.241.107.39 ansible_network_os=cnos ansible_ssh_user=<username> ansible_ssh_pass=<password> deviceType=g8272_cnos

View file

@ -10,5 +10,5 @@
# Following you should specify IP Addresses details
# Please change <username> and <password> with appropriate value for your switch.
[cnos_facts]
[cnos_facts_sample]
10.241.107.39 ansible_network_os=cnos ansible_ssh_user=<username> ansible_ssh_pass=<password> deviceType=g8272_cnos

View file

@ -0,0 +1,14 @@
# You have to paste this dummy information in /etc/ansible/hosts
# Notes:
# - Comments begin with the '#' character
# - Blank lines are ignored
# - Groups of hosts are delimited by [header] elements
# - You can enter hostnames or ip Addresses
# - A hostname/ip can be a member of multiple groups
#
# In the /etc/ansible/hosts file u have to enter [cnos_lldp_sample] tag
# Following you should specify IP Addresses details
# Please change <username> and <password> with appropriate value for your switch.
[cnos_lldp_sample]
10.241.107.39 ansible_network_os=cnos ansible_ssh_user=<username> ansible_ssh_pass=<password> deviceType=g8272_cnos

View file

@ -6,7 +6,7 @@
# - You can enter hostnames or ip Addresses
# - A hostname/ip can be a member of multiple groups
#
# In the /etc/ansible/hosts file u have to enter [cnos_portchannel_sample] tag
# In the /etc/ansible/hosts file u have to enter [cnos_logging_sample] tag
# Following you should specify IP Addresses details
# Please change <username> and <password> with appropriate value for your switch.

View file

@ -6,7 +6,7 @@
# - You can enter hostnames or ip Addresses
# - A hostname/ip can be a member of multiple groups
#
# In the /etc/ansible/hosts file u have to enter [cnos_facts_sample] tag
# In the /etc/ansible/hosts file u have to enter [cnos_showrun_sample] tag
# Following you should specify IP Addresses details
# Please change <username> and <password> with appropriate value for your switch.

View file

@ -10,5 +10,5 @@
# Following you should specify IP Addresses details
# Please change <username> and <password> with appropriate value for your switch.
[cnos_sample_sample]
[cnos_user_sample]
10.241.107.39 ansible_network_os=cnos ansible_ssh_user=admin ansible_ssh_pass=admin

View file

@ -10,5 +10,5 @@
# Following you should specify IP Addresses details
# Please change <username> and <password> with appropriate value for your switch.
[cnos]
[cnos_vlan_sample]
10.241.107.39 ansible_network_os=cnos ansible_ssh_user=<username> ansible_ssh_pass=<password>

View file

@ -0,0 +1,2 @@
# No Lenovo Switch simulator yet, so not enabled
unsupported

View file

@ -0,0 +1,14 @@
# You have to paste this dummy information in /etc/ansible/hosts
# Notes:
# - Comments begin with the '#' character
# - Blank lines are ignored
# - Groups of hosts are delimited by [header] elements
# - You can enter hostnames or ip Addresses
# - A hostname/ip can be a member of multiple groups
#
# In the /etc/ansible/hosts file u have to enter [cnos_vrf_sample] tag
# Following you should specify IP Addresses details
# Please change <username> and <password> with appropriate value for your switch.
[cnos_vrf_sample]
10.241.107.39 ansible_network_os=cnos ansible_ssh_user=<username> ansible_ssh_pass=<password> test_interface=ethernet1/33 test_interface2=ethernet1/44

View file

@ -0,0 +1,3 @@
---
testcase: "*"
test_items: []

View file

@ -0,0 +1,16 @@
---
- name: collect all cli test cases
find:
paths: "{{ role_path }}/tests/cli"
patterns: "{{ testcase }}.yaml"
register: test_cases
delegate_to: localhost
- 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"
with_items: "{{ test_items }}"
loop_control:
loop_var: test_case_to_run

View file

@ -0,0 +1,2 @@
---
- { include: cli.yaml, tags: ['cli'] }

View file

@ -0,0 +1,250 @@
---
- name: setup - remove vrf
cnos_vrf:
name: "{{ item }}"
state: absent
become: yes
with_items:
- test
- test1
- test2
- test3
- test4
- test5
- name: Create vrf
cnos_vrf:
name: test
rd: 1:200
state: present
become: yes
register: result
- assert:
that:
- "result.changed == true"
- "'vrf context test' in result.commands"
- "'rd 1:200' in result.commands"
- name: Create vrf again (idempotent)
cnos_vrf:
name: test
rd: 1:200
state: present
become: yes
register: result
- assert:
that:
- "result.changed == false"
- "result.commands | length == 0"
- name: Modify rd
cnos_vrf:
name: test
rd: 1:201
state: present
become: yes
register: result
- assert:
that:
- "result.changed == true"
- "'vrf context test' in result.commands"
- "'rd 1:201' in result.commands"
- name: Modify rd again (idempotent)
cnos_vrf:
name: test
rd: 1:201
state: present
become: yes
register: result
- assert:
that:
- "result.changed == false"
- "result.commands | length == 0"
- name: Add Ethernet1/33 to vrf and check interface assigned state
cnos_vrf:
name: test
rd: 1:201
state: present
interfaces:
- Ethernet1/33
associated_interfaces:
- Ethernet1/33
become: yes
register: result
- assert:
that:
- "result.changed == true"
- "'interface ethernet1/33' in result.commands"
- "'vrf member test' in result.commands"
- name: Add Ethernet1/33 to vrf again (idempotent)
cnos_vrf:
name: test
rd: 1:201
state: present
interfaces:
- ethernet 1/33 # interface name modified to test case insensitive and space scenario
become: yes
register: result
- assert:
that:
- "result.changed == false"
- "result.commands | length == 0"
- name: Add multiple interfaces to vrf
cnos_vrf:
name: test1
rd: 1:202
state: present
interfaces:
- loopback 1
- loopback 2
- loopback 3
- loopback 4
- loopback 5
- loopback 6
become: yes
register: result
- assert:
that:
- "result.changed == true"
- "'interface loopback1' in result.commands"
- "'vrf member test1' in result.commands"
- "'interface loopback2' in result.commands"
- "'vrf member test1' in result.commands"
- "'interface loopback3' in result.commands"
- "'vrf member test1' in result.commands"
- "'interface loopback4' in result.commands"
- "'vrf member test1' in result.commands"
- "'interface loopback5' in result.commands"
- "'vrf member test1' in result.commands"
- "'interface loopback6' in result.commands"
- "'vrf member test1' in result.commands"
- name: Add multiple interfaces to vrf (idempotent)
cnos_vrf:
name: test1
rd: 1:202
state: present
interfaces:
- loopback 1
- loopback 2
- loopback 3
- loopback 4
- loopback 5
- loopback 6
become: yes
register: result
- assert:
that:
- "result.changed == false"
- "result.commands | length == 0"
- name: setup - remove vrf
cnos_vrf:
name: "{{ item }}"
state: absent
become: yes
with_items:
- test1
- test2
- test3
- name: Create aggregate of VRFs
cnos_vrf:
aggregate:
- { name: test2, rd: "1:202" }
- { name: test3, rd: "1:203" }
state: present
register: result
- assert:
that:
- "result.changed == true"
- "'vrf context test2' in result.commands"
- "'rd 1:202' in result.commands"
- "'vrf context test3' in result.commands"
- "'rd 1:203' in result.commands"
- name: Create aggregate of VRFs again (idempotent)
cnos_vrf:
aggregate:
- { name: test2, rd: "1:202" }
- { name: test3, rd: "1:203" }
state: present
become: yes
register: result
- assert:
that:
- "result.changed == false"
- "result.commands | length == 0"
- name: Create aggregate of VRFs with purge
cnos_vrf:
aggregate:
- { name: test4, rd: "1:204" }
- { name: test5, rd: "1:205" }
state: present
purge: yes
become: yes
register: result
- assert:
that:
- "result.changed == true"
- "'vrf context test4' in result.commands"
- "'rd 1:204' in result.commands"
- "'vrf context test5' in result.commands"
- "'rd 1:205' in result.commands"
- "'no vrf context test' in result.commands"
- "'no vrf context test2' in result.commands"
- "'no vrf context test3' in result.commands"
- name: Delete VRFs
cnos_vrf:
name: test
state: absent
become: yes
- name: Delete VRFs again (idempotent)
cnos_vrf:
name: test
state: absent
become: yes
- name: Delete aggregate of VRFs
cnos_vrf:
aggregate:
- { name: test1 }
- { name: test2 }
- { name: test3 }
- { name: test4 }
- { name: test5 }
state: absent
become: yes
- name: Delete VRFs again (idempotent)
cnos_vrf:
aggregate:
- { name: test1 }
- { name: test2 }
- { name: test3 }
- { name: test4 }
- { name: test5 }
state: absent
become: yes
- assert:
that:
- "result.changed == true"

View file

@ -0,0 +1,176 @@
Maximum number of vrfs allowed: 65
VRF default, FIB ID 0
Router ID: 20.141.2.1 (automatic)
RD 0:0
Interfaces:
Vlan1
Vlan2
Vlan13
loopback0
Ethernet1/5
Ethernet1/6
Ethernet1/7
Ethernet1/8
Ethernet1/9
Ethernet1/11
Ethernet1/12
Ethernet1/13
Ethernet1/44
po1
po2
po3
po4
po6
po7
po8
po9
po10
po11
po12
po13
po14
po15
po16
po17
po18
po19
po21
po22
po23
po24
po25
po26
po27
po28
po29
po30
po31
po32
po33
po34
po35
po36
po37
po38
po39
po40
po41
po42
po43
po44
po45
po46
po47
po48
po49
po50
po51
po52
po53
po54
po55
po56
po57
po58
po59
po60
po61
po62
po63
po64
po65
po66
po67
po1001
po1002
po1003
po1004
Ethernet1/1
Ethernet1/2
Ethernet1/3
Ethernet1/4
Ethernet1/10
Ethernet1/14
Ethernet1/15
Ethernet1/16
Ethernet1/17
Ethernet1/18
Ethernet1/19
Ethernet1/20
Ethernet1/21
Ethernet1/22
Ethernet1/23
Ethernet1/24
Ethernet1/25
Ethernet1/26
Ethernet1/27
Ethernet1/28
Ethernet1/29
Ethernet1/30
Ethernet1/31
Ethernet1/32
Ethernet1/34
Ethernet1/35
Ethernet1/36
Ethernet1/37
Ethernet1/38
Ethernet1/39
Ethernet1/40
Ethernet1/41
Ethernet1/42
Ethernet1/43
Ethernet1/45
Ethernet1/46
Ethernet1/47
Ethernet1/48
Ethernet1/49
Ethernet1/50
Ethernet1/51
Ethernet1/52
Ethernet1/53
Ethernet1/54
!
VRF management, FIB ID 1
Router ID: 10.241.107.39 (automatic)
RD 0:0
Interfaces:
mgmt0
!
VRF test, FIB ID 2
Router ID is not set
RD 1:201
Interfaces:
Ethernet1/33
!
VRF test1, FIB ID 3
Router ID is not set
RD 1:202
Interfaces:
loopback1
loopback2
loopback3
loopback4
loopback5
loopback6
!
VRF test2, FIB ID 4
Router ID is not set
RD 0:0
Interfaces:
!
VRF test3, FIB ID 5
Router ID is not set
RD 1:203
Interfaces:
!
VRF test4, FIB ID 6
Router ID is not set
RD 1:204
Interfaces:
!
VRF test5, FIB ID 7
Router ID is not set
RD 1:205
Interfaces:
!

View file

@ -0,0 +1,71 @@
#
# (c) 2019 Lenovo.
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from units.compat.mock import patch
from ansible.modules.network.cnos import cnos_vrf
from units.modules.utils import set_module_args
from .cnos_module import TestCnosModule, load_fixture
class TestCnosVrfModule(TestCnosModule):
module = cnos_vrf
def setUp(self):
super(TestCnosVrfModule, self).setUp()
self.mock_load_config = patch('ansible.modules.network.cnos.cnos_vrf.load_config')
self.load_config = self.mock_load_config.start()
self.mock_run_commands = patch('ansible.modules.network.cnos.cnos_vrf.run_commands')
self.run_commands = self.mock_run_commands.start()
def tearDown(self):
super(TestCnosVrfModule, self).tearDown()
self.mock_load_config.stop()
self.mock_run_commands.stop()
def load_fixtures(self, commands=None):
config_file = 'cnos_vrf_config.cfg'
self.load_config.return_value = load_fixture(config_file)
self.run_commands.return_value = load_fixture(config_file)
def test_cnos_vrf_present(self):
set_module_args(dict(name='test1', state='present'))
self.execute_module(changed=True, commands=['vrf context test1'])
def test_cnos_vrf_present_management(self):
set_module_args(dict(name='management', state='present'))
self.execute_module(changed=True, commands=['vrf context management'])
def test_cnos_vrf_absent_management(self):
set_module_args(dict(name='management', state='absent'))
result = self.execute_module(failed=True)
self.assertEqual(result['msg'], 'Management VRF context cannot be deleted')
def test_cnos_vrf_absent_no_change(self):
set_module_args(dict(name='test1', state='absent'))
self.execute_module(changed=False, commands=[])
def test_cnos_vrf_default(self):
set_module_args(dict(name='default', state='present'))
result = self.execute_module(failed=True)
self.assertEqual(result['msg'], 'VRF context default is reserved')