nxos_vlans: fix rmb behaviors and tests (#63650)

* nxos_vlans: fix rmb behaviors and tests

* whitespace fixes

* whitespace fixes 2

* review comments addressed

* TBD for legacy support

* additional tests to hit code coverage misses

* whitespace
pull/65054/head
Chris Van Heuveln 5 years ago committed by Trishna Guha
parent 067e96b152
commit 3252665a94

@ -34,8 +34,6 @@ class Vlans(ConfigBase):
'vlans', 'vlans',
] ]
exclude_params = ['name', 'state']
def __init__(self, module): def __init__(self, module):
super(Vlans, self).__init__(module) super(Vlans, self).__init__(module)
@ -49,8 +47,17 @@ class Vlans(ConfigBase):
vlans_facts = facts['ansible_network_resources'].get('vlans') vlans_facts = facts['ansible_network_resources'].get('vlans')
if not vlans_facts: if not vlans_facts:
return [] return []
# Remove vlan 1 from facts list
vlans_facts = [i for i in vlans_facts if (int(i['vlan_id'])) != 1]
return vlans_facts return vlans_facts
def edit_config(self, commands):
"""Wrapper method for `_connection.edit_config()`
This exists solely to allow the unit test framework to mock device connection calls.
"""
return self._connection.edit_config(commands)
def execute_module(self): def execute_module(self):
""" Execute the module """ Execute the module
@ -65,7 +72,7 @@ class Vlans(ConfigBase):
commands.extend(self.set_config(existing_vlans_facts)) commands.extend(self.set_config(existing_vlans_facts))
if commands: if commands:
if not self._module.check_mode: if not self._module.check_mode:
self._connection.edit_config(commands) self.edit_config(commands)
result['changed'] = True result['changed'] = True
result['commands'] = commands result['commands'] = commands
@ -90,6 +97,8 @@ class Vlans(ConfigBase):
want = [] want = []
if config: if config:
for w in config: for w in config:
if int(w['vlan_id']) == 1:
self._module.fail_json(msg="Vlan 1 is not allowed to be managed by this module")
want.append(remove_empties(w)) want.append(remove_empties(w))
have = existing_vlans_facts have = existing_vlans_facts
resp = self.set_state(want, have) resp = self.set_state(want, have)
@ -121,57 +130,78 @@ class Vlans(ConfigBase):
commands.extend(self._state_replaced(w, have)) commands.extend(self._state_replaced(w, have))
return commands return commands
def _state_replaced(self, w, have): def remove_default_states(self, obj):
""" The command generator when state is replaced """Removes non-empty but default states from the obj.
"""
default_states = {
'enabled': True,
'state': 'active',
'mode': 'ce',
}
for k in default_states.keys():
if obj[k] == default_states[k]:
obj.pop(k, None)
return obj
def _state_replaced(self, want, have):
""" The command generator when state is replaced.
Scope is limited to vlan objects defined in the playbook.
:rtype: A list :rtype: A list
:returns: the commands necessary to migrate the current configuration :returns: The minimum command set required to migrate the current
to the desired configuration configuration to the desired configuration.
""" """
commands = [] obj_in_have = search_obj_in_list(want['vlan_id'], have, 'vlan_id')
obj_in_have = search_obj_in_list(w['vlan_id'], have, 'vlan_id') if obj_in_have:
diff = dict_diff(w, obj_in_have) # ignore states that are already reset, then diff what's left
merged_commands = self.set_commands(w, have) obj_in_have = self.remove_default_states(obj_in_have)
if 'vlan_id' not in diff: diff = dict_diff(want, obj_in_have)
diff['vlan_id'] = w['vlan_id'] # Remove merge items from diff; what's left will be used to
wkeys = w.keys() # remove states not specified in the playbook
dkeys = diff.keys() for k in dict(set(want.items()) - set(obj_in_have.items())).keys():
for k in wkeys: diff.pop(k, None)
if k in self.exclude_params and k in dkeys: else:
del diff[k] diff = want
replaced_commands = self.del_attribs(diff)
# merged_cmds: 'want' cmds to update 'have' states that don't match
if merged_commands: # replaced_cmds: remaining 'have' cmds that need to be reset to default
cmds = set(replaced_commands).intersection(set(merged_commands)) merged_cmds = self.set_commands(want, have)
for cmd in cmds: replaced_cmds = []
merged_commands.remove(cmd) if obj_in_have:
commands.extend(replaced_commands) # Remaining diff items are used to reset states to default
commands.extend(merged_commands) replaced_cmds = self.del_attribs(diff)
return commands cmds = []
if replaced_cmds or merged_cmds:
cmds += ['vlan %s' % str(want['vlan_id'])]
cmds += merged_cmds + replaced_cmds
return cmds
def _state_overridden(self, want, have): def _state_overridden(self, want, have):
""" The command generator when state is overridden """ The command generator when state is overridden.
Scope includes all vlan objects on the device.
:rtype: A list :rtype: A list
:returns: the commands necessary to migrate the current configuration :returns: the minimum command set required to migrate the current
to the desired configuration configuration to the desired configuration.
""" """
commands = [] # overridden behavior is the same as replaced except for scope.
cmds = []
existing_vlans = []
for h in have: for h in have:
existing_vlans.append(h['vlan_id'])
obj_in_want = search_obj_in_list(h['vlan_id'], want, 'vlan_id') obj_in_want = search_obj_in_list(h['vlan_id'], want, 'vlan_id')
if h == obj_in_want: if obj_in_want:
continue if h != obj_in_want:
for w in want: replaced_cmds = self._state_replaced(obj_in_want, [h])
if h['vlan_id'] == w['vlan_id']: if replaced_cmds:
wkeys = w.keys() cmds.extend(replaced_cmds)
hkeys = h.keys() else:
for k in wkeys: cmds.append('no vlan %s' % h['vlan_id'])
if k in self.exclude_params and k in hkeys:
del h[k] # Add wanted vlans that don't exist on the device yet
commands.extend(self.del_attribs(h))
for w in want: for w in want:
commands.extend(self.set_commands(w, have)) if w['vlan_id'] not in existing_vlans:
return commands new_vlan = ['vlan %s' % w['vlan_id']]
cmds.extend(new_vlan + self.add_commands(w))
return cmds
def _state_merged(self, w, have): def _state_merged(self, w, have):
""" The command generator when state is merged """ The command generator when state is merged
@ -180,7 +210,10 @@ class Vlans(ConfigBase):
:returns: the commands necessary to merge the provided into :returns: the commands necessary to merge the provided into
the current configuration the current configuration
""" """
return self.set_commands(w, have) cmds = self.set_commands(w, have)
if cmds:
cmds.insert(0, 'vlan %s' % str(w['vlan_id']))
return(cmds)
def _state_deleted(self, want, have): def _state_deleted(self, want, have):
""" The command generator when state is deleted """ The command generator when state is deleted
@ -193,7 +226,8 @@ class Vlans(ConfigBase):
if want: if want:
for w in want: for w in want:
obj_in_have = search_obj_in_list(w['vlan_id'], have, 'vlan_id') obj_in_have = search_obj_in_list(w['vlan_id'], have, 'vlan_id')
commands.append('no vlan ' + str(obj_in_have['vlan_id'])) if obj_in_have:
commands.append('no vlan ' + str(obj_in_have['vlan_id']))
else: else:
if not have: if not have:
return commands return commands
@ -202,20 +236,20 @@ class Vlans(ConfigBase):
return commands return commands
def del_attribs(self, obj): def del_attribs(self, obj):
"""Returns a list of commands to reset states to default
"""
commands = [] commands = []
if not obj or len(obj.keys()) == 1: if not obj:
return commands return commands
commands.append('vlan ' + str(obj['vlan_id'])) default_cmds = {
if 'name' in obj: 'name': 'no name',
commands.append('no' + ' ' + 'name') 'state': 'no state',
if 'state' in obj: 'enabled': 'no shutdown',
commands.append('no state') 'mode': 'mode ce',
if 'enabled' in obj: 'mapped_vni': 'no vn-segment',
commands.append('no shutdown') }
if 'mode' in obj: for k in obj:
commands.append('mode ce') commands.append(default_cmds[k])
if 'mapped_vni' in obj:
commands.append('no vn-segment')
return commands return commands
def diff_of_dicts(self, w, obj): def diff_of_dicts(self, w, obj):
@ -229,20 +263,19 @@ class Vlans(ConfigBase):
commands = [] commands = []
if not d: if not d:
return commands return commands
commands.append('vlan' + ' ' + str(d['vlan_id']))
if 'name' in d: if 'name' in d:
commands.append('name ' + d['name']) commands.append('name ' + d['name'])
if 'state' in d: if 'state' in d:
commands.append('state ' + d['state']) commands.append('state ' + d['state'])
if 'enabled' in d: if 'enabled' in d:
if d['enabled'] == 'True': if d['enabled'] is True:
commands.append('no shutdown') commands.append('no shutdown')
else: else:
commands.append('shutdown') commands.append('shutdown')
if 'mode' in d: if 'mode' in d:
commands.append('mode ' + d['mode']) commands.append('mode ' + d['mode'])
if 'mapped_vni' in d: if 'mapped_vni' in d:
commands.append('vn-segment ' + d['mapped_vni']) commands.append('vn-segment %s' % d['mapped_vni'])
return commands return commands

@ -13,6 +13,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import re import re
import ast
from copy import deepcopy from copy import deepcopy
from ansible.module_utils.network.common import utils from ansible.module_utils.network.common import utils
@ -38,6 +39,12 @@ class VlansFacts(object):
self.generated_spec = utils.generate_dict(facts_argument_spec) self.generated_spec = utils.generate_dict(facts_argument_spec)
def get_device_data(self, connection, show_cmd):
"""Wrapper method for `connection.get()`
This exists solely to allow the unit test framework to mock device connection calls.
"""
return connection.get(show_cmd)
def populate_facts(self, connection, ansible_facts, data=None): def populate_facts(self, connection, ansible_facts, data=None):
""" Populate the facts for vlans """ Populate the facts for vlans
:param connection: the device connection :param connection: the device connection
@ -46,25 +53,26 @@ class VlansFacts(object):
:returns: facts :returns: facts
""" """
objs = [] objs = []
# **TBD**
# N7K EOL/legacy image 6.2 does not support show vlan | json output.
# If support is still required for this image then:
# - Wrapp the json calls below in a try/except
# - When excepted, use a helper method to parse the run_cfg_output,
# using the run_cfg_output data to generate compatible json data that
# can be read by normalize_table_data.
if not data: if not data:
data = connection.get('show running-config | section ^vlan') # Use structured for most of the vlan parameter states.
vlans = re.split(r'(,|-)', data.split()[1]) # This data is consistent across the supported nxos platforms.
for v in vlans: structured = self.get_device_data(connection, 'show vlan | json')
if not v.isdigit():
vlans.remove(v) # Raw cli config is needed for mapped_vni, which is not included in structured.
run_cfg_output = self.get_device_data(connection, 'show running-config | section ^vlan')
config = re.split(r'(^|\n)vlan', data)
for conf in config: # Create a single dictionary from all data sources
conf = conf.strip() data = self.normalize_table_data(structured, run_cfg_output)
if conf:
if conf[0] in vlans: for vlan in data:
vlans.remove(conf[0]) obj = self.render_config(self.generated_spec, vlan)
obj = self.render_config(self.generated_spec, conf)
if obj and len(obj.keys()) > 1:
objs.append(obj)
for v in vlans:
obj = self.render_config(self.generated_spec, v)
if obj: if obj:
objs.append(obj) objs.append(obj)
@ -75,36 +83,90 @@ class VlansFacts(object):
params = utils.validate_config(self.argument_spec, {'config': objs}) params = utils.validate_config(self.argument_spec, {'config': objs})
for cfg in params['config']: for cfg in params['config']:
facts['vlans'].append(utils.remove_empties(cfg)) facts['vlans'].append(utils.remove_empties(cfg))
ansible_facts['ansible_network_resources'].update(facts) ansible_facts['ansible_network_resources'].update(facts)
return ansible_facts return ansible_facts
def render_config(self, spec, conf): def render_config(self, spec, vlan):
""" """
Render config as dictionary structure and delete keys Render config as dictionary structure and delete keys
from spec for null values from spec for null values
:param spec: The facts tree, generated from the argspec :param spec: The facts tree, generated from the argspec
:param conf: The configuration :param vlan: structured data vlan settings (dict) and raw cfg from device
:rtype: dictionary :rtype: dictionary
:returns: The generated config :returns: The generated config
Sample inputs: test/units/modules/network/nxos/fixtures/nxos_vlans/show_vlan
""" """
config = deepcopy(spec) obj = deepcopy(spec)
if len(conf) == 1:
return utils.remove_empties({'vlan_id': conf}) obj['vlan_id'] = vlan['vlan_id']
match = re.search(r'^(\S+)?', conf, re.M) # name: 'VLAN000x' (default name) or custom name
if match: name = vlan['vlanshowbr-vlanname']
if len(match.group(1)) == 1: if name and re.match("VLAN%04d" % int(vlan['vlan_id']), name):
config['vlan_id'] = match.group(1) name = None
config['name'] = parse_conf_arg(conf, 'name') obj['name'] = name
config['mode'] = parse_conf_arg(conf, 'mode')
config['mapped_vni'] = parse_conf_arg(conf, 'vn-segment') # mode: 'ce-vlan' or 'fabricpath-vlan'
config['state'] = parse_conf_arg(conf, 'state') obj['mode'] = vlan['vlanshowinfo-vlanmode'].replace('-vlan', '')
admin_state = parse_conf_cmd_arg(conf, 'shutdown', 'down', 'up')
if admin_state == 'up': # enabled: shutdown, noshutdown
config['enabled'] = True obj['enabled'] = True if 'noshutdown' in vlan['vlanshowbr-shutstate'] else False
elif admin_state == 'down':
config['enabled'] = False # state: active, suspend
obj['state'] = vlan['vlanshowbr-vlanstate']
vlans_cfg = utils.remove_empties(config)
return vlans_cfg # non-structured data
obj['mapped_vni'] = parse_conf_arg(vlan['run_cfg'], 'vn-segment')
return utils.remove_empties(obj)
def normalize_table_data(self, structured, run_cfg_output):
"""Normalize structured output and raw running-config output into
a single dict to simplify render_config usage.
This is needed because:
- The NXOS devices report most of the vlan settings within two
structured data keys: 'vlanbrief' and 'mtuinfo', but the output is
incomplete and therefore raw running-config data is also needed.
- running-config by itself is insufficient because of major differences
in the cli config syntax across platforms.
- Thus a helper method combines settings from the separate top-level keys,
and adds a 'run_cfg' key containing raw cli from the device.
"""
# device output may be string, convert to list
structured = ast.literal_eval(str(structured))
vlanbrief = []
mtuinfo = []
if 'TABLE_vlanbrief' in structured:
# SAMPLE: {"TABLE_vlanbriefid": {"ROW_vlanbriefid": {
# "vlanshowbr-vlanid": "4", "vlanshowbr-vlanid-utf": "4",
# "vlanshowbr-vlanname": "VLAN0004", "vlanshowbr-vlanstate": "active",
# "vlanshowbr-shutstate": "noshutdown"}},
vlanbrief = structured['TABLE_vlanbrief']['ROW_vlanbrief']
# SAMPLE: "TABLE_mtuinfoid": {"ROW_mtuinfoid": {
# "vlanshowinfo-vlanid": "4", "vlanshowinfo-media-type": "enet",
# "vlanshowinfo-vlanmode": "ce-vlan"}}
mtuinfo = structured['TABLE_mtuinfo']['ROW_mtuinfo']
if type(vlanbrief) is not list:
# vlanbrief is not a list when only one vlan is found.
vlanbrief = [vlanbrief]
mtuinfo = [mtuinfo]
# split out any per-vlan cli config
run_cfg_list = re.split(r'[\n^]vlan ', run_cfg_output)
# Create a list of vlan dicts where each dict contains vlanbrief,
# mtuinfo, and non-structured running-config data for one vlan.
vlans = []
for index, v in enumerate(vlanbrief):
v['vlan_id'] = v.get('vlanshowbr-vlanid-utf')
vlan = {}
vlan.update(v)
vlan.update(mtuinfo[index])
run_cfg = [i for i in run_cfg_list if "%s\n" % v['vlan_id'] in i] or ['']
vlan['run_cfg'] = run_cfg.pop()
vlans.append(vlan)
return vlans

@ -5,6 +5,7 @@
- name: setup - name: setup
cli_config: cli_config:
config: | config: |
no vlan 2-100
vlan 5 vlan 5
vlan 6 vlan 6
@ -23,7 +24,7 @@
- assert: - assert:
that: that:
- "ansible_facts.network_resources.vlans|symmetric_difference(result.before)|length == 0" - "result.before|length == (ansible_facts.network_resources.vlans|length - 1)"
- "result.after|length == 0" - "result.after|length == 0"
- "result.changed == true" - "result.changed == true"
- "'no vlan 5' in result.commands" - "'no vlan 5' in result.commands"

@ -5,8 +5,7 @@
- name: setup - name: setup
cli_config: &cleanup cli_config: &cleanup
config: | config: |
no vlan 5 no vlan 2-100
no vlan 6
- block: - block:
- name: Merged - name: Merged
@ -40,7 +39,7 @@
- assert: - assert:
that: that:
- "ansible_facts.network_resources.vlans|symmetric_difference(result.after)|length == 0" - "result.after|length == (ansible_facts.network_resources.vlans|length - 1)"
- name: Idempotence - Merged - name: Idempotence - Merged
nxos_vlans: *merged nxos_vlans: *merged

@ -5,18 +5,17 @@
- name: setup1 - name: setup1
cli_config: &cleanup cli_config: &cleanup
config: | config: |
no vlan 5 no vlan 2-100
no vlan 6
no vlan 9
- block: - block:
- name: setup - name: setup
cli_config: cli_config:
config: | config: |
vlan 5 vlan 5
name test-vlan5 name test-vlan5
state suspend state suspend
vlan 6 vlan 6
exit
- name: Gather vlans facts - name: Gather vlans facts
nxos_facts: &facts nxos_facts: &facts
@ -36,22 +35,21 @@
- assert: - assert:
that: that:
- "ansible_facts.network_resources.vlans|symmetric_difference(result.before)|length == 0" - "result.before|length == (ansible_facts.network_resources.vlans|length - 1)"
- "result.changed == true" - "result.changed == true"
- "'vlan 5' in result.commands" - "'no vlan 5' in result.commands"
- "'no name' in result.commands" - "'no vlan 6' in result.commands"
- "'no state' in result.commands"
- "'vlan 9' in result.commands" - "'vlan 9' in result.commands"
- "'name test-vlan9' in result.commands" - "'name test-vlan9' in result.commands"
- "'shutdown' in result.commands" - "'shutdown' in result.commands"
- "result.commands|length == 6" - "result.commands|length == 5"
- name: Gather vlans post facts - name: Gather vlans post facts
nxos_facts: *facts nxos_facts: *facts
- assert: - assert:
that: that:
- "ansible_facts.network_resources.vlans|symmetric_difference(result.after)|length == 0" - "result.after|length == (ansible_facts.network_resources.vlans|length - 1)"
- name: Idempotence - Overridden - name: Idempotence - Overridden
nxos_vlans: *overridden nxos_vlans: *overridden

@ -5,17 +5,17 @@
- name: setup1 - name: setup1
cli_config: &cleanup cli_config: &cleanup
config: | config: |
no vlan 5 no vlan 2-100
no vlan 6
- block: - block:
- name: setup2 - name: setup2
cli_config: cli_config:
config: | config: |
vlan 5 vlan 5
name test-vlan5 name test-vlan5
vlan 6 vlan 6
name test-vlan6 name test-vlan6
exit
- name: Gather vlans facts - name: Gather vlans facts
nxos_facts: &facts nxos_facts: &facts
@ -34,7 +34,7 @@
- assert: - assert:
that: that:
- "ansible_facts.network_resources.vlans|symmetric_difference(result.before)|length == 0" - "result.before|length == (ansible_facts.network_resources.vlans|length - 1)"
- "result.changed == true" - "result.changed == true"
- "'vlan 6' in result.commands" - "'vlan 6' in result.commands"
- "'no name' in result.commands" - "'no name' in result.commands"
@ -46,7 +46,7 @@
- assert: - assert:
that: that:
- "ansible_facts.network_resources.vlans|symmetric_difference(result.after)|length == 0" - "result.after|length == (ansible_facts.network_resources.vlans|length - 1)"
- name: Idempotence - Replaced - name: Idempotence - Replaced
nxos_vlans: *replaced nxos_vlans: *replaced

@ -0,0 +1,13 @@
vlan 1,3-5,8
vlan 3
name test-vlan3
vlan 5
shutdown
name test-changeme
mode fabricpath
state suspend
vn-segment 942
vlan 8
shutdown
name test-changeme-not
state suspend

@ -0,0 +1,45 @@
{
"TABLE_vlanbrief": {
"ROW_vlanbrief": [
{ "vlanshowbr-vlanid": "1", "vlanshowbr-vlanid-utf": "1",
"vlanshowbr-vlanname": "default",
"vlanshowbr-vlanstate": "active",
"vlanshowbr-shutstate": "noshutdown"
},
{ "vlanshowbr-vlanid": "3", "vlanshowbr-vlanid-utf": "3",
"vlanshowbr-vlanname": "test-vlan3",
"vlanshowbr-vlanstate": "active",
"vlanshowbr-shutstate": "noshutdown"
},
{ "vlanshowbr-vlanid": "4", "vlanshowbr-vlanid-utf": "4",
"vlanshowbr-vlanname": "VLAN0004",
"vlanshowbr-vlanstate": "active",
"vlanshowbr-shutstate": "noshutdown"
},
{ "vlanshowbr-vlanid": "5", "vlanshowbr-vlanid-utf": "5",
"vlanshowbr-vlanname": "test-changeme",
"vlanshowbr-vlanstate": "suspend",
"vlanshowbr-shutstate": "shutdown"
},
{ "vlanshowbr-vlanid": "8", "vlanshowbr-vlanid-utf": "8",
"vlanshowbr-vlanname": "test-changeme-not",
"vlanshowbr-vlanstate": "suspend",
"vlanshowbr-shutstate": "shutdown"
}
]
},
"TABLE_mtuinfo": {
"ROW_mtuinfo": [
{ "vlanshowinfo-vlanid": "1", "vlanshowinfo-media-type": "enet",
"vlanshowinfo-vlanmode": "ce-vlan" },
{ "vlanshowinfo-vlanid": "3", "vlanshowinfo-media-type": "enet",
"vlanshowinfo-vlanmode": "ce-vlan" },
{ "vlanshowinfo-vlanid": "4", "vlanshowinfo-media-type": "enet",
"vlanshowinfo-vlanmode": "ce-vlan" },
{ "vlanshowinfo-vlanid": "5", "vlanshowinfo-media-type": "enet",
"vlanshowinfo-vlanmode": "fabricpath-vlan" },
{ "vlanshowinfo-vlanid": "8", "vlanshowinfo-media-type": "enet",
"vlanshowinfo-vlanmode": "ce-vlan" }
]
}
}

@ -0,0 +1,16 @@
{
"TABLE_vlanbrief": {
"ROW_vlanbrief": {
"vlanshowbr-vlanid": "1", "vlanshowbr-vlanid-utf": "1",
"vlanshowbr-vlanname": "default",
"vlanshowbr-vlanstate": "active",
"vlanshowbr-shutstate": "noshutdown"
},
},
"TABLE_mtuinfo": {
"ROW_mtuinfo": {
"vlanshowinfo-vlanid": "1", "vlanshowinfo-media-type": "enet",
"vlanshowinfo-vlanmode": "ce-vlan"
},
}
}

@ -0,0 +1,210 @@
# (c) 2019 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/>.
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from textwrap import dedent
from units.compat.mock import patch
from units.modules.utils import AnsibleFailJson
from ansible.modules.network.nxos import nxos_vlans
from ansible.module_utils.network.nxos.config.vlans.vlans import Vlans
from .nxos_module import TestNxosModule, load_fixture, set_module_args
ignore_provider_arg = True
class TestNxosVlansModule(TestNxosModule):
module = nxos_vlans
def setUp(self):
super(TestNxosVlansModule, self).setUp()
self.mock_FACT_LEGACY_SUBSETS = patch('ansible.module_utils.network.nxos.facts.facts.FACT_LEGACY_SUBSETS')
self.FACT_LEGACY_SUBSETS = self.mock_FACT_LEGACY_SUBSETS.start()
self.mock_get_resource_connection_config = patch('ansible.module_utils.network.common.cfg.base.get_resource_connection')
self.get_resource_connection_config = self.mock_get_resource_connection_config.start()
self.mock_get_resource_connection_facts = patch('ansible.module_utils.network.common.facts.facts.get_resource_connection')
self.get_resource_connection_facts = self.mock_get_resource_connection_facts.start()
self.mock_edit_config = patch('ansible.module_utils.network.nxos.config.vlans.vlans.Vlans.edit_config')
self.edit_config = self.mock_edit_config.start()
self.mock_get_device_data = patch('ansible.module_utils.network.nxos.facts.vlans.vlans.VlansFacts.get_device_data')
self.get_device_data = self.mock_get_device_data.start()
def tearDown(self):
super(TestNxosVlansModule, self).tearDown()
self.mock_FACT_LEGACY_SUBSETS.stop()
self.mock_get_resource_connection_config.stop()
self.mock_get_resource_connection_facts.stop()
self.mock_edit_config.stop()
def load_fixtures(self, commands=None, device=''):
self.mock_FACT_LEGACY_SUBSETS.return_value = dict()
self.edit_config.return_value = None
def load_from_file(*args, **kwargs):
cmd = args[1]
filename = str(cmd).split(' | ')[0].replace(' ', '_')
return load_fixture('nxos_vlans', filename)
def load_from_file_no_facts(*args, **kwargs):
cmd = args[1]
filename = str(cmd).split(' | ')[0].replace(' ', '_')
filename += '_no_facts'
return load_fixture('nxos_vlans', filename)
def load_from_file_vlan_1(*args, **kwargs):
cmd = args[1]
filename = str(cmd).split(' | ')[0].replace(' ', '_')
filename += '_vlan_1'
return load_fixture('nxos_vlans', filename)
if device == '':
self.get_device_data.side_effect = load_from_file
elif device == '_no_facts':
self.get_device_data.side_effect = load_from_file_no_facts
elif device == '_vlan_1':
self.get_device_data.side_effect = load_from_file_vlan_1
def test_1(self):
'''
**NOTE** This config is for reference only! See fixtures files for real data.
vlan 1,3-5,8
vlan 3
name test-vlan3
!Note:vlan 4 is present with default settings
vlan 5
shutdown
name test-changeme
mode fabricpath
state suspend
vn-segment 942
!Note:vlan 7 is not present
vlan 8
shutdown
name test-changeme-not
state suspend
'''
playbook = dict(config=[
dict(vlan_id=4),
dict(vlan_id=5, mapped_vni=555, mode='ce'),
dict(vlan_id=7, mapped_vni=777, name='test-vlan7', enabled=False),
dict(vlan_id='8', state='active', name='test-changeme-not')
# vlan 3 is not present in playbook.
])
merged = [
# Update existing device states with any differences in the playbook.
'vlan 5', 'vn-segment 555', 'mode ce',
'vlan 7', 'vn-segment 777', 'name test-vlan7', 'shutdown',
'vlan 8', 'state active'
]
playbook['state'] = 'merged'
set_module_args(playbook, ignore_provider_arg)
self.execute_module(changed=True, commands=merged)
deleted = [
# Reset existing device state to default values. Scope is limited to
# objects in the play when the 'config' key is specified. For vlans
# this means deleting each vlan listed in the playbook and ignoring
# any play attrs other than 'vlan_id'.
'no vlan 4',
'no vlan 5',
'no vlan 8'
]
playbook['state'] = 'deleted'
set_module_args(playbook, ignore_provider_arg)
self.execute_module(changed=True, commands=deleted)
overridden = [
# The play is the source of truth. Similar to replaced but the scope
# includes all objects on the device; i.e. it will also reset state
# on objects not found in the play.
'no vlan 3',
'vlan 5', 'mode ce', 'vn-segment 555', 'no state', 'no shutdown', 'no name',
'vlan 8', 'no shutdown', 'state active',
'vlan 7', 'name test-vlan7', 'shutdown', 'vn-segment 777'
]
playbook['state'] = 'overridden'
set_module_args(playbook, ignore_provider_arg)
self.execute_module(changed=True, commands=overridden)
replaced = [
# Scope is limited to objects in the play.
# replaced should ignore existing vlan 3.
'vlan 5', 'mode ce', 'vn-segment 555', 'no state', 'no shutdown', 'no name',
'vlan 7', 'shutdown', 'name test-vlan7', 'vn-segment 777',
'vlan 8', 'no shutdown', 'state active'
]
playbook['state'] = 'replaced'
set_module_args(playbook, ignore_provider_arg)
self.execute_module(changed=True, commands=replaced)
def test_2(self):
# vlan 1 in playbook should raise
playbook = dict(config=[dict(vlan_id=1)], state='merged')
set_module_args(playbook, ignore_provider_arg)
self.execute_module(failed=True)
def test_3(self):
# Test when no 'config' key is used in playbook.
deleted = [
# Reset existing device state for all vlans found on device other than vlan 1.
'no vlan 3',
'no vlan 4',
'no vlan 5',
'no vlan 8'
]
playbook = dict(state='deleted')
set_module_args(playbook, ignore_provider_arg)
self.execute_module(changed=True, commands=deleted)
for test_state in ['merged', 'replaced', 'overridden']:
set_module_args(dict(state=test_state), ignore_provider_arg)
self.execute_module(failed=True)
def test_4(self):
# Test only vlan 1 found
playbook = dict(state='deleted')
set_module_args(playbook, ignore_provider_arg)
self.execute_module(device='_vlan_1', changed=False)
def test_5(self):
# Test no facts returned
playbook = dict(state='deleted')
set_module_args(playbook, ignore_provider_arg)
self.execute_module(device='_no_facts', changed=False)
def test_6(self):
# Misc tests to hit codepaths highlighted by code coverage tool as missed.
playbook = dict(config=[
dict(vlan_id=8, enabled=True)
])
replaced = [
# Update existing device states with any differences in the playbook.
'vlan 8', 'no shutdown', 'no state', 'no name'
]
playbook['state'] = 'replaced'
playbook['_ansible_check_mode'] = True
set_module_args(playbook, ignore_provider_arg)
self.execute_module(changed=True, commands=replaced)
Loading…
Cancel
Save