Convert nxos_vrf to DI module (#34274)

* Convert nxos_vrf to DI module

Signed-off-by: Trishna Guha <trishnaguha17@gmail.com>

* Add purge example and improve logic

Signed-off-by: Trishna Guha <trishnaguha17@gmail.com>

* revert version_added for rd param

Signed-off-by: Trishna Guha <trishnaguha17@gmail.com>

* update test to use network_cli

Signed-off-by: Trishna Guha <trishnaguha17@gmail.com>
This commit is contained in:
Trishna Guha 2018-01-08 10:10:34 +05:30 committed by GitHub
parent 7f59f7d80e
commit 5e6b2495c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 500 additions and 132 deletions

View file

@ -28,10 +28,12 @@ extends_documentation_fragment: nxos
version_added: "2.1"
short_description: Manages global VRF configuration.
description:
- Manages global VRF configuration.
- This module provides declarative management of VRFs
on CISCO NXOS network devices.
author:
- Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele)
- Trishna Guha (@trishnaguha)
notes:
- Tested against NXOSv 7.3.(0)D1(1) on VIRL
- Cisco NX-OS creates the default VRF by itself. Therefore,
@ -43,10 +45,11 @@ notes:
a VRF named NTC, but running it again with C(vrf=ntc) will not cause
a configuration change.
options:
vrf:
name:
description:
- Name of VRF to be managed.
required: true
aliases: [vrf]
admin_state:
description:
- Administrative state of the VRF.
@ -60,7 +63,7 @@ options:
required: false
default: null
version_added: "2.2"
route_distinguisher:
rd:
description:
- VPN Route Distinguisher (RD). Valid values are a string in
one of the route-distinguisher formats (ASN2:NN, ASN4:NN, or
@ -68,6 +71,19 @@ options:
required: false
default: null
version_added: "2.2"
interfaces:
description:
- List of interfaces to check the VRF has been
configured correctly.
version_added: 2.5
aggregate:
description: List of VRFs definitions.
version_added: 2.5
purge:
description:
- Purge VRFs not defined in the I(aggregate) parameter.
default: no
version_added: 2.5
state:
description:
- Manages desired state of the resource.
@ -84,8 +100,48 @@ options:
EXAMPLES = '''
- name: Ensure ntc VRF exists on switch
nxos_vrf:
vrf: ntc
name: ntc
description: testing
state: present
- name: Aggregate definition of VRFs
nxos_vrf:
aggregate:
- { name: test1, description: Testing, admin_state: down }
- { name: test2, interfaces: Ethernet1/2 }
- name: Aggregate definitions of VRFs with Purge
nxos_vrf:
aggregate:
- { name: ntc1, description: purge test1 }
- { name: ntc2, description: purge test2 }
state: present
purge: yes
- name: Delete VRFs exist on switch
nxos_vrf:
aggregate:
- { name: ntc1 }
- { name: ntc2 }
state: absent
- name: Assign interfaces to VRF declaratively
nxos_vrf:
name: test1
interfaces:
- Ethernet2/3
- Ethernet2/5
- name: Ensure VRF is tagged with interface Ethernet2/5 only (Removes from Ethernet2/3)
nxos_vrf:
name: test1
interfaces:
- Ethernet2/5
- name: Delete VRF
nxos_vrf:
name: ntc
state: absent
'''
RETURN = '''
@ -93,13 +149,29 @@ commands:
description: commands sent to the device
returned: always
type: list
sample: ["vrf context ntc", "shutdown"]
sample:
- vrf context ntc
- no shutdown
- interface Ethernet1/2
- no switchport
- vrf member test2
'''
import re
from ansible.module_utils.network.nxos.nxos import load_config, run_commands
from ansible.module_utils.network.nxos.nxos import nxos_argument_spec, check_args
import re
import time
from copy import deepcopy
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.nxos.nxos import load_config, run_commands
from ansible.module_utils.network.nxos.nxos import nxos_argument_spec
from ansible.module_utils.network.common.utils import remove_default_spec
def search_obj_in_list(name, lst):
for o in lst:
if o['name'] == name:
return o
def execute_show_command(command, module):
@ -115,58 +187,151 @@ def execute_show_command(command, module):
return body
def apply_key_map(key_map, table):
new_dict = {}
for key in table:
new_key = key_map.get(key)
if new_key:
new_dict[new_key] = str(table.get(key))
return new_dict
def get_commands_to_config_vrf(delta, vrf):
commands = []
for param, value in delta.items():
command = ''
if param == 'description':
command = 'description {0}'.format(value)
elif param == 'admin_state':
if value.lower() == 'up':
command = 'no shutdown'
elif value.lower() == 'down':
command = 'shutdown'
elif param == 'rd':
command = 'rd {0}'.format(value)
elif param == 'vni':
command = 'vni {0}'.format(value)
if command:
commands.append(command)
if commands:
commands.insert(0, 'vrf context {0}'.format(vrf))
return commands
def get_vrf_description(vrf, module):
command = (r'show run section vrf | begin ^vrf\scontext\s{0} | end ^vrf.*'.format(vrf))
description = ''
descr_regex = r".*description\s(?P<descr>[\S+\s]+).*"
def get_existing_vrfs(module):
objs = list()
command = "show vrf all"
try:
body = execute_show_command(command, module)[0]
except IndexError:
return description
return list()
try:
vrf_table = body['TABLE_vrf']['ROW_vrf']
except (TypeError, IndexError, KeyError):
return list()
if body:
splitted_body = body.split('\n')
for element in splitted_body:
if 'description' in element:
match_description = re.match(descr_regex, element,
re.DOTALL)
group_description = match_description.groupdict()
description = group_description["descr"]
if isinstance(vrf_table, list):
for vrf in vrf_table:
obj = {}
obj['name'] = vrf['vrf_name']
objs.append(obj)
return description
elif isinstance(vrf_table, dict):
obj = {}
obj['name'] = vrf_table['vrf_name']
objs.append(obj)
return objs
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']
description = w['description']
vni = w['vni']
rd = w['rd']
admin_state = w['admin_state']
interfaces = w.get('interfaces') or []
state = w['state']
del w['state']
obj_in_have = search_obj_in_list(name, have)
if state == 'absent' and obj_in_have:
commands.append('no vrf context {0}'.format(name))
elif state == 'present':
if not obj_in_have:
commands.append('vrf context {0}'.format(name))
if rd and rd != '':
commands.append('rd {0}'.format(rd))
if description:
commands.append('description {0}'.format(description))
if vni and vni != '':
commands.append('vni {0}'.format(vni))
if admin_state == 'up':
commands.append('no shutdown')
elif admin_state == 'down':
commands.append('shutdown')
if commands:
if vni:
if have.get('vni') and have.get('vni') != '':
commands.insert(1, 'no vni {0}'.format(have['vni']))
commands.append('exit')
if interfaces:
for i in interfaces:
commands.append('interface {0}'.format(i))
commands.append('no switchport')
commands.append('vrf member {0}'.format(name))
else:
if interfaces:
if not obj_in_have['interfaces']:
for i in interfaces:
commands.append('vrf context {0}'.format(name))
commands.append('exit')
commands.append('interface {0}'.format(i))
commands.append('no switchport')
commands.append('vrf member {0}'.format(name))
elif set(interfaces) != set(obj_in_have['interfaces']):
missing_interfaces = list(set(interfaces) - set(obj_in_have['interfaces']))
for i in missing_interfaces:
commands.append('vrf context {0}'.format(name))
commands.append('exit')
commands.append('interface {0}'.format(i))
commands.append('no switchport')
commands.append('vrf member {0}'.format(name))
superfluous_interfaces = list(set(obj_in_have['interfaces']) - set(interfaces))
for i in superfluous_interfaces:
commands.append('vrf context {0}'.format(name))
commands.append('exit')
commands.append('interface {0}'.format(i))
commands.append('no switchport')
commands.append('no vrf member {0}'.format(name))
if purge:
existing = get_existing_vrfs(module)
if existing:
for h in existing:
if h['name'] in ('default', 'management'):
pass
else:
obj_in_want = search_obj_in_list(h['name'], want)
if not obj_in_want:
commands.append('no vrf context {0}'.format(h['name']))
return commands
def validate_vrf(name, module):
if name == 'default':
module.fail_json(msg='cannot use default as name of a VRF')
elif len(name) > 32:
module.fail_json(msg='VRF name exceeded max length of 32', name=name)
else:
return name
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]
d = item.copy()
d['name'] = validate_vrf(d['name'], module)
obj.append(d)
else:
obj.append({
'name': validate_vrf(module.params['name'], module),
'description': module.params['description'],
'vni': module.params['vni'],
'rd': module.params['rd'],
'admin_state': module.params['admin_state'],
'state': module.params['state'],
'interfaces': module.params['interfaces']
})
return obj
def get_value(arg, config, module):
@ -177,100 +342,120 @@ def get_value(arg, config, module):
return value
def get_vrf(vrf, module):
command = 'show vrf {0}'.format(vrf)
vrf_key = {
'vrf_name': 'vrf',
'vrf_state': 'admin_state'
}
def map_config_to_obj(want, element_spec, module):
objs = list()
for w in want:
obj = deepcopy(element_spec)
del obj['delay']
del obj['state']
command = 'show vrf {0}'.format(w['name'])
try:
body = execute_show_command(command, module)[0]
vrf_table = body['TABLE_vrf']['ROW_vrf']
except (TypeError, IndexError):
return {}
return list()
parsed_vrf = apply_key_map(vrf_key, vrf_table)
parsed_vrf['admin_state'] = parsed_vrf['admin_state'].lower()
name = vrf_table['vrf_name']
obj['name'] = name
obj['admin_state'] = vrf_table['vrf_state'].lower()
command = 'show run all | section vrf.context.{0}'.format(vrf)
command = 'show run all | section vrf.context.{0}'.format(name)
body = execute_show_command(command, module)[0]
extra_params = ['vni', 'rd', 'description']
for param in extra_params:
parsed_vrf[param] = get_value(param, body, module)
obj[param] = get_value(param, body, module)
return parsed_vrf
obj['interfaces'] = []
command = 'show vrf {0} interface'.format(name)
try:
body = execute_show_command(command, module)[0]
vrf_int = body['TABLE_if']['ROW_if']
except (TypeError, IndexError):
vrf_int = None
if vrf_int:
if isinstance(vrf_int, list):
for i in vrf_int:
intf = i['if_name']
obj['interfaces'].append(intf)
elif isinstance(vrf_int, dict):
intf = vrf_int['if_name']
obj['interfaces'].append(intf)
objs.append(obj)
return objs
def check_declarative_intent_params(want, element_spec, module):
if module.params['interfaces']:
time.sleep(module.params['delay'])
have = map_config_to_obj(want, element_spec, module)
for w in want:
for i in w['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():
argument_spec = dict(
vrf=dict(required=True),
description=dict(default=None, required=False),
vni=dict(required=False, type='str'),
rd=dict(required=False, type='str'),
admin_state=dict(default='up', choices=['up', 'down'], required=False),
state=dict(default='present', choices=['present', 'absent'], required=False),
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
""" main entry point for module execution
"""
element_spec = dict(
name=dict(aliases=['vrf']),
description=dict(),
vni=dict(type=str),
rd=dict(type=str),
admin_state=dict(default='up', choices=['up', 'down']),
interfaces=dict(type='list'),
delay=dict(default=10, type='int'),
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)
argument_spec.update(nxos_argument_spec)
module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
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)
results = dict(changed=False, warnings=warnings)
result = {'changed': False}
if warnings:
result['warnings'] = warnings
vrf = module.params['vrf']
admin_state = module.params['admin_state'].lower()
description = module.params['description']
rd = module.params['rd']
vni = module.params['vni']
state = module.params['state']
want = map_params_to_obj(module)
have = map_config_to_obj(want, element_spec, module)
if vrf == 'default':
module.fail_json(msg='cannot use default as name of a VRF')
elif len(vrf) > 32:
module.fail_json(msg='VRF name exceeded max length of 32', vrf=vrf)
existing = get_vrf(vrf, module)
args = dict(vrf=vrf, description=description, vni=vni,
admin_state=admin_state, rd=rd)
proposed = dict((k, v) for k, v in args.items() if v is not None)
delta = dict(set(proposed.items()).difference(existing.items()))
commands = []
if state == 'absent':
if existing:
command = ['no vrf context {0}'.format(vrf)]
commands.extend(command)
elif state == 'present':
if not existing:
command = get_commands_to_config_vrf(delta, vrf)
commands.extend(command)
elif delta:
command = get_commands_to_config_vrf(delta, vrf)
commands.extend(command)
if state == 'present' and commands:
if proposed.get('vni'):
if existing.get('vni') and existing.get('vni') != '':
commands.insert(1, 'no vni {0}'.format(existing['vni']))
commands = map_obj_to_commands((want, have), module)
result['commands'] = commands
if commands and not module.check_mode:
load_config(module, commands)
results['changed'] = True
result['changed'] = True
if 'configure' in commands:
commands.pop(0)
if result['changed']:
check_declarative_intent_params(want, element_spec, module)
results['commands'] = commands
module.exit_json(**results)
module.exit_json(**result)
if __name__ == '__main__':

View file

@ -0,0 +1,169 @@
---
- debug: msg="START connection={{ ansible_connection }} nxos_vrf intent & aggregate test"
- debug: msg="Using provider={{ connection.transport }}"
when: ansible_connection == "local"
- set_fact: testint1="{{ nxos_int1 }}"
- set_fact: testint2="{{ nxos_int2 }}"
- name: setup - remove vrf from interfaces used in test(part1)
nxos_config:
lines:
- no vrf member test1
parents: no switchport
before: "interface {{ testint1 }}"
provider: "{{ connection }}"
ignore_errors: yes
- name: setup - remove vrf from interfaces used in test(part2)
nxos_config:
lines:
- no vrf member test1
parents: no switchport
before: "interface {{ testint2 }}"
provider: "{{ connection }}"
ignore_errors: yes
- name: setup - delete VRF test1 used in test
nxos_config:
lines:
- no vrf context test1
provider: "{{ connection }}"
ignore_errors: yes
- name: setup - remove VRF test2 used in test
nxos_config:
lines:
- no vrf context test2
provider: "{{ connection }}"
ignore_errors: yes
- name: aggregate definitions of VRFs
nxos_vrf: &create
aggregate:
- { name: test1, description: Configured by Ansible }
- { name: test2, description: Testing, admin_state: down }
provider: "{{ connection }}"
register: result
- assert:
that:
- 'result.changed == true'
- '"vrf context test1" in result.commands'
- '"description Configured by Ansible" in result.commands'
- '"no shutdown" in result.commands'
- '"vrf context test2" in result.commands'
- '"description Testing" in result.commands'
- '"shutdown" in result.commands'
- name: aggregate definitions of VRFs(Idempotence)
nxos_vrf: *create
register: result
- assert:
that:
- "result.changed == false"
- name: Assign interfaces to VRF
nxos_vrf: &interfaces
name: test1
interfaces:
- "{{ testint1 }}"
- "{{ testint2 }}"
provider: "{{ connection }}"
register: result
- assert:
that:
- 'result.changed == true'
- '"interface {{ testint1 }}" in result.commands'
- '"vrf member test1" in result.commands'
- '"interface {{ testint2 }}" in result.commands'
- '"vrf member test1" in result.commands'
- name: Assign interfaces to vrf(Idempotence)
nxos_vrf: *interfaces
register: result
- assert:
that:
- 'result.changed == false'
- name: Remove interface from vrf
nxos_vrf: &single_int
name: test1
interfaces:
- "{{ testint2 }}"
provider: "{{ connection }}"
register: result
- assert:
that:
- 'result.changed == true'
- '"interface {{ testint1 }}" in result.commands'
- '"no vrf member test1" in result.commands'
- name: Remove interface from vrf(idempotence)
nxos_vrf: *single_int
register: result
- assert:
that:
- 'result.changed == false'
- name: Delete VRFs
nxos_vrf: &delete
aggregate:
- { name: test1, description: Configured by Ansible }
- { name: test2, description: Testing, admin_state: down }
state: absent
provider: "{{ connection }}"
register: result
- assert:
that:
- 'result.changed == true'
- '"no vrf context test1" in result.commands'
- '"no vrf context test2" in result.commands'
- name: Delete VRFs(Idempotence)
nxos_vrf: *delete
register: result
- assert:
that:
- "result.changed == false"
- name: setup - remove vrf from interfaces used in test(part1)
nxos_config:
lines:
- no vrf member test1
parents: no switchport
before: "interface {{ testint1 }}"
provider: "{{ connection }}"
ignore_errors: yes
- name: setup - remove vrf from interfaces used in test(part2)
nxos_config:
lines:
- no vrf member test1
parents: no switchport
before: "interface {{ testint2 }}"
provider: "{{ connection }}"
ignore_errors: yes
- name: setup - delete VRF test1 used in test
nxos_config:
lines:
- no vrf context test1
provider: "{{ connection }}"
ignore_errors: yes
- name: setup - remove VRF test2 used in test
nxos_config:
lines:
- no vrf context test2
provider: "{{ connection }}"
ignore_errors: yes
- debug: msg="END connection={{ ansible_connection }} nxos_vrf intent & aggregate test"

View file

@ -4,6 +4,13 @@
when: ansible_connection == "local"
- block:
- name: 'Setup: Delete VRF before test'
nxos_config:
lines:
- no vrf context ntc
provider: "{{ connection }}"
ignore_errors: yes
- name: Ensure ntc VRF exists on switch
nxos_vrf: &configure
vrf: ntc
@ -48,4 +55,11 @@
- assert: *false
- name: 'Teardown: Delete VRF after test'
nxos_config:
lines:
- no vrf context ntc
provider: "{{ connection }}"
ignore_errors: yes
- debug: msg="END connection={{ ansible_connection }} nxos_vrf sanity test"

View file

@ -59,7 +59,7 @@ class TestNxosVrfModule(TestNxosModule):
def test_nxos_vrf_present(self):
set_module_args(dict(vrf='ntc', state='present', admin_state='up'))
self.execute_module(changed=True, commands=['vrf context ntc', 'no shutdown'])
self.execute_module(changed=True, commands=['vrf context ntc', 'no shutdown', 'exit'])
def test_nxos_vrf_present_no_change(self):
set_module_args(dict(vrf='management', state='present', admin_state='up'))