diff --git a/README.md b/README.md index 6c12fd979..e7e4380c5 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ Name | Description [cisco.ios.ios_route_maps](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_route_maps_module.rst)|Resource module to configure route maps. [cisco.ios.ios_service](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_service_module.rst)|Resource module to configure service. [cisco.ios.ios_snmp_server](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_snmp_server_module.rst)|Resource module to configure snmp server. +[cisco.ios.ios_spanning_tree](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_spanning_tree_module.rst)|Resource module to configure Spanning Tree. [cisco.ios.ios_static_routes](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_static_routes_module.rst)|Resource module to configure static routes. [cisco.ios.ios_system](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_system_module.rst)|Module to manage the system attributes. [cisco.ios.ios_user](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_user_module.rst)|Module to manage the aggregates of local users. diff --git a/changelogs/fragments/ios_spanning_tree.yml b/changelogs/fragments/ios_spanning_tree.yml new file mode 100644 index 000000000..8297a63c5 --- /dev/null +++ b/changelogs/fragments/ios_spanning_tree.yml @@ -0,0 +1,3 @@ +--- +major_changes: + - ios_spanning_tree - Added a new resource module to manage spanning-tree configuration. diff --git a/meta/runtime.yml b/meta/runtime.yml index 249987330..c9edaa8e9 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -57,6 +57,8 @@ plugin_routing: redirect: cisco.ios.ios_static_routes system: redirect: cisco.ios.ios_system + spanning_tree: + redirect: cisco.ios.ios_spanning_tree user: redirect: cisco.ios.ios_user vlans: diff --git a/plugins/action/spanning_tree.py b/plugins/action/spanning_tree.py new file mode 120000 index 000000000..7747aa9dd --- /dev/null +++ b/plugins/action/spanning_tree.py @@ -0,0 +1 @@ +ios.py \ No newline at end of file diff --git a/plugins/module_utils/network/ios/argspec/spanning_tree/__init__.py b/plugins/module_utils/network/ios/argspec/spanning_tree/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/network/ios/argspec/spanning_tree/spanning_tree.py b/plugins/module_utils/network/ios/argspec/spanning_tree/spanning_tree.py new file mode 100644 index 000000000..db5dd1ee7 --- /dev/null +++ b/plugins/module_utils/network/ios/argspec/spanning_tree/spanning_tree.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +# Copyright 2023 Timur Nizharadze (@tnizharadze) +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the +# cli_rm_builder. +# +# Manually editing this file is not advised. +# +# To update the argspec make the desired changes +# in the module docstring and re-run +# cli_rm_builder. +# +############################################# + +""" +The arg spec for the ios_spanning_tree module +""" + + +class Spanning_treeArgs(object): # pylint: disable=R0903 + """The arg spec for the ios_spanning_tree module""" + + argument_spec = { + "config": { + "type": "dict", + "options": { + "backbonefast": {"type": "bool"}, + "bridge_assurance": {"type": "bool"}, + "etherchannel_guard_misconfig": {"type": "bool"}, + "logging": {"type": "bool"}, + "loopguard_default": {"type": "bool"}, + "mode": { + "type": "str", + "choices": ["mst", "pvst", "rapid-pvst"], + }, + "pathcost_method": { + "type": "str", + "choices": ["long", "short"], + }, + "transmit_hold_count": {"type": "int"}, + "portfast": { + "type": "dict", + "mutually_exclusive": [ + ["network_default", "edge_default"], + ], + "options": { + "default": {"type": "bool"}, + "network_default": {"type": "bool"}, + "edge_default": {"type": "bool"}, + "bpdufilter_default": {"type": "bool"}, + "edge_bpdufilter_default": {"type": "bool"}, + "bpduguard_default": {"type": "bool"}, + "edge_bpduguard_default": {"type": "bool"}, + }, + }, + "uplinkfast": { + "type": "dict", + "options": { + "enabled": {"type": "bool"}, + "max_update_rate": {"type": "int"}, + }, + }, + "forward_time": { + "type": "list", + "elements": "dict", + "required_together": [["vlan_list", "value"]], + "options": { + "vlan_list": {"type": "str"}, + "value": {"type": "int"}, + }, + }, + "hello_time": { + "type": "list", + "elements": "dict", + "required_together": [["vlan_list", "value"]], + "options": { + "vlan_list": {"type": "str"}, + "value": {"type": "int"}, + }, + }, + "max_age": { + "type": "list", + "elements": "dict", + "required_together": [["vlan_list", "value"]], + "options": { + "vlan_list": {"type": "str"}, + "value": {"type": "int"}, + }, + }, + "priority": { + "type": "list", + "elements": "dict", + "required_together": [["vlan_list", "value"]], + "options": { + "vlan_list": {"type": "str"}, + "value": { + "type": "int", + "choices": [ + 0, + 4096, + 8192, + 12288, + 16384, + 20480, + 24576, + 28672, + 32768, + 36864, + 40960, + 45056, + 49152, + 53248, + 57344, + 61440, + ], + }, + }, + }, + "mst": { + "type": "dict", + "options": { + "simulate_pvst_global": {"type": "bool"}, + "hello_time": {"type": "int"}, + "forward_time": {"type": "int"}, + "max_age": {"type": "int"}, + "max_hops": {"type": "int"}, + "priority": { + "type": "list", + "elements": "dict", + "required_together": [["instance", "value"]], + "options": { + "instance": {"type": "str"}, + "value": { + "type": "int", + "choices": [ + 0, + 4096, + 8192, + 12288, + 16384, + 20480, + 24576, + 28672, + 32768, + 36864, + 40960, + 45056, + 49152, + 53248, + 57344, + 61440, + ], + }, + }, + }, + "configuration": { + "type": "dict", + "options": { + "name": {"type": "str"}, + "revision": {"type": "int"}, + "instances": { + "type": "list", + "elements": "dict", + "required_together": [ + [ + "instance", + "vlan_list", + ], + ], + "options": { + "instance": {"type": "int"}, + "vlan_list": {"type": "str"}, + }, + }, + }, + }, + }, + }, + }, + }, + "running_config": {"type": "str"}, + "state": { + "type": "str", + "choices": [ + "merged", + "replaced", + "deleted", + "rendered", + "parsed", + "gathered", + ], + "default": "merged", + }, + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/ios/config/spanning_tree/__init__.py b/plugins/module_utils/network/ios/config/spanning_tree/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/network/ios/config/spanning_tree/spanning_tree.py b/plugins/module_utils/network/ios/config/spanning_tree/spanning_tree.py new file mode 100644 index 000000000..137919b06 --- /dev/null +++ b/plugins/module_utils/network/ios/config/spanning_tree/spanning_tree.py @@ -0,0 +1,398 @@ +# -*- coding: utf-8 -*- +# Copyright 2023 Timur Nizharadze (@tnizharadze) +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The ios_spanning_tree config file. +It is in this file where the current configuration (as dict) +is compared to the provided configuration (as dict) and the command set +necessary to bring the current configuration to its desired end-state is +created. +""" + +from ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( + ResourceModule, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + get_from_dict, +) + +from ansible_collections.cisco.ios.plugins.module_utils.network.ios.facts.facts import Facts +from ansible_collections.cisco.ios.plugins.module_utils.network.ios.rm_templates.spanning_tree import ( + Spanning_treeTemplate, +) + + +class Spanning_tree(ResourceModule): + """ + The ios_spanning_tree config class + """ + + def __init__(self, module): + super(Spanning_tree, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="spanning_tree", + tmplt=Spanning_treeTemplate(), + ) + self.linear_parsers = [ + "backbonefast", + "bridge_assurance", + "etherchannel_guard_misconfig", + "logging", + "loopguard_default", + "mode", + "pathcost_method", + "transmit_hold_count", + "portfast.default", + "portfast.network_default", + "portfast.edge_default", + "portfast.bpdufilter_default", + "portfast.edge_bpdufilter_default", + "portfast.bpduguard_default", + "portfast.edge_bpduguard_default", + "uplinkfast.enabled", + "uplinkfast.max_update_rate", + "mst.simulate_pvst_global", + ] + self.complex_parsers = [ + "forward_time", + "hello_time", + "max_age", + "priority", + ] + self.mst_parsers = [ + "mst.hello_time", + "mst.forward_time", + "mst.max_age", + "mst.max_hops", + "mst.priority", + ] + self.mst_config_parsers = [ + "mst.configuration", + "mst.configuration.name", + "mst.configuration.revision", + "mst.configuration.instances", + ] + self.negated_parsers = [ + "bridge_assurance", + "etherchannel_guard_misconfig", + "mst.simulate_pvst_global", + ] + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + return self.result + + def generate_commands(self): + """Generate configuration commands to send based on + want, have and desired state. + """ + wantd = self.want + haved = self.have + + # if state is merged, merge want onto have and then compare + if self.state == "merged": + wantd = self._dict_copy_merged(wantd, haved) + + # if state is deleted, empty out wantd and set haved to wantd + if self.state == "deleted": + haved = self._dict_copy_deleted(want=wantd, have=haved) + wantd = {} + + self._compare_linear(wantd, haved) + self._compare_complex(wantd, haved) + self._compare_mst(wantd, haved) + self._compare_mst_config(wantd, haved) + + def _compare_linear(self, want, have): + for x in self.linear_parsers: + if x == "mode": + wmode = get_from_dict(want, "mode") + hmode = get_from_dict(have, "mode") + if wmode is None and (hmode == "pvst" or hmode == "rapid-pvst"): + continue + if x in self.negated_parsers: + inw = get_from_dict(want, x) + inh = get_from_dict(have, x) + if inw == inh: + continue + if inh is None and not inw: + self.addcmd(want, x, True) + elif inw is None and not inh: + self.addcmd(have, x, False) + elif inw and inh is not None and not inh: + self.addcmd(want, x, False) + continue + self.compare([x], want=want, have=have) + + def _compare_complex(self, want, have): + for x in self.complex_parsers: + self._compare_complex_dict(want, have, "vlan_list", "value", x) + + def _compare_mst(self, want, have): + wmode = get_from_dict(want, "mode") + hmode = get_from_dict(have, "mode") + if not ((wmode is None and (hmode == "mst" or hmode is None)) or wmode == "mst"): + return + for x in self.mst_parsers: + if x == "mst.priority": + self._compare_complex_dict(want, have, "instance", "value", x) + else: + self.compare([x], want=want, have=have) + + def _compare_mst_config(self, want, have): + cmd_len = len(self.commands) + for x in self.mst_config_parsers: + if x == "mst.configuration": + wx = get_from_dict(want, x) + hx = get_from_dict(have, x) + if self.state == "deleted": + if hx is not None and not hx: + self.compare(parsers=[x], want={}, have=have) + return + elif hx: + self.compare(parsers=[x], want=have, have={}) + else: + return + elif self.state == "replaced": + if wx is None: + self.compare(parsers=[x], want={}, have=have) + return + elif wx: + self.compare(parsers=[x], want=want, have={}) + else: + return + elif wx: + self.compare(parsers=[x], want=want, have={}) + else: + return + if x in [ + "mst.configuration.name", + "mst.configuration.revision", + ]: + self.compare(parsers=[x], want=want, have=have) + if x == "mst.configuration.instances": + self._compare_complex_dict(want, have, "vlan_list", "instance", x) + if (cmd_len + 1) == len(self.commands): + self.commands.pop() + elif (cmd_len + 1) < len(self.commands): + self.commands.append("exit") + + def _compare_complex_dict(self, want, have, dkey, dvalue, x): + wx = get_from_dict(want, x) or [] + hx = get_from_dict(have, x) or [] + + wparams = {} + while len(wx) > 0: + each = wx.pop() + if each[dvalue] not in wparams: + wparams.update({each[dvalue]: set()}) + vlan_list = set(self._str_to_num_list(each[dkey])) + wparams[each[dvalue]].update(vlan_list) + + hparams = {} + while len(hx) > 0: + each = hx.pop() + if each[dvalue] not in hparams: + hparams.update({each[dvalue]: set()}) + vlan_list = set(self._str_to_num_list(each[dkey])) + hparams[each[dvalue]].update(vlan_list) + + wdiff = {} + for k in wparams.keys(): + if k in hparams: + wdiff[k] = wparams[k] - hparams[k] + else: + wdiff[k] = wparams[k] + + hdiff = {} + for k in hparams.keys(): + if k in wparams: + hdiff[k] = hparams[k] - wparams[k] + else: + hdiff[k] = hparams[k] + + for k in wdiff.keys(): + if len(wdiff[k]) == 0: + wparams.pop(k) + else: + wparams[k] = self._num_list_to_str(sorted(list(wdiff[k]))) + + for k in hdiff.keys(): + if len(hdiff[k]) == 0: + hparams.pop(k) + else: + hparams[k] = self._num_list_to_str(sorted(list(hdiff[k]))) + + if wparams != hparams: + for k, v in iteritems(wparams): + wx += [{dkey: v, dvalue: k}] + + for k, v in iteritems(hparams): + hx += [{dkey: v, dvalue: k}] + + if self.state in ["replaced", "deleted"] and hx: + self.addcmd(have, x, negate=True) + if wx: + self.addcmd(want, x) + + def _dict_copy_merged(self, want, have, x=""): + hrec = {} + have_dict = have if x == "" else get_from_dict(have, x) + want_dict = want if x == "" else get_from_dict(want, x) + for k, wx in iteritems(want_dict): + if k not in have_dict: + hrec.update({k: wx}) + + for k, hx in iteritems(have_dict): + dstr = k if x == "" else x + "." + k + wx = get_from_dict(want, dstr) + if wx is None: + hrec.update({k: hx}) + continue + if isinstance(wx, dict): + hrec.update({k: self._dict_copy_merged(want, have, dstr)}) + else: + if dstr in self.mst_parsers: + wmode = get_from_dict(want, "mode") + hmode = get_from_dict(have, "mode") + if not ((wmode is None and hmode == "mst") or wmode == "mst"): + self._module.fail_json( + msg="mst options like simulate_pvst_global, hello_time, forward_time, " + "max_age, max_hops and priority cannot be used until [spanning-tree " + "mode mst] is enabled or already configured in device!", + ) + continue + if not isinstance(wx, list): + hrec.update({k: wx if wx is not None else hx}) + else: + cmp_list = [] + if dstr in self.complex_parsers: + cmp_list = self._compare_lists("vlan_list", "value", wx, hx) + elif dstr == "mst.priority": + cmp_list = self._compare_lists("instance", "value", wx, hx) + elif dstr == "mst.configuration.instances": + cmp_list = self._compare_lists("vlan_list", "instance", wx, hx) + if cmp_list: + hrec.update({k: cmp_list}) + return hrec + + def _dict_copy_deleted(self, want, have, x=""): + hrec = {} + have_dict = have if x == "" else get_from_dict(have, x) + for k, hx in iteritems(have_dict): + if not want: + hrec.update({k: hx}) + continue + dstr = k if x == "" else x + "." + k + wx = get_from_dict(want, dstr) + if wx is None: + continue + if dstr == "mst.configuration" and wx == hx: + hrec.update({k: {}}) + continue + if isinstance(wx, dict): + hrec.update({k: self._dict_copy_deleted(want, have, dstr)}) + else: + if dstr in self.mst_parsers: + wmode = get_from_dict(want, "mode") + if wmode == "mst": + continue + if not isinstance(wx, list): + if wx == hx: + hrec.update({k: hx}) + else: + cmp_list = [] + if dstr in self.complex_parsers: + cmp_list = self._compare_lists("vlan_list", "value", wx, hx) + elif dstr == "mst.priority": + cmp_list = self._compare_lists("instance", "value", wx, hx) + elif dstr == "mst.configuration.instances": + cmp_list = self._compare_lists("vlan_list", "instance", wx, hx) + if cmp_list: + hrec.update({k: cmp_list}) + return hrec + + def _compare_lists(self, dkey, dvalue, want, have): + num_list = [] + + wparams = {} + for each in want: + if each[dvalue] not in wparams: + wparams.update({each[dvalue]: set()}) + num_set = set(self._str_to_num_list(each[dkey])) + wparams[each[dvalue]].update(num_set) + + hparams = {} + for each in have: + if each[dvalue] not in hparams: + hparams.update({each[dvalue]: set()}) + num_set = set(self._str_to_num_list(each[dkey])) + hparams[each[dvalue]].update(num_set) + + if self.state == "merged": + for k in set(wparams.keys()).union(set(hparams.keys())): + if k in wparams and k in hparams: + diff_list = sorted(list(wparams[k].union(hparams[k]))) + elif k in wparams: + diff_list = sorted(list(wparams[k])) + elif k in hparams: + diff_list = sorted(list(hparams[k])) + num_list += [{dkey: self._num_list_to_str(diff_list), dvalue: k}] + elif self.state == "deleted": + for k in wparams.keys(): + if k in hparams: + diff_list = sorted(list(wparams[k].intersection(hparams[k]))) + if diff_list: + num_list += [{dkey: self._num_list_to_str(diff_list), dvalue: k}] + + return num_list + + def _str_to_num_list(self, num_str): + num_list = [] + for each in num_str.split(","): + block = each.split("-") + if len(block) > 1: + num_list += list(range(int(block[0]), int(block[1]) + 1)) + else: + num_list.append(int(block[0])) + return sorted(num_list) + + def _num_list_to_str(self, num_list): + seq = [] + out_list = [] + last = 0 + for index, val in enumerate(num_list): + if last + 1 == val or index == 0: + seq.append(val) + last = val + else: + if len(seq) > 1: + out_list.append(str(seq[0]) + "-" + str(seq[len(seq) - 1])) + else: + out_list.append(str(seq[0])) + seq = [] + seq.append(val) + last = val + if index == len(num_list) - 1: + if len(seq) > 1: + out_list.append(str(seq[0]) + "-" + str(seq[len(seq) - 1])) + else: + out_list.append(str(seq[0])) + return ",".join(map(str, out_list)) diff --git a/plugins/module_utils/network/ios/facts/facts.py b/plugins/module_utils/network/ios/facts/facts.py index b5c74021b..c18e51299 100644 --- a/plugins/module_utils/network/ios/facts/facts.py +++ b/plugins/module_utils/network/ios/facts/facts.py @@ -99,6 +99,9 @@ from ansible_collections.cisco.ios.plugins.module_utils.network.ios.facts.snmp_server.snmp_server import ( Snmp_serverFacts, ) +from ansible_collections.cisco.ios.plugins.module_utils.network.ios.facts.spanning_tree.spanning_tree import ( + Spanning_treeFacts, +) from ansible_collections.cisco.ios.plugins.module_utils.network.ios.facts.static_routes.static_routes import ( Static_routesFacts, ) @@ -159,6 +162,7 @@ vrf_global=Vrf_globalFacts, vrf_interfaces=Vrf_interfacesFacts, hsrp_interfaces=Hsrp_interfacesFacts, + spanning_tree=Spanning_treeFacts, ) diff --git a/plugins/module_utils/network/ios/facts/spanning_tree/__init__.py b/plugins/module_utils/network/ios/facts/spanning_tree/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/network/ios/facts/spanning_tree/spanning_tree.py b/plugins/module_utils/network/ios/facts/spanning_tree/spanning_tree.py new file mode 100644 index 000000000..e45597580 --- /dev/null +++ b/plugins/module_utils/network/ios/facts/spanning_tree/spanning_tree.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# Copyright 2023 Timur Nizharadze (@tnizharadze) +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The ios spanning_tree fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils + +from ansible_collections.cisco.ios.plugins.module_utils.network.ios.argspec.spanning_tree.spanning_tree import ( + Spanning_treeArgs, +) +from ansible_collections.cisco.ios.plugins.module_utils.network.ios.rm_templates.spanning_tree import ( + Spanning_treeTemplate, +) + + +class Spanning_treeFacts(object): + """The ios spanning_tree facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Spanning_treeArgs.argument_spec + + spec = deepcopy(self.argument_spec) + if subspec: + if options: + facts_argument_spec = spec[subspec][options] + else: + facts_argument_spec = spec[subspec] + else: + facts_argument_spec = spec + + self.generated_spec = utils.generate_dict(facts_argument_spec) + + def get_spanning_tree_data(self, connection): + return connection.get("show running-config | section ^spanning-tree|^no spanning-tree") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Spanning_tree network resource + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + + :rtype: dictionary + :returns: facts + """ + facts = {} + objs = [] + if not data: + data = self.get_spanning_tree_data(connection) + + # parse native config using the Spanning_tree template + spanning_tree_parser = Spanning_treeTemplate(lines=data.splitlines(), module=self._module) + objs = spanning_tree_parser.parse() + + ansible_facts["ansible_network_resources"].pop("spanning_tree", None) + + params = utils.remove_empties( + spanning_tree_parser.validate_config(self.argument_spec, {"config": objs}, redact=True), + ) + + facts["spanning_tree"] = params.get("config", {}) + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts diff --git a/plugins/module_utils/network/ios/rm_templates/spanning_tree.py b/plugins/module_utils/network/ios/rm_templates/spanning_tree.py new file mode 100644 index 000000000..7ae5cc970 --- /dev/null +++ b/plugins/module_utils/network/ios/rm_templates/spanning_tree.py @@ -0,0 +1,535 @@ +# -*- coding: utf-8 -*- +# Copyright 2023 Timur Nizharadze (@tnizharadze) +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The Spanning_tree parser templates file. This contains +a list of parser definitions and associated functions that +facilitates both facts gathering and native command generation for +the given network resource. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +def _tmplt_spanning_tree_mst_priority(data): + cmd = [] + for each in data["mst"]["priority"]: + cmd.append("spanning-tree mst {instance} priority {value}".format(**each)) + return cmd + + +def _tmplt_spanning_tree_mst_config_instances(data): + cmd = [] + for each in data["mst"]["configuration"]["instances"]: + cmd.append("instance {instance} vlan {vlan_list}".format(**each)) + return cmd + + +def _tmplt_spanning_tree_priority(data): + cmd = [] + for each in data["priority"]: + cmd.append("spanning-tree vlan {vlan_list} priority {value}".format(**each)) + return cmd + + +def _tmplt_spanning_tree_max_age(data): + cmd = [] + for each in data["max_age"]: + cmd.append("spanning-tree vlan {vlan_list} max-age {value}".format(**each)) + return cmd + + +def _tmplt_spanning_tree_hello_time(data): + cmd = [] + for each in data["hello_time"]: + cmd.append("spanning-tree vlan {vlan_list} hello-time {value}".format(**each)) + return cmd + + +def _tmplt_spanning_tree_forward_time(data): + cmd = [] + for each in data["forward_time"]: + cmd.append("spanning-tree vlan {vlan_list} forward-time {value}".format(**each)) + return cmd + + +class Spanning_treeTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + super(Spanning_treeTemplate, self).__init__(lines=lines, tmplt=self, module=module) + + # fmt: off + PARSERS = [ + { + "name": "backbonefast", + "getval": re.compile( + r""" + (spanning-tree\s(?Pbackbonefast))? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree backbonefast", + "result": { + "backbonefast": "{{ not not backbonefast }}", + }, + }, + { + "name": "bridge_assurance", + "getval": re.compile( + r""" + ((?Pno\s)?spanning-tree\sbridge\s(?Passurance))? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree bridge assurance", + "result": { + "bridge_assurance": "{{ False if negated is defined else (not not bridge_assurance) }}", + }, + }, + { + "name": "etherchannel_guard_misconfig", + "getval": re.compile( + r""" + ((?Pno\s)?spanning-tree\setherchannel\sguard\s(?Pmisconfig))? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree etherchannel guard misconfig", + "result": { + "etherchannel_guard_misconfig": "{{ False if negated is defined else (not not etherchannel_guard_misconfig) }}", + }, + }, + { + "name": "logging", + "getval": re.compile( + r""" + (spanning-tree\s(?Plogging))? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree logging", + "result": { + "logging": "{{ not not logging }}", + }, + }, + { + "name": "loopguard_default", + "getval": re.compile( + r""" + (spanning-tree\sloopguard\s(?Pdefault))? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree loopguard default", + "result": { + "loopguard_default": "{{ not not loopguard_default }}", + }, + }, + { + "name": "mode", + "getval": re.compile( + r""" + (spanning-tree\smode\s(?Pmst|pvst|rapid-pvst))? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree mode {{ mode }}", + "result": { + "mode": "{{ mode }}", + }, + }, + { + "name": "pathcost_method", + "getval": re.compile( + r""" + (spanning-tree\spathcost\smethod\s(?Plong|short))? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree pathcost method {{ pathcost_method }}", + "result": { + "pathcost_method": "{{ pathcost_method }}", + }, + }, + { + "name": "transmit_hold_count", + "getval": re.compile( + r""" + (spanning-tree\stransmit\shold-count\s(?P\d+))? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree transmit hold-count {{ transmit_hold_count }}", + "result": { + "transmit_hold_count": "{{ transmit_hold_count }}", + }, + }, + { + "name": "portfast.default", + "getval": re.compile( + r""" + (spanning-tree\sportfast\s(?Pdefault))? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree portfast default", + "result": { + "portfast": { + "default": "{{ not not default }}", + }, + }, + }, + { + "name": "portfast.network_default", + "getval": re.compile( + r""" + (spanning-tree\sportfast\snetwork\s(?Pdefault))? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree portfast network default", + "result": { + "portfast": { + "network_default": "{{ not not network_default }}", + }, + }, + }, + { + "name": "portfast.edge_default", + "getval": re.compile( + r""" + (spanning-tree\sportfast\sedge\s(?Pdefault))? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree portfast edge default", + "result": { + "portfast": { + "edge_default": "{{ not not edge_default }}", + }, + }, + }, + { + "name": "portfast.edge_bpdufilter_default", + "getval": re.compile( + r""" + (spanning-tree\sportfast\sedge\sbpdufilter\s(?Pdefault))? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree portfast edge bpdufilter default", + "result": { + "portfast": { + "edge_bpdufilter_default": "{{ not not edge_bpdufilter_default }}", + }, + }, + }, + { + "name": "portfast.bpdufilter_default", + "getval": re.compile( + r""" + (spanning-tree\sportfast\sbpdufilter\s(?Pdefault))? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree portfast bpdufilter default", + "result": { + "portfast": { + "bpdufilter_default": "{{ not not bpdufilter_default }}", + }, + }, + }, + { + "name": "portfast.edge_bpduguard_default", + "getval": re.compile( + r""" + (spanning-tree\sportfast\sedge\sbpduguard\s(?Pdefault))? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree portfast edge bpduguard default", + "result": { + "portfast": { + "edge_bpduguard_default": "{{ not not edge_bpduguard_default }}", + }, + }, + }, + { + "name": "portfast.bpduguard_default", + "getval": re.compile( + r""" + (spanning-tree\sportfast\sbpduguard\s(?Pdefault))? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree portfast bpduguard default", + "result": { + "portfast": { + "bpduguard_default": "{{ not not bpduguard_default }}", + }, + }, + }, + { + "name": "uplinkfast.enabled", + "getval": re.compile( + r""" + (spanning-tree\s(?Puplinkfast)$)? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree uplinkfast", + "result": { + "uplinkfast": { + "enabled": "{{ not not enabled }}", + }, + }, + }, + { + "name": "uplinkfast.max_update_rate", + "getval": re.compile( + r""" + (spanning-tree\suplinkfast\smax-update-rate\s(?P\d+))? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree uplinkfast max-update-rate {{ uplinkfast.max_update_rate }}", + "result": { + "uplinkfast": { + "max_update_rate": "{{ max_update_rate }}", + }, + }, + }, + { + "name": "forward_time", + "getval": re.compile( + r""" + spanning-tree\svlan\s(?P[0-9,\,\-]+)\sforward-time\s(?P\d+) + \s* + $""", re.VERBOSE, + ), + "setval": _tmplt_spanning_tree_forward_time, + "result": { + "forward_time": [{ + "vlan_list": "'{{ vlan_list }}'", + "value": "{{ value }}", + }], + }, + }, + { + "name": "hello_time", + "getval": re.compile( + r""" + spanning-tree\svlan\s(?P[0-9,\,\-]+)\shello-time\s(?P\d+) + \s* + $""", re.VERBOSE, + ), + "setval": _tmplt_spanning_tree_hello_time, + "result": { + "hello_time": [{ + "vlan_list": "'{{ vlan_list }}'", + "value": "{{ value }}", + }], + }, + }, + { + "name": "max_age", + "getval": re.compile( + r""" + spanning-tree\svlan\s(?P[0-9,\,\-]+)\smax-age\s(?P\d+) + \s* + $""", re.VERBOSE, + ), + "setval": _tmplt_spanning_tree_max_age, + "result": { + "max_age": [{ + "vlan_list": "'{{ vlan_list }}'", + "value": "{{ value }}", + }], + }, + }, + { + "name": "priority", + "getval": re.compile( + r""" + spanning-tree\svlan\s(?P[0-9,\,\-]+)\spriority\s(?P\d+) + \s* + $""", re.VERBOSE, + ), + "setval": _tmplt_spanning_tree_priority, + "result": { + "priority": [{ + "vlan_list": "'{{ vlan_list }}'", + "value": "{{ value }}", + }], + }, + }, + { + "name": "mst.simulate_pvst_global", + "getval": re.compile( + r""" + ((?Pno\s)?spanning-tree\smst\ssimulate\spvst\s(?Pglobal))? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree mst simulate pvst global", + "result": { + "mst": { + "simulate_pvst_global": "{{ False if negated is defined else (not not simulate_pvst_global) }}", + }, + }, + }, + { + "name": "mst.hello_time", + "getval": re.compile( + r""" + (spanning-tree\smst\shello-time\s(?P\d+))? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree mst hello-time {{ mst.hello_time }}", + "result": { + "mst": { + "hello_time": "{{ hello_time }}", + }, + }, + }, + { + "name": "mst.forward_time", + "getval": re.compile( + r""" + (spanning-tree\smst\sforward-time\s(?P\d+))? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree mst forward-time {{ mst.forward_time }}", + "result": { + "mst": { + "forward_time": "{{ forward_time }}", + }, + }, + }, + { + "name": "mst.max_age", + "getval": re.compile( + r""" + (spanning-tree\smst\smax-age\s(?P\d+))? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree mst max-age {{ mst.max_age }}", + "result": { + "mst": { + "max_age": "{{ max_age }}", + }, + }, + }, + { + "name": "mst.max_hops", + "getval": re.compile( + r""" + (spanning-tree\smst\smax-hops\s(?P\d+))? + \s* + $""", re.VERBOSE, + ), + "setval": "spanning-tree mst max-hops {{ mst.max_hops }}", + "result": { + "mst": { + "max_hops": "{{ max_hops }}", + }, + }, + }, + { + "name": "mst.priority", + "getval": re.compile( + r""" + (spanning-tree\smst\s(?P[0-9,\,\-]+)\spriority\s(?P\d+))? + \s* + $""", re.VERBOSE, + ), + "setval": _tmplt_spanning_tree_mst_priority, + "result": { + "mst": { + "priority": [{ + "instance": "'{{ instance }}'", + "value": "{{ value }}", + }], + }, + }, + }, + { + "name": "mst.configuration", + "getval": re.compile( + r""" + (spanning-tree\smst\s(?Pconfiguration)$)? + $""", re.VERBOSE, + ), + "setval": "{{ 'spanning-tree mst configuration' if mst.configuration is defined else '' }}", + "result": { + "mst": { + "{{ enabled }}": {}, + }, + }, + }, + { + "name": "mst.configuration.name", + "getval": re.compile( + r""" + (\sname\s(?P\S+))? + $""", re.VERBOSE, + ), + "setval": "name {{ mst.configuration.name }}", + "result": { + "mst": { + "configuration": { + "name": "{{ name }}", + }, + }, + }, + }, + { + "name": "mst.configuration.revision", + "getval": re.compile( + r""" + (\srevision\s(?P\d+))? + $""", re.VERBOSE, + ), + "setval": "revision {{ mst.configuration.revision }}", + "result": { + "mst": { + "configuration": { + "revision": "{{ revision }}", + }, + }, + }, + }, + { + "name": "mst.configuration.instances", + "getval": re.compile( + r""" + (\sinstance\s(?P\d+)\svlan\s(?P[0-9,\,\-]+))? + $""", re.VERBOSE, + ), + "setval": _tmplt_spanning_tree_mst_config_instances, + "result": { + "mst": { + "configuration": { + "instances": [{ + "instance": "{{ instance }}", + "vlan_list": "'{{ vlan_list }}'", + }], + }, + }, + }, + }, + ] + # fmt: on diff --git a/plugins/modules/ios_spanning_tree.py b/plugins/modules/ios_spanning_tree.py new file mode 100644 index 000000000..759884f76 --- /dev/null +++ b/plugins/modules/ios_spanning_tree.py @@ -0,0 +1,1008 @@ +#!/usr/bin/python +# +# 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 . +# +""" +The module file for ios_spanning_tree +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: ios_spanning_tree +short_description: Resource module to configure Spanning Tree. +description: + - This module provides declarative management of Spanning tree on Cisco IOS network devices. +version_added: 1.0.0 +author: Timur Nizharadze (@tnizharadze) +notes: + - Tested against Cisco IOS Version 17.3 on CML. + - This module works with connection C(network_cli). + See U(https://docs.ansible.com/ansible/latest/network/user_guide/platform_ios.html) +options: + config: + description: A dictionary of spanning tree options. + type: dict + suboptions: + backbonefast: + description: + - Use the spanning-tree backbonefast global configuration command on the switch + - stack or on a standalone switch to enable the BackboneFast feature. + type: bool + bridge_assurance: + description: + - Enables Bridge Assurance on all network ports on the switch. + - Bridge Assurance is enabled by default. + type: bool + etherchannel_guard_misconfig: + description: + - Enable EtherChannel guard to detect an EtherChannel misconfiguration if your + - switch is running PVST+, Rapid PVST+, or MSTP. Enabled by default. + type: bool + logging: + description: + - Enable logging of spanning-tree changes. + type: bool + loopguard_default: + description: + - To enable loop guard as a default on all ports of a given bridge + type: bool + mode: + description: + - To switch between Per-VLAN Spanning Tree+ (PVST+), Rapid-PVST+, and Multiple + - Spanning Tree (MST) modes. + type: str + choices: ["mst", "pvst", "rapid-pvst"] + pathcost_method: + description: + - To set the default path-cost calculation method. + - The long path-cost calculation method utilizes all 32 bits for path-cost + - calculation and yields values in the range of 1 through 200,000,000. + - The short path-cost calculation method (16 bits) yields values in the range + - of 1 through 65535. + type: str + choices: ["long", "short"] + transmit_hold_count: + description: + - Number of bridge protocol data units (BPDUs) that can be sent before pausing + - for 1 second. The range is from 1 to 20. + type: int + portfast: + description: Portfast configurations. + type: dict + suboptions: + default: + description: + - Immediately brings an STP port configured as an access or trunk port to the forwarding state. + type: bool + network_default: + description: + - Enables PortFast network mode by default on all switch access ports. Command for old IOS support. + type: bool + edge_default: + description: + - Enables PortFast edge mode by default on all switch access ports. Command for old IOS support. + type: bool + bpdufilter_default: + description: + - Enables PortFast BPDU filter by default on all PortFast ports. + type: bool + edge_bpdufilter_default: + description: + - Enables PortFast edge BPDU filter by default on all PortFast edge ports. Command for old IOS support. + type: bool + bpduguard_default: + description: + - Enables PortFast BPDU guard by default on all PortFast ports. + type: bool + edge_bpduguard_default: + description: + - Enables PortFast edge BPDU guard by default on all PortFast edge ports. Command for old IOS support. + type: bool + uplinkfast: + description: UplinkFast feature + type: dict + suboptions: + enabled: + description: + - Use to to enable UplinkFast + type: bool + max_update_rate: + description: + - Set the rate at which update packets are sent. The range is from 0 to 32000 + type: int + forward_time: + description: Sets the STP forward delay time. + type: list + elements: dict + suboptions: + vlan_list: + description: List of VLAN identification numbers. The range is from 1 to 4094. + type: str + value: + description: The range is from 4 to 30 seconds + type: int + hello_time: + description: + - Specifies the duration, in seconds, between the generation of configuration messages + - by the root switch. + type: list + elements: dict + suboptions: + vlan_list: + description: List of VLAN identification numbers. The range is from 1 to 4094. + type: str + value: + description: The range is from 1 to 10 seconds + type: int + max_age: + description: + - Sets the maximum number of seconds the information in a bridge packet data unit (BPDU) + - is valid. + type: list + elements: dict + suboptions: + vlan_list: + description: List of VLAN identification numbers. The range is from 1 to 4094. + type: str + value: + description: The range is from 6 to 40 seconds + type: int + priority: + description: + - Sets the STP bridge priority. + type: list + elements: dict + suboptions: + vlan_list: + description: List of VLAN identification numbers. The range is from 1 to 4094. + type: str + value: + description: Bridge priority in increments of 4096 + type: int + choices: [0, 4096, 8192, 12288, 16384, 20480, 24576, 28672, 32768, 36864, 40960, 45056, 49152, 53248, 57344, 61440] + mst: + description: + - Option for multiple spanning-tree global configurations. + type: dict + suboptions: + simulate_pvst_global: + description: + - Set to enable Per-VLAN Spanning Tree (PVST) simulation globally. + - PVST simulation is enabled by default so that all interfaces on the device interoperate between + - Multiple Spanning Tree (MST) and Rapid Per-VLAN Spanning Tree Plus (PVST+). To prevent an accidental + - connection to a device that does not run MST as the default Spanning Tree Protocol (STP) mode, + - you can disable PVST simulation. + type: bool + hello_time: + description: + - Specifies the duration, in seconds, between the generation of configuration messages + - by the root switch. The range is from 1 to 10 seconds. + type: int + forward_time: + description: Sets the STP forward delay time. The range is from 4 to 30 seconds. + type: int + max_age: + description: + - Sets the maximum number of seconds the information in a bridge packet data unit (BPDU) + - is valid. The range is from 6 to 40 seconds. + type: int + max_hops: + description: + - Number of possible hops in the region before a BPDU is discarded; valid values are from 1 to 255 hops. + type: int + priority: + description: + - Sets the MST instance priority. + type: list + elements: dict + suboptions: + instance: + description: List of MST instances. + type: str + value: + description: STP priority value. + type: int + choices: [0, 4096, 8192, 12288, 16384, 20480, 24576, 28672, 32768, 36864, 40960, 45056, 49152, 53248, 57344, 61440] + configuration: + description: Options for multiple spanning-tree region configuration. + type: dict + suboptions: + name: + description: Sets the name of an MST region. + type: str + revision: + description: Sets the revision number for the MST configuration. + type: int + instances: + description: List of configured instances. + type: list + elements: dict + suboptions: + instance: + description: MST instance number. + type: int + vlan_list: + description: List of VLANs assosiated to MST instance. + type: str + running_config: + description: + - This option is used only with state I(parsed). + type: str + state: + description: + - The state the configuration should be left in + - The states I(rendered), I(gathered) and I(parsed) does not perform any change + on the device. + - The state I(rendered) will transform the configuration in C(config) option to + platform specific CLI commands which will be returned in the I(rendered) key + within the result. For state I(rendered) active connection to remote host is + not required. + - The state I(gathered) will fetch the running configuration from device and transform + it into structured data in the format as per the resource module argspec and + the value is returned in the I(gathered) key within the result. + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into JSON format as per the resource module parameters and the + value is returned in the I(parsed) key within the result. The value of C(running_config) + option should be the same format as the output of command I(show running-config + | section ^spanning-tree|^no spanning-tree) executed on device. For state I(parsed) active + connection to remote host is not required. + type: str + choices: + - merged + - replaced + - deleted + - rendered + - parsed + - gathered + default: merged +""" + +EXAMPLES = """ +# Using gathered + +# Before state: +# ------------- +# +# vios#show running-config | section ^spanning-tree|^no spanning-tree +# spanning-tree mode mst +# no spanning-tree bridge assurance +# spanning-tree transmit hold-count 5 +# spanning-tree loopguard default +# spanning-tree logging +# spanning-tree portfast default +# spanning-tree portfast bpduguard default +# spanning-tree portfast bpdufilter default +# no spanning-tree etherchannel guard misconfig +# spanning-tree extend system-id +# spanning-tree uplinkfast max-update-rate 32 +# spanning-tree uplinkfast +# spanning-tree backbonefast +# spanning-tree pathcost method long +# no spanning-tree mst simulate pvst global +# spanning-tree mst configuration +# name NAME +# revision 34 +# instance 1 vlan 40-50 +# instance 2 vlan 10-20 +# spanning-tree mst hello-time 4 +# spanning-tree mst forward-time 25 +# spanning-tree mst max-age 33 +# spanning-tree mst max-hops 33 +# spanning-tree mst 0 priority 12288 +# spanning-tree mst 1 priority 4096 +# spanning-tree mst 5,7-9 priority 57344 +# spanning-tree vlan 1,3-5,7,9-11 priority 24576 +# spanning-tree vlan 1,3,9 hello-time 4 +# spanning-tree vlan 4,6-8 hello-time 5 +# spanning-tree vlan 5 hello-time 6 +# spanning-tree vlan 1,7-20 forward-time 20 +# spanning-tree vlan 1-2,4-5 max-age 38 + +- name: Gather facts for spanning_tree + cisco.ios.ios_spanning_tree: + state: gathered + +# Task Output: +# ------------ +# +# gathered: +# backbonefast: true +# bridge_assurance: false +# etherchannel_guard_misconfig: false +# forward_time: +# - value: 20 +# vlan_list: 1,7-20 +# hello_time: +# - value: 4 +# vlan_list: 1,3,9 +# - value: 5 +# vlan_list: 4,6-8 +# - value: 6 +# vlan_list: '5' +# logging: true +# loopguard_default: true +# max_age: +# - value: 38 +# vlan_list: 1-2,4-5 +# mode: mst +# mst: +# configuration: +# instances: +# - instance: 1 +# vlan_list: 40-50 +# - instance: 2 +# vlan_list: 10-20 +# name: NAME +# revision: 34 +# forward_time: 25 +# hello_time: 4 +# max_age: 33 +# max_hops: 33 +# priority: +# - instance: '0' +# value: 12288 +# - instance: '1' +# value: 4096 +# - instance: 5,7-9 +# value: 57344 +# simulate_pvst_global: false +# pathcost_method: long +# portfast: +# bpdufilter_default: true +# bpduguard_default: true +# default: true +# priority: +# - value: 24576 +# vlan_list: 1,3-5,7,9-11 +# transmit_hold_count: 5 +# uplinkfast: +# enabled: true +# max_update_rate: 32 + + +# Using parsed + +# File: parsed.cfg +# ---------------- +# +# spanning-tree mode mst +# no spanning-tree bridge assurance +# spanning-tree transmit hold-count 5 +# spanning-tree loopguard default +# spanning-tree logging +# spanning-tree portfast default +# spanning-tree portfast bpduguard default +# spanning-tree portfast bpdufilter default +# no spanning-tree etherchannel guard misconfig +# spanning-tree extend system-id +# spanning-tree uplinkfast max-update-rate 32 +# spanning-tree uplinkfast +# spanning-tree backbonefast +# spanning-tree pathcost method long +# no spanning-tree mst simulate pvst global +# spanning-tree mst configuration +# name NAME +# revision 34 +# instance 1 vlan 40-50 +# instance 2 vlan 10-20 +# spanning-tree mst hello-time 4 +# spanning-tree mst forward-time 25 +# spanning-tree mst max-age 33 +# spanning-tree mst max-hops 33 +# spanning-tree mst 0 priority 12288 +# spanning-tree mst 1 priority 4096 +# spanning-tree mst 5,7-9 priority 57344 +# spanning-tree vlan 1,3-5,7,9-11 priority 24576 +# spanning-tree vlan 1,3,9 hello-time 4 +# spanning-tree vlan 4,6-8 hello-time 5 +# spanning-tree vlan 5 hello-time 6 +# spanning-tree vlan 1,7-20 forward-time 20 +# spanning-tree vlan 1-2,4-5 max-age 38 + +- name: Parse the commands for provided configuration + cisco.ios.ios_spanning_tree: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Task Output: +# ------------ +# +# parsed: +# backbonefast: true +# bridge_assurance: false +# etherchannel_guard_misconfig: false +# forward_time: +# - value: 20 +# vlan_list: 1,7-20 +# hello_time: +# - value: 4 +# vlan_list: 1,3,9 +# - value: 5 +# vlan_list: 4,6-8 +# - value: 6 +# vlan_list: '5' +# logging: true +# loopguard_default: true +# max_age: +# - value: 38 +# vlan_list: 1-2,4-5 +# mode: mst +# mst: +# configuration: +# instances: +# - instance: 1 +# vlan_list: 40-50 +# - instance: 2 +# vlan_list: 10-20 +# name: NAME +# revision: 34 +# forward_time: 25 +# hello_time: 4 +# max_age: 33 +# max_hops: 33 +# priority: +# - instance: '0' +# value: 12288 +# - instance: '1' +# value: 4096 +# - instance: 5,7-9 +# value: 57344 +# simulate_pvst_global: false +# pathcost_method: long +# portfast: +# bpdufilter_default: true +# bpduguard_default: true +# default: true +# priority: +# - value: 24576 +# vlan_list: 1,3-5,7,9-11 +# transmit_hold_count: 5 +# uplinkfast: +# enabled: true +# max_update_rate: 32 + +# Using Rendered + +- name: Rendered the provided configuration with the existing running configuration + cisco.ios.ios_spanning_tree: + state: rendered + config: + backbonefast: true + bridge_assurance: false + etherchannel_guard_misconfig: false + forward_time: + - value: 20 + vlan_list: '1,7-20' + hello_time: + - value: 4 + vlan_list: '1,3,9' + - value: 5 + vlan_list: '4,6-8' + - value: 6 + vlan_list: '5' + logging: true + loopguard_default: true + max_age: + - value: 38 + vlan_list: '1-2,4-5' + mode: mst + mst: + configuration: + instances: + - instance: 1 + vlan_list: 40-50 + - instance: 2 + vlan_list: 10-20 + name: NAME + revision: 34 + forward_time: 25 + hello_time: 4 + max_age: 33 + max_hops: 33 + priority: + - instance: '0' + value: 12288 + - instance: '1' + value: 4096 + - instance: '5,7-9' + value: 57344 + simulate_pvst_global: false + pathcost_method: long + portfast: + edge_bpdufilter_default: true + edge_bpduguard_default: true + edge_default: true + priority: + - value: 24576 + vlan_list: '1,3-5,7,9-11' + transmit_hold_count: 5 + uplinkfast: + enabled: true + max_update_rate: 32 + +# Task Output: +# ------------ +# +# rendered: +# - spanning-tree backbonefast +# - no spanning-tree bridge assurance +# - no spanning-tree etherchannel guard misconfig +# - spanning-tree logging +# - spanning-tree loopguard default +# - spanning-tree mode mst +# - spanning-tree pathcost method long +# - spanning-tree transmit hold-count 5 +# - spanning-tree portfast edge default +# - spanning-tree portfast edge bpdufilter default +# - spanning-tree portfast edge bpduguard default +# - spanning-tree uplinkfast +# - spanning-tree uplinkfast max-update-rate 32 +# - no spanning-tree mst simulate pvst global +# - spanning-tree vlan 1,7-20 forward-time 20 +# - spanning-tree vlan 5 hello-time 6 +# - spanning-tree vlan 4,6-8 hello-time 5 +# - spanning-tree vlan 1,3,9 hello-time 4 +# - spanning-tree vlan 1-2,4-5 max-age 38 +# - spanning-tree vlan 1,3-5,7,9-11 priority 24576 +# - spanning-tree mst hello-time 4 +# - spanning-tree mst forward-time 25 +# - spanning-tree mst max-age 33 +# - spanning-tree mst max-hops 33 +# - spanning-tree mst 5,7-9 priority 57344 +# - spanning-tree mst 1 priority 4096 +# - spanning-tree mst 0 priority 12288 +# - spanning-tree mst configuration +# - name NAME +# - revision 34 +# - instance 2 vlan 10-20 +# - instance 1 vlan 40-50 +# - exit + +# Using Merged +# Example #1 + +# Before state: +# ------------- +# +# vios#show running-config | section ^spanning-tree|^no spanning-tree +# spanning-tree mode rapid-pvst +# spanning-tree extend system-id + +- name: Merged the provided configuration with the existing running configuration + cisco.ios.ios_spanning_tree: + state: merged + config: + mst: + forward_time: 25 + hello_time: 4 + max_age: 33 + max_hops: 33 + +# No commands will be sent out because STP mode is not mst (neither want nor have) + +# After state: +# ------------- +# +# vios#show running-config | section ^spanning-tree|^no spanning-tree +# spanning-tree mode rapid-pvst +# spanning-tree extend system-id + +# Using Merged +# Example #2 + +# Before state: +# ------------- +# +# vios#show running-config | section ^spanning-tree|^no spanning-tree +# spanning-tree mode rapid-pvst +# spanning-tree extend system-id + +- name: Merged the provided configuration with the existing running configuration + cisco.ios.ios_spanning_tree: + state: merged + config: + mode: mst + mst: + forward_time: 25 + hello_time: 4 + max_age: 33 + max_hops: 33 + +# Task Output: +# ------------ +# +# commands: +# - spanning-tree mode mst +# - spanning-tree mst hello-time 4 +# - spanning-tree mst forward-time 25 +# - spanning-tree mst max-age 33 +# - spanning-tree mst max-hops 33 + +# After state: +# ------------- +# +# vios#show running-config | section ^spanning-tree|^no spanning-tree +# spanning-tree mode mst +# spanning-tree extend system-id +# spanning-tree mst hello-time 4 +# spanning-tree mst forward-time 25 +# spanning-tree mst max-age 33 +# spanning-tree mst max-hops 33 + +# Using Merged +# Example #3 + +# Before state: +# ------------- +# +# vios#show running-config | section ^spanning-tree|^no spanning-tree +# spanning-tree mode mst +# spanning-tree extend system-id +# spanning-tree mst configuration +# name NAME +# revision 34 +# instance 1 vlan 40-50 +# instance 2 vlan 10-20 +# spanning-tree mst 1 priority 4096 +# spanning-tree mst 5,7-9 priority 57344 + +- name: Merged the provided configuration with the existing running configuration + cisco.ios.ios_spanning_tree: + state: merged + config: + mst: + priority: + - instance: 1 + value: 4096 + - instance: '5-7,9' + value: 57344 + configuration: + instances: + - instance: 1 + vlan_list: 40-50 + - instance: 2 + vlan_list: 20-30 + +# Task Output: +# ------------ +# +# commands: +# - spanning-tree mst 6 priority 57344 +# - spanning-tree mst configuration +# - instance 2 vlan 21-30 +# - exit + +# After state: +# ------------- +# vios#show running-config | section ^spanning-tree|^no spanning-tree +# spanning-tree mode mst +# spanning-tree extend system-id +# spanning-tree mst configuration +# name NAME +# revision 34 +# instance 1 vlan 40-50 +# instance 2 vlan 10-30 +# spanning-tree mst 1 priority 4096 +# spanning-tree mst 5-9 priority 57344 + +# Using Replaced + +# Before state: +# ------------- +# +# vios#show running-config | section ^spanning-tree|^no spanning-tree +# spanning-tree mode mst +# no spanning-tree bridge assurance +# spanning-tree transmit hold-count 10 +# spanning-tree loopguard default +# spanning-tree portfast default +# spanning-tree portfast bpduguard default +# spanning-tree portfast bpdufilter default +# no spanning-tree etherchannel guard misconfig +# spanning-tree extend system-id +# spanning-tree uplinkfast max-update-rate 32 +# spanning-tree uplinkfast +# spanning-tree backbonefast +# spanning-tree pathcost method long +# no spanning-tree mst simulate pvst global +# spanning-tree mst configuration +# name NAME +# revision 34 +# instance 1 vlan 40-50 +# instance 2 vlan 10-20 +# spanning-tree mst hello-time 4 +# spanning-tree mst forward-time 25 +# spanning-tree mst max-age 33 +# spanning-tree mst max-hops 33 +# spanning-tree mst 0 priority 12288 +# spanning-tree mst 1 priority 4096 +# spanning-tree mst 5-9 priority 57344 +# spanning-tree vlan 1,3-5,7,9-11 priority 24576 +# spanning-tree vlan 1,3,9 hello-time 4 +# spanning-tree vlan 4,6-8 hello-time 5 +# spanning-tree vlan 5 hello-time 6 +# spanning-tree vlan 1,7-20 forward-time 20 +# spanning-tree vlan 1-2,4-5 max-age 38 + +- name: Replaced the provided configuration with the existing running configuration + cisco.ios.ios_spanning_tree: + state: replaced + config: + mode: rapid-pvst + logging: true + priority: + - value: 24576 + vlan_list: '1,3-5' + mst: + priority: + - instance: 7-9 + value: 57344 + +# provided mst configuration will be ignored since stp mode changed to rapid-pvst + +# Task Output: +# ------------ +# +# commands: +# no spanning-tree backbonefast +# spanning-tree bridge assurance +# spanning-tree etherchannel guard misconfig +# spanning-tree logging +# no spanning-tree loopguard default +# spanning-tree mode rapid-pvst +# no spanning-tree pathcost method long +# no spanning-tree transmit hold-count 10 +# no spanning-tree portfast default +# no spanning-tree portfast bpdufilter default +# no spanning-tree portfast bpduguard default +# no spanning-tree uplinkfast +# no spanning-tree uplinkfast max-update-rate 32 +# spanning-tree mst simulate pvst global +# no spanning-tree vlan 1,7-20 forward-time 20 +# no spanning-tree vlan 5 hello-time 6 +# no spanning-tree vlan 4,6-8 hello-time 5 +# no spanning-tree vlan 1,3,9 hello-time 4 +# no spanning-tree vlan 1-2,4-5 max-age 38 +# no spanning-tree vlan 7,9-11 priority 24576 +# no spanning-tree mst configuration + +# After state: +# ------------- +# +# vios#show running-config | section ^spanning-tree|^no spanning-tree +# spanning-tree mode rapid-pvst +# spanning-tree logging +# spanning-tree extend system-id +# spanning-tree vlan 1,3-5 priority 24576 + +# Using Deleted +# Example #1 + +# Before state: +# ------------- +# +# vios#show running-config | section ^spanning-tree|^no spanning-tree +# spanning-tree mode mst +# spanning-tree extend system-id +# spanning-tree mst configuration +# name NAME +# revision 34 +# instance 1 vlan 40-50 +# instance 2 vlan 10-20 + +- name: Delete the provided configuration from the existing running configuration + cisco.ios.ios_spanning_tree: + state: deleted + config: + mst: + configuration: + name: NAME + revision: 34 + instances: + - instance: 1 + vlan_list: 40-50 + +# Task Output: +# ------------ +# +# commands: +# - spanning-tree mst configuration +# - no name NAME +# - no revision 34 +# - no instance 1 vlan 40-50 +# - exit + +# After state: +# ------------- +# +# vios#show running-config | section ^spanning-tree|^no spanning-tree +# spanning-tree mode mst +# spanning-tree extend system-id +# spanning-tree mst configuration +# instance 2 vlan 10-20 + +# Using Deleted +# Example #2 + +# Before state: +# ------------- +# +# vios#show running-config | section ^spanning-tree|^no spanning-tree +# spanning-tree mode mst +# spanning-tree extend system-id +# spanning-tree mst configuration +# name NAME +# revision 34 +# instance 1 vlan 40-50 +# instance 2 vlan 10-20 + +- name: Delete the provided configuration from the existing running configuration + cisco.ios.ios_spanning_tree: + state: deleted + config: + mst: + configuration: + name: NAME + revision: 34 + instances: + - instance: 1 + vlan_list: 40-50 + - instance: 2 + vlan_list: 10-20 + +# Task Output: +# ------------ +# +# commands: +# - no spanning-tree mst configuration + +# After state: +# ------------- +# +# vios#show running-config | section ^spanning-tree|^no spanning-tree +# spanning-tree mode mst +# spanning-tree extend system-id + +# Using Deleted +# Example #3 + +# Before state: +# ------------- +# +# vios#show running-config | section ^spanning-tree|^no spanning-tree +# spanning-tree mode mst +# no spanning-tree bridge assurance +# no spanning-tree etherchannel guard misconfig +# spanning-tree extend system-id +# no spanning-tree mst simulate pvst global + +- name: Delete the provided configuration from the existing running configuration + cisco.ios.ios_spanning_tree: + state: deleted + config: + bridge_assurance: false + etherchannel_guard_misconfig: false + mst: + simulate_pvst_global: false + +# Task Output: +# ------------ +# +# commands: +# - spanning-tree bridge assurance +# - spanning-tree etherchannel guard misconfig +# - spanning-tree mst simulate pvst global + +# After state: +# ------------- +# +# vios#show running-config | section ^spanning-tree|^no spanning-tree +# spanning-tree mode mst +# spanning-tree extend system-id +""" + +RETURN = """ +before: + description: The configuration prior to the module execution. + returned: when I(state) is C(merged), C(replaced) or C(deleted) + type: dict + sample: > + This output will always be in the same format as the + module argspec. +after: + description: The resulting configuration after module execution. + returned: when changed + type: dict + sample: > + This output will always be in the same format as the + module argspec. +commands: + description: The set of commands pushed to the remote device. + returned: when I(state) is C(merged), C(replaced) or C(deleted) + type: list + sample: + - spanning-tree pathcost method long + - no spanning-tree mst simulate pvst global + - spanning-tree mst configuration + - name NAME + - revision 34 + - instance 1 vlan 40-50 + - exit +rendered: + description: The provided configuration in the task rendered in device-native format (offline). + returned: when I(state) is C(rendered) + type: list + sample: + - spanning-tree pathcost method long + - no spanning-tree mst simulate pvst global + - spanning-tree mst configuration + - name NAME + - revision 34 + - instance 1 vlan 40-50 + - exit +gathered: + description: Facts about the network resource gathered from the remote device as structured data. + returned: when I(state) is C(gathered) + type: list + sample: > + This output will always be in the same format as the + module argspec. +parsed: + description: The device native config provided in I(running_config) option parsed into structured data as per module argspec. + returned: when I(state) is C(parsed) + type: list + sample: > + This output will always be in the same format as the + module argspec. +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.ios.plugins.module_utils.network.ios.argspec.spanning_tree.spanning_tree import ( + Spanning_treeArgs, +) +from ansible_collections.cisco.ios.plugins.module_utils.network.ios.config.spanning_tree.spanning_tree import ( + Spanning_tree, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Spanning_treeArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Spanning_tree(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/ios_spanning_tree/defaults/main.yaml b/tests/integration/targets/ios_spanning_tree/defaults/main.yaml new file mode 100644 index 000000000..164afead2 --- /dev/null +++ b/tests/integration/targets/ios_spanning_tree/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/tests/integration/targets/ios_spanning_tree/meta/main.yaml b/tests/integration/targets/ios_spanning_tree/meta/main.yaml new file mode 100644 index 000000000..23d65c7ef --- /dev/null +++ b/tests/integration/targets/ios_spanning_tree/meta/main.yaml @@ -0,0 +1,2 @@ +--- +dependencies: [] diff --git a/tests/integration/targets/ios_spanning_tree/tasks/cli.yaml b/tests/integration/targets/ios_spanning_tree/tasks/cli.yaml new file mode 100644 index 000000000..6f505600c --- /dev/null +++ b/tests/integration/targets/ios_spanning_tree/tasks/cli.yaml @@ -0,0 +1,21 @@ +--- +- name: Collect all CLI test cases + ansible.builtin.find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}.yaml" + use_regex: true + register: test_cases + delegate_to: localhost + +- name: Set test_items + ansible.builtin.set_fact: + test_items: "{{ test_cases.files | map(attribute='path') | list }}" + delegate_to: localhost + +- name: Run test case (connection=ansible.netcommon.network_cli) + ansible.builtin.include_tasks: "{{ test_case_to_run }}" + vars: + ansible_connection: ansible.netcommon.network_cli + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/ios_spanning_tree/tasks/main.yaml b/tests/integration/targets/ios_spanning_tree/tasks/main.yaml new file mode 100644 index 000000000..447e7938d --- /dev/null +++ b/tests/integration/targets/ios_spanning_tree/tasks/main.yaml @@ -0,0 +1,5 @@ +--- +- name: Main task for route_maps module + ansible.builtin.include_tasks: cli.yaml + tags: + - network_cli diff --git a/tests/integration/targets/ios_spanning_tree/tests/cli/_parsed.cfg b/tests/integration/targets/ios_spanning_tree/tests/cli/_parsed.cfg new file mode 100644 index 000000000..55a72c7df --- /dev/null +++ b/tests/integration/targets/ios_spanning_tree/tests/cli/_parsed.cfg @@ -0,0 +1,32 @@ +spanning-tree mode mst +no spanning-tree bridge assurance +spanning-tree transmit hold-count 5 +spanning-tree loopguard default +spanning-tree logging +spanning-tree portfast default +spanning-tree portfast bpduguard default +spanning-tree portfast bpdufilter default +spanning-tree extend system-id +spanning-tree uplinkfast max-update-rate 32 +spanning-tree uplinkfast +spanning-tree backbonefast +spanning-tree pathcost method long +no spanning-tree mst simulate pvst global +spanning-tree mst configuration + name NAME + revision 34 + instance 1 vlan 40-50 + instance 2 vlan 10-20 +spanning-tree mst hello-time 4 +spanning-tree mst forward-time 25 +spanning-tree mst max-age 33 +spanning-tree mst max-hops 33 +spanning-tree mst 0 priority 12288 +spanning-tree mst 1 priority 4096 +spanning-tree mst 5,7-9 priority 57344 +spanning-tree vlan 1,3-5,7,9-11 priority 24576 +spanning-tree vlan 1,3,9 hello-time 4 +spanning-tree vlan 4,6-8 hello-time 5 +spanning-tree vlan 5 hello-time 6 +spanning-tree vlan 1,7-20 forward-time 20 +spanning-tree vlan 1-2,4-5 max-age 38 diff --git a/tests/integration/targets/ios_spanning_tree/tests/cli/_populate_config.yaml b/tests/integration/targets/ios_spanning_tree/tests/cli/_populate_config.yaml new file mode 100644 index 000000000..48979db6f --- /dev/null +++ b/tests/integration/targets/ios_spanning_tree/tests/cli/_populate_config.yaml @@ -0,0 +1,5 @@ +--- +- name: Populate spanning-tree configuration + cisco.ios.ios_spanning_tree: + config: "{{ parsed['config'] }}" + state: merged diff --git a/tests/integration/targets/ios_spanning_tree/tests/cli/_remove_config.yaml b/tests/integration/targets/ios_spanning_tree/tests/cli/_remove_config.yaml new file mode 100644 index 000000000..449ad85e3 --- /dev/null +++ b/tests/integration/targets/ios_spanning_tree/tests/cli/_remove_config.yaml @@ -0,0 +1,4 @@ +--- +- name: Remove all spanning-tree configuration + cisco.ios.ios_spanning_tree: + state: deleted diff --git a/tests/integration/targets/ios_spanning_tree/tests/cli/deleted.yaml b/tests/integration/targets/ios_spanning_tree/tests/cli/deleted.yaml new file mode 100644 index 000000000..7cc387b9d --- /dev/null +++ b/tests/integration/targets/ios_spanning_tree/tests/cli/deleted.yaml @@ -0,0 +1,49 @@ +--- +- ansible.builtin.debug: + msg: Start Deleted integration state for ios_spanning_tree ansible_connection={{ ansible_connection }} + +- block: + - ansible.builtin.include_tasks: _remove_config.yaml + - ansible.builtin.include_tasks: _populate_config.yaml + + - name: Delete provided spanning tree configuration + register: result + cisco.ios.ios_spanning_tree: &id001 + config: + forward_time: + - value: 20 + vlan_list: 4-9 + state: deleted + + - name: Assert that correct set of commands were generated + ansible.builtin.assert: + that: + - "{{ deleted['commands'] | symmetric_difference(result['commands']) | length == 0 }}" + + - name: Delete provided spanning tree configuration (idempotent) + register: result + cisco.ios.ios_spanning_tree: *id001 + - name: Assert that the previous task was idempotent + ansible.builtin.assert: + that: + - result.changed == false + + - name: Delete all provided spanning tree configuration + register: result + cisco.ios.ios_spanning_tree: &id002 + state: deleted + + - name: Assert that correct set of commands were generated + ansible.builtin.assert: + that: + - "{{ deleted['commands2'] | symmetric_difference(result['commands']) | length == 0 }}" + + - name: Delete all provided spanning tree configuration (idempotent) + register: result + cisco.ios.ios_spanning_tree: *id002 + - name: Assert that the previous task was idempotent + ansible.builtin.assert: + that: + - result.changed == false + always: + - ansible.builtin.include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/ios_spanning_tree/tests/cli/gathered.yaml b/tests/integration/targets/ios_spanning_tree/tests/cli/gathered.yaml new file mode 100644 index 000000000..af10a026c --- /dev/null +++ b/tests/integration/targets/ios_spanning_tree/tests/cli/gathered.yaml @@ -0,0 +1,21 @@ +--- +- ansible.builtin.debug: + msg: START ios_spanning_tree gathered integration tests on connection={{ ansible_connection }} + +- ansible.builtin.include_tasks: _remove_config.yaml +- ansible.builtin.include_tasks: _populate_config.yaml + +- block: + - name: Gather the existing running configuration + register: result + cisco.ios.ios_spanning_tree: + state: gathered + + - name: Assert + ansible.builtin.assert: + that: + - result.changed == false + - parsed['config'] == result['gathered'] + + always: + - ansible.builtin.include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/ios_spanning_tree/tests/cli/merged.yaml b/tests/integration/targets/ios_spanning_tree/tests/cli/merged.yaml new file mode 100644 index 000000000..f9787afd6 --- /dev/null +++ b/tests/integration/targets/ios_spanning_tree/tests/cli/merged.yaml @@ -0,0 +1,159 @@ +--- +- ansible.builtin.debug: + msg: START Merged ios_spanning_tree state for integration tests on connection={{ ansible_connection }} + +- ansible.builtin.include_tasks: _remove_config.yaml + +- block: + - name: Test 1. Merge provided spanning tree configuration with device configuration + register: result + cisco.ios.ios_spanning_tree: &id001 + config: + mst: + forward_time: 25 + hello_time: 4 + max_age: 33 + state: merged + + - name: Assert that the previous task has no changes made + ansible.builtin.assert: + that: + - result['changed'] == false + + - name: Test 2. Merge provided spanning tree configuration with device configuration + register: result + cisco.ios.ios_spanning_tree: + config: + mode: "mst" + mst: + forward_time: 25 + hello_time: 4 + max_age: 33 + state: merged + + - name: Assert that correct set of commands were generated + ansible.builtin.assert: + that: + - "{{ merged['commands'] | symmetric_difference(result['commands']) | length == 0 }}" + + - name: Assert that after dict is correctly generated + ansible.builtin.assert: + that: + - merged['after'] == result['after'] + + - name: Test 3. Merge provided spanning tree configuration with device configuration (idempotent) + register: result + cisco.ios.ios_spanning_tree: *id001 + + - name: Assert that the previous task was idempotent + ansible.builtin.assert: + that: + - result['changed'] == false + + - name: Test 4. Merge provided spanning tree configuration with device configuration + register: result + ignore_errors: true + cisco.ios.ios_spanning_tree: + config: + mode: "rapid-pvst" + mst: + forward_time: 25 + hello_time: 4 + max_age: 33 + state: merged + + - name: Assert that the previous task was idempotent + ansible.builtin.assert: + that: + - result['changed'] == false + + - name: Populate spanning tree configuration + register: result + cisco.ios.ios_spanning_tree: + config: + mode: mst + bridge_assurance: false + transmit_hold_count: 5 + logging: true + loopguard_default: true + portfast: + bpdufilter_default: true + default: true + uplinkfast: + enabled: true + max_update_rate: 32 + pathcost_method: long + hello_time: + - value: 4 + vlan_list: 1,3,9 + - value: 5 + vlan_list: 4,6-8 + mst: + forward_time: 25 + hello_time: 4 + max_age: 33 + max_hops: 33 + priority: + - instance: "0" + value: 12288 + - instance: "1" + value: 4096 + - instance: 5,7-9 + value: 57344 + configuration: + instances: + - instance: 1 + vlan_list: 40-50 + - instance: 2 + vlan_list: 10-20 + name: NAME + revision: 34 + state: merged + + - name: Assert that correct set of commands were generated + ansible.builtin.assert: + that: + - "{{ merged['commands2'] | symmetric_difference(result['commands']) | length == 0 }}" + + - name: Test 5. Merge provided spanning tree configuration with device configuration + register: result + cisco.ios.ios_spanning_tree: + config: + backbonefast: true + bridge_assurance: true + portfast: + bpdufilter_default: true + default: true + mst: + configuration: + instances: + - instance: 1 + vlan_list: 40-50 + - instance: 2 + vlan_list: 20-30 + name: NAME + revision: 35 + simulate_pvst_global: false + forward_time: 25 + hello_time: 4 + max_age: 33 + max_hops: 33 + priority: + - instance: "0" + value: 12288 + - instance: "1" + value: 4096 + - instance: 5-7,9 + value: 57344 + hello_time: + - value: 6 + vlan_list: 1-3,5-6 + state: merged + + - name: Assert that correct set of commands were generated + ansible.builtin.assert: + that: + - "{{ merged['commands3'] | symmetric_difference(result['commands']) | length == 0 }}" + + always: + - ansible.builtin.include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/ios_spanning_tree/tests/cli/parsed.yaml b/tests/integration/targets/ios_spanning_tree/tests/cli/parsed.yaml new file mode 100644 index 000000000..405b3352b --- /dev/null +++ b/tests/integration/targets/ios_spanning_tree/tests/cli/parsed.yaml @@ -0,0 +1,14 @@ +--- +- ansible.builtin.debug: + msg: START ios_spanning_tree parsed integration tests on connection={{ ansible_connection }} + +- name: Parse the commands for provided configuration + register: result + cisco.ios.ios_spanning_tree: + running_config: "{{ lookup('file', '_parsed.cfg') }}" + state: parsed + +- ansible.builtin.assert: + that: + - result.changed == false + - parsed['config'] == result['parsed'] diff --git a/tests/integration/targets/ios_spanning_tree/tests/cli/rendered.yaml b/tests/integration/targets/ios_spanning_tree/tests/cli/rendered.yaml new file mode 100644 index 000000000..a9284bdbb --- /dev/null +++ b/tests/integration/targets/ios_spanning_tree/tests/cli/rendered.yaml @@ -0,0 +1,20 @@ +--- +- ansible.builtin.debug: + msg: Start ios_spanning_tree rendered integration tests ansible_connection={{ ansible_connection }} + +- ansible.builtin.include_tasks: _remove_config.yaml + +- block: + - name: Render the commands for provided spanning tree configuration + register: result + cisco.ios.ios_spanning_tree: + config: "{{ parsed['config'] }}" + state: rendered + + - ansible.builtin.assert: + that: + - result.changed == false + - result.rendered|symmetric_difference(rendered.commands) == [] + + always: + - ansible.builtin.include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/ios_spanning_tree/tests/cli/replaced.yaml b/tests/integration/targets/ios_spanning_tree/tests/cli/replaced.yaml new file mode 100644 index 000000000..b494f31aa --- /dev/null +++ b/tests/integration/targets/ios_spanning_tree/tests/cli/replaced.yaml @@ -0,0 +1,48 @@ +--- +- ansible.builtin.debug: + msg: START Replaced ios_spanning_tree state for integration tests on connection={{ ansible_connection }} + +- ansible.builtin.include_tasks: _remove_config.yaml +- ansible.builtin.include_tasks: _populate_config.yaml + +- block: + - name: Replaced provided spanning tree configuration + register: result + cisco.ios.ios_spanning_tree: &id001 + config: + backbonefast: false + bridge_assurance: true + hello_time: + - value: 4 + vlan_list: 1,3,9 + - value: 5 + vlan_list: 4,6-8 + logging: false + loopguard_default: false + state: replaced + + - name: Assert that before dict is correctly generated + ansible.builtin.assert: + that: + - parsed['config'] == result['before'] + + - name: Assert that correct set of commands were generated + ansible.builtin.assert: + that: + - "{{ replaced['commands'] | symmetric_difference(result['commands']) | length == 0 }}" + + - name: Assert that after dict is correctly generated + ansible.builtin.assert: + that: + - replaced['after'] == result['after'] + + - name: Replace provided spanning tree configuration (idempotent) + register: result + cisco.ios.ios_spanning_tree: *id001 + + - name: Assert that task was idempotent + ansible.builtin.assert: + that: + - result['changed'] == false + always: + - ansible.builtin.include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/ios_spanning_tree/vars/main.yaml b/tests/integration/targets/ios_spanning_tree/vars/main.yaml new file mode 100644 index 000000000..a64fcae4b --- /dev/null +++ b/tests/integration/targets/ios_spanning_tree/vars/main.yaml @@ -0,0 +1,207 @@ +--- +merged: + commands: + - "spanning-tree mode mst" + - "spanning-tree mst hello-time 4" + - "spanning-tree mst forward-time 25" + - "spanning-tree mst max-age 33" + commands2: + - "no spanning-tree bridge assurance" + - "spanning-tree logging" + - "spanning-tree loopguard default" + - "spanning-tree pathcost method long" + - "spanning-tree transmit hold-count 5" + - "spanning-tree portfast default" + - "spanning-tree portfast bpdufilter default" + - "spanning-tree uplinkfast" + - "spanning-tree uplinkfast max-update-rate 32" + - "spanning-tree vlan 4,6-8 hello-time 5" + - "spanning-tree vlan 1,3,9 hello-time 4" + - "spanning-tree mst max-hops 33" + - "spanning-tree mst 5,7-9 priority 57344" + - "spanning-tree mst 1 priority 4096" + - "spanning-tree mst 0 priority 12288" + - "spanning-tree mst configuration" + - "name NAME" + - "revision 34" + - "instance 2 vlan 10-20" + - "instance 1 vlan 40-50" + - "exit" + commands3: + - "spanning-tree backbonefast" + - "spanning-tree bridge assurance" + - "no spanning-tree mst simulate pvst global" + - "spanning-tree vlan 1-3,5-6 hello-time 6" + - "spanning-tree mst 6 priority 57344" + - "spanning-tree mst configuration" + - "revision 35" + - "instance 2 vlan 21-30" + - "exit" + + after: + mode: "mst" + mst: + forward_time: 25 + hello_time: 4 + max_age: 33 + +rendered: + commands: + - "spanning-tree backbonefast" + - "no spanning-tree bridge assurance" + - "spanning-tree logging" + - "spanning-tree loopguard default" + - "spanning-tree mode mst" + - "spanning-tree pathcost method long" + - "spanning-tree transmit hold-count 5" + - "spanning-tree portfast default" + - "spanning-tree portfast bpdufilter default" + - "spanning-tree portfast bpduguard default" + - "spanning-tree uplinkfast" + - "spanning-tree uplinkfast max-update-rate 32" + - "no spanning-tree mst simulate pvst global" + - "spanning-tree vlan 1,7-20 forward-time 20" + - "spanning-tree vlan 5 hello-time 6" + - "spanning-tree vlan 4,6-8 hello-time 5" + - "spanning-tree vlan 1,3,9 hello-time 4" + - "spanning-tree vlan 1-2,4-5 max-age 38" + - "spanning-tree vlan 1,3-5,7,9-11 priority 24576" + - "spanning-tree mst hello-time 4" + - "spanning-tree mst forward-time 25" + - "spanning-tree mst max-age 33" + - "spanning-tree mst max-hops 33" + - "spanning-tree mst 5,7-9 priority 57344" + - "spanning-tree mst 1 priority 4096" + - "spanning-tree mst 0 priority 12288" + - "spanning-tree mst configuration" + - "name NAME" + - "revision 34" + - "instance 2 vlan 10-20" + - "instance 1 vlan 40-50" + - "exit" + +replaced: + commands: + - "no spanning-tree backbonefast" + - "spanning-tree bridge assurance" + - "no spanning-tree logging" + - "no spanning-tree loopguard default" + - "no spanning-tree mode mst" + - "no spanning-tree pathcost method long" + - "no spanning-tree transmit hold-count 5" + - "no spanning-tree portfast default" + - "no spanning-tree portfast bpdufilter default" + - "no spanning-tree portfast bpduguard default" + - "no spanning-tree uplinkfast" + - "no spanning-tree uplinkfast max-update-rate 32" + - "spanning-tree mst simulate pvst global" + - "no spanning-tree vlan 1,7-20 forward-time 20" + - "no spanning-tree vlan 5 hello-time 6" + - "no spanning-tree vlan 1-2,4-5 max-age 38" + - "no spanning-tree vlan 1,3-5,7,9-11 priority 24576" + - "no spanning-tree mst hello-time 4" + - "no spanning-tree mst forward-time 25" + - "no spanning-tree mst max-age 33" + - "no spanning-tree mst max-hops 33" + - "no spanning-tree mst 5,7-9 priority 57344" + - "no spanning-tree mst 1 priority 4096" + - "no spanning-tree mst 0 priority 12288" + - "no spanning-tree mst configuration" + after: + mode: "rapid-pvst" + hello_time: + - value: 4 + vlan_list: "1,3,9" + - value: 5 + vlan_list: "4,6-8" + +deleted: + commands: + - "no spanning-tree vlan 7-9 forward-time 20" + commands2: + - "no spanning-tree backbonefast" + - "spanning-tree bridge assurance" + - "no spanning-tree logging" + - "no spanning-tree loopguard default" + - "no spanning-tree mode mst" + - "no spanning-tree pathcost method long" + - "no spanning-tree transmit hold-count 5" + - "no spanning-tree portfast default" + - "no spanning-tree portfast bpdufilter default" + - "no spanning-tree portfast bpduguard default" + - "no spanning-tree uplinkfast" + - "no spanning-tree uplinkfast max-update-rate 32" + - "spanning-tree mst simulate pvst global" + - "no spanning-tree vlan 1,10-20 forward-time 20" + - "no spanning-tree vlan 5 hello-time 6" + - "no spanning-tree vlan 4,6-8 hello-time 5" + - "no spanning-tree vlan 1,3,9 hello-time 4" + - "no spanning-tree vlan 1-2,4-5 max-age 38" + - "no spanning-tree vlan 1,3-5,7,9-11 priority 24576" + - "no spanning-tree mst hello-time 4" + - "no spanning-tree mst forward-time 25" + - "no spanning-tree mst max-age 33" + - "no spanning-tree mst max-hops 33" + - "no spanning-tree mst 5,7-9 priority 57344" + - "no spanning-tree mst 1 priority 4096" + - "no spanning-tree mst 0 priority 12288" + - "spanning-tree mst configuration" + - "no name NAME" + - "no revision 34" + - "no instance 2 vlan 10-20" + - "no instance 1 vlan 40-50" + - "exit" + +parsed: + config: + backbonefast: true + bridge_assurance: false + forward_time: + - value: 20 + vlan_list: 1,7-20 + hello_time: + - value: 4 + vlan_list: 1,3,9 + - value: 5 + vlan_list: 4,6-8 + - value: 6 + vlan_list: "5" + logging: true + loopguard_default: true + max_age: + - value: 38 + vlan_list: 1-2,4-5 + mode: mst + mst: + configuration: + instances: + - instance: 1 + vlan_list: 40-50 + - instance: 2 + vlan_list: 10-20 + name: NAME + revision: 34 + forward_time: 25 + hello_time: 4 + max_age: 33 + max_hops: 33 + priority: + - instance: "0" + value: 12288 + - instance: "1" + value: 4096 + - instance: 5,7-9 + value: 57344 + simulate_pvst_global: false + pathcost_method: long + portfast: + bpdufilter_default: true + bpduguard_default: true + default: true + priority: + - value: 24576 + vlan_list: 1,3-5,7,9-11 + transmit_hold_count: 5 + uplinkfast: + enabled: true + max_update_rate: 32 diff --git a/tests/unit/modules/network/ios/test_ios_spanning_tree.py b/tests/unit/modules/network/ios/test_ios_spanning_tree.py new file mode 100644 index 000000000..4629957c6 --- /dev/null +++ b/tests/unit/modules/network/ios/test_ios_spanning_tree.py @@ -0,0 +1,920 @@ +# 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 . + +# Make coding more python3-ish +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from textwrap import dedent +from unittest.mock import patch + +from ansible_collections.cisco.ios.plugins.modules import ios_spanning_tree +from ansible_collections.cisco.ios.tests.unit.modules.utils import set_module_args + +from .ios_module import TestIosModule + + +class TestIosSpanningTreeModule(TestIosModule): + module = ios_spanning_tree + + def setUp(self): + super(TestIosSpanningTreeModule, self).setUp() + + self.mock_get_resource_connection_facts = patch( + "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module_base." + "get_resource_connection", + ) + self.get_resource_connection_facts = self.mock_get_resource_connection_facts.start() + + self.mock_execute_show_command = patch( + "ansible_collections.cisco.ios.plugins.module_utils.network.ios.facts.spanning_tree.spanning_tree." + "Spanning_treeFacts.get_spanning_tree_data", + ) + self.execute_show_command = self.mock_execute_show_command.start() + + def tearDown(self): + super(TestIosSpanningTreeModule, self).tearDown() + self.mock_get_resource_connection_facts.stop() + self.mock_execute_show_command.stop() + + def test_ios_spanning_tree_gathered(self): + self.execute_show_command.return_value = dedent( + """\ + spanning-tree mode mst + no spanning-tree bridge assurance + spanning-tree transmit hold-count 5 + spanning-tree loopguard default + spanning-tree logging + spanning-tree portfast edge default + spanning-tree portfast edge bpduguard default + spanning-tree portfast edge bpdufilter default + spanning-tree extend system-id + spanning-tree uplinkfast max-update-rate 32 + spanning-tree uplinkfast + spanning-tree backbonefast + spanning-tree pathcost method long + no spanning-tree mst simulate pvst global + spanning-tree mst configuration + name NAME + revision 34 + instance 1 vlan 40-50 + instance 2 vlan 10-20 + spanning-tree mst hello-time 4 + spanning-tree mst forward-time 25 + spanning-tree mst max-age 33 + spanning-tree mst max-hops 33 + spanning-tree mst 0 priority 12288 + spanning-tree mst 1 priority 4096 + spanning-tree mst 5,7-9 priority 57344 + spanning-tree vlan 1,3-5,7,9-11 priority 24576 + spanning-tree vlan 1,3,9 hello-time 4 + spanning-tree vlan 4,6-8 hello-time 5 + spanning-tree vlan 5 hello-time 6 + spanning-tree vlan 1,7-20 forward-time 20 + spanning-tree vlan 1-2,4-5 max-age 38 + """, + ) + gathered = { + "backbonefast": True, + "bridge_assurance": False, + "forward_time": [ + { + "value": 20, + "vlan_list": "1,7-20", + }, + ], + "hello_time": [ + { + "value": 4, + "vlan_list": "1,3,9", + }, + { + "value": 5, + "vlan_list": "4,6-8", + }, + { + "value": 6, + "vlan_list": "5", + }, + ], + "logging": True, + "loopguard_default": True, + "max_age": [ + { + "value": 38, + "vlan_list": "1-2,4-5", + }, + ], + "mode": "mst", + "mst": { + "configuration": { + "instances": [ + { + "instance": 1, + "vlan_list": "40-50", + }, + { + "instance": 2, + "vlan_list": "10-20", + }, + ], + "name": "NAME", + "revision": 34, + }, + "forward_time": 25, + "hello_time": 4, + "max_age": 33, + "max_hops": 33, + "priority": [ + { + "instance": "0", + "value": 12288, + }, + { + "instance": "1", + "value": 4096, + }, + { + "instance": "5,7-9", + "value": 57344, + }, + ], + "simulate_pvst_global": False, + }, + "pathcost_method": "long", + "portfast": { + "edge_bpdufilter_default": True, + "edge_bpduguard_default": True, + "edge_default": True, + }, + "priority": [ + { + "value": 24576, + "vlan_list": "1,3-5,7,9-11", + }, + ], + "transmit_hold_count": 5, + "uplinkfast": { + "enabled": True, + "max_update_rate": 32, + }, + } + set_module_args(dict(state="gathered")) + result = self.execute_module(changed=False) + self.assertEqual(result["gathered"], gathered) + + def test_ios_spanning_tree_parsed(self): + set_module_args( + dict( + running_config=dedent( + """\ + spanning-tree mode mst + no spanning-tree bridge assurance + spanning-tree transmit hold-count 5 + spanning-tree loopguard default + spanning-tree logging + spanning-tree portfast edge default + spanning-tree portfast edge bpduguard default + spanning-tree portfast edge bpdufilter default + no spanning-tree etherchannel guard misconfig + spanning-tree extend system-id + spanning-tree uplinkfast max-update-rate 32 + spanning-tree uplinkfast + spanning-tree backbonefast + spanning-tree pathcost method long + no spanning-tree mst simulate pvst global + spanning-tree mst configuration + name NAME + revision 34 + instance 1 vlan 40-50 + instance 2 vlan 10-20 + spanning-tree mst hello-time 4 + spanning-tree mst forward-time 25 + spanning-tree mst max-age 33 + spanning-tree mst max-hops 33 + spanning-tree mst 0 priority 12288 + spanning-tree mst 1 priority 4096 + spanning-tree mst 5,7-9 priority 57344 + spanning-tree vlan 1,3-5,7,9-11 priority 24576 + spanning-tree vlan 1,3,9 hello-time 4 + spanning-tree vlan 4,6-8 hello-time 5 + spanning-tree vlan 5 hello-time 6 + spanning-tree vlan 1,7-20 forward-time 20 + spanning-tree vlan 1-2,4-5 max-age 38 + """, + ), + state="parsed", + ), + ) + + parsed = { + "backbonefast": True, + "bridge_assurance": False, + "etherchannel_guard_misconfig": False, + "forward_time": [ + { + "value": 20, + "vlan_list": "1,7-20", + }, + ], + "hello_time": [ + { + "value": 4, + "vlan_list": "1,3,9", + }, + { + "value": 5, + "vlan_list": "4,6-8", + }, + { + "value": 6, + "vlan_list": "5", + }, + ], + "logging": True, + "loopguard_default": True, + "max_age": [ + { + "value": 38, + "vlan_list": "1-2,4-5", + }, + ], + "mode": "mst", + "mst": { + "configuration": { + "instances": [ + { + "instance": 1, + "vlan_list": "40-50", + }, + { + "instance": 2, + "vlan_list": "10-20", + }, + ], + "name": "NAME", + "revision": 34, + }, + "forward_time": 25, + "hello_time": 4, + "max_age": 33, + "max_hops": 33, + "priority": [ + { + "instance": "0", + "value": 12288, + }, + { + "instance": "1", + "value": 4096, + }, + { + "instance": "5,7-9", + "value": 57344, + }, + ], + "simulate_pvst_global": False, + }, + "pathcost_method": "long", + "portfast": { + "edge_bpdufilter_default": True, + "edge_bpduguard_default": True, + "edge_default": True, + }, + "priority": [ + { + "value": 24576, + "vlan_list": "1,3-5,7,9-11", + }, + ], + "transmit_hold_count": 5, + "uplinkfast": { + "enabled": True, + "max_update_rate": 32, + }, + } + + result = self.execute_module(changed=False) + self.assertEqual(result["parsed"], parsed) + + def test_ios_spanning_tree_rendered(self): + set_module_args( + dict( + config={ + "mode": "mst", + "backbonefast": True, + "bridge_assurance": False, + "etherchannel_guard_misconfig": False, + "mst": { + "configuration": { + "instances": [ + { + "instance": 1, + "vlan_list": "40-50", + }, + { + "instance": 2, + "vlan_list": "20-30", + }, + ], + "name": "NAME", + "revision": 34, + }, + "forward_time": 25, + "hello_time": 4, + "max_age": 33, + "max_hops": 33, + "priority": [ + { + "instance": "0", + "value": 12288, + }, + { + "instance": "1", + "value": 4096, + }, + { + "instance": "5-7,9", + "value": 57344, + }, + ], + "simulate_pvst_global": False, + }, + "portfast": { + "edge_bpdufilter_default": True, + "edge_default": True, + }, + "hello_time": [ + { + "value": 6, + "vlan_list": "1-3,5-6", + }, + ], + }, + state="rendered", + ), + ) + commands = [ + "spanning-tree mode mst", + "spanning-tree backbonefast", + "no spanning-tree bridge assurance", + "no spanning-tree etherchannel guard misconfig", + "no spanning-tree mst simulate pvst global", + "spanning-tree portfast edge default", + "spanning-tree portfast edge bpdufilter default", + "spanning-tree mst 0 priority 12288", + "spanning-tree mst 1 priority 4096", + "spanning-tree mst 5-7,9 priority 57344", + "spanning-tree mst forward-time 25", + "spanning-tree mst hello-time 4", + "spanning-tree mst max-age 33", + "spanning-tree mst max-hops 33", + "spanning-tree vlan 1-3,5-6 hello-time 6", + "spanning-tree mst configuration", + "name NAME", + "revision 34", + "instance 1 vlan 40-50", + "instance 2 vlan 20-30", + "exit", + ] + result = self.execute_module(changed=False) + self.assertEqual(set(result["rendered"]), set(commands)) + + def test_ios_spanning_tree_merged_idempotent1(self): + self.execute_show_command.return_value = dedent( + """\ + spanning-tree mode rapid-pvst + """, + ) + set_module_args( + dict( + config={ + "mst": { + "forward_time": 25, + "hello_time": 4, + "max_age": 33, + }, + }, + state="merged", + ), + ) + result = self.execute_module(changed=False) + + def test_ios_spanning_tree_merged_idempotent2(self): + self.execute_show_command.return_value = dedent( + """\ + spanning-tree mode rapid-pvst + """, + ) + set_module_args( + dict( + config={ + "mode": "mst", + "mst": { + "forward_time": 25, + "hello_time": 4, + "max_age": 33, + }, + }, + state="merged", + ), + ) + commands = [ + "spanning-tree mode mst", + "spanning-tree mst hello-time 4", + "spanning-tree mst forward-time 25", + "spanning-tree mst max-age 33", + ] + + result = self.execute_module(changed=True) + self.assertEqual(set(result["commands"]), set(commands)) + + def test_ios_spanning_tree_merged_idempotent3(self): + self.execute_show_command.return_value = dedent( + """\ + spanning-tree mode mst + """, + ) + set_module_args( + dict( + config={ + "mode": "rapid-pvst", + "mst": { + "forward_time": 25, + "hello_time": 4, + "max_age": 33, + }, + }, + state="merged", + ), + ) + commands = [ + "spanning-tree mode rapid-pvst", + ] + + result = self.execute_module(changed=True) + self.assertEqual(set(result["commands"]), set(commands)) + + def test_ios_spanning_tree_merged_idempotent4(self): + self.execute_show_command.return_value = dedent( + """\ + spanning-tree mode mst + no spanning-tree bridge assurance + spanning-tree transmit hold-count 5 + spanning-tree loopguard default + spanning-tree logging + spanning-tree portfast edge default + spanning-tree portfast edge bpdufilter default + spanning-tree extend system-id + spanning-tree uplinkfast max-update-rate 32 + spanning-tree uplinkfast + spanning-tree pathcost method long + spanning-tree mst hello-time 4 + spanning-tree mst forward-time 25 + spanning-tree mst max-age 33 + spanning-tree mst max-hops 33 + spanning-tree mst 0 priority 12288 + spanning-tree mst 1 priority 4096 + spanning-tree mst 5,7-9 priority 57344 + spanning-tree vlan 1,3,9 hello-time 4 + spanning-tree vlan 4,6-8 hello-time 5 + spanning-tree mst configuration + name NAME + revision 34 + instance 1 vlan 40-50 + instance 2 vlan 10-20 + """, + ) + set_module_args( + dict( + config={ + "backbonefast": True, + "bridge_assurance": True, + "mst": { + "configuration": { + "instances": [ + { + "instance": 1, + "vlan_list": "40-50", + }, + { + "instance": 2, + "vlan_list": "20-30", + }, + ], + "name": "NAME", + "revision": 34, + }, + "forward_time": 25, + "hello_time": 4, + "max_age": 33, + "max_hops": 33, + "priority": [ + { + "instance": "0", + "value": 12288, + }, + { + "instance": "1", + "value": 4096, + }, + { + "instance": "5-7,9", + "value": 57344, + }, + ], + "simulate_pvst_global": False, + }, + "portfast": { + "edge_bpdufilter_default": True, + "edge_default": True, + }, + "hello_time": [ + { + "value": 6, + "vlan_list": "1-3,5-6", + }, + ], + }, + state="merged", + ), + ) + commands = [ + "spanning-tree backbonefast", + "spanning-tree bridge assurance", + "no spanning-tree mst simulate pvst global", + "spanning-tree mst 6 priority 57344", + "spanning-tree vlan 1-3,5-6 hello-time 6", + "spanning-tree mst configuration", + "instance 2 vlan 21-30", + "exit", + ] + result = self.execute_module(changed=True) + self.assertEqual(set(result["commands"]), set(commands)) + + def test_ios_spanning_tree_merged_idempotent5(self): + self.execute_show_command.return_value = dedent( + """\ + spanning-tree mode pvst + """, + ) + set_module_args( + dict( + config={ + "bridge_assurance": False, + "etherchannel_guard_misconfig": False, + }, + state="merged", + ), + ) + commands = [ + "no spanning-tree bridge assurance", + "no spanning-tree etherchannel guard misconfig", + ] + result = self.execute_module(changed=True) + self.assertEqual(set(result["commands"]), set(commands)) + + def test_ios_spanning_tree_replaced_idempotent(self): + self.execute_show_command.return_value = dedent( + """\ + spanning-tree mode mst + spanning-tree extend system-id + no spanning-tree bridge assurance + spanning-tree transmit hold-count 10 + spanning-tree loopguard default + spanning-tree portfast edge default + spanning-tree portfast edge bpduguard default + spanning-tree portfast edge bpdufilter default + no spanning-tree etherchannel guard misconfig + spanning-tree uplinkfast + spanning-tree backbonefast + spanning-tree mst hello-time 4 + spanning-tree mst forward-time 25 + spanning-tree mst max-age 33 + spanning-tree mst max-hops 33 + no spanning-tree mst simulate pvst global + spanning-tree mst 0 priority 12288 + spanning-tree mst 1 priority 4096 + spanning-tree mst 5,7-9 priority 57344 + spanning-tree vlan 1,3-5,7,9-11 priority 24576 + spanning-tree vlan 1,3,9 hello-time 4 + spanning-tree vlan 4,6-8 hello-time 5 + spanning-tree vlan 5 hello-time 6 + spanning-tree vlan 1,7-20 forward-time 20 + spanning-tree vlan 1-2,4-5 max-age 38 + spanning-tree pathcost method long + spanning-tree uplinkfast max-update-rate 32 + spanning-tree mst configuration + name NAME + revision 34 + instance 1 vlan 40-50 + instance 2 vlan 10-20 + """, + ) + set_module_args( + dict( + config={ + "mode": "rapid-pvst", + "logging": True, + "priority": [ + { + "value": 24576, + "vlan_list": "1,3-5", + }, + ], + "mst": { + "priority": [ + { + "instance": "7-9", + "value": 57344, + }, + ], + }, + }, + state="replaced", + ), + ) + commands = [ + "spanning-tree mode rapid-pvst", + "spanning-tree logging", + "spanning-tree bridge assurance", + "no spanning-tree transmit hold-count 10", + "no spanning-tree loopguard default", + "no spanning-tree portfast edge default", + "no spanning-tree portfast edge bpduguard default", + "no spanning-tree portfast edge bpdufilter default", + "spanning-tree etherchannel guard misconfig", + "spanning-tree mst simulate pvst global", + "no spanning-tree uplinkfast", + "no spanning-tree backbonefast", + "no spanning-tree vlan 7,9-11 priority 24576", + "no spanning-tree vlan 1,3,9 hello-time 4", + "no spanning-tree vlan 4,6-8 hello-time 5", + "no spanning-tree vlan 5 hello-time 6", + "no spanning-tree vlan 1,7-20 forward-time 20", + "no spanning-tree vlan 1-2,4-5 max-age 38", + "no spanning-tree mst configuration", + "no spanning-tree pathcost method long", + "no spanning-tree uplinkfast max-update-rate 32", + ] + result = self.execute_module(changed=True) + self.assertEqual(set(result["commands"]), set(commands)) + + def test_ios_spanning_tree_deleted_idempotent1(self): + self.execute_show_command.return_value = dedent( + """\ + spanning-tree mst configuration + name NAME + revision 34 + instance 1 vlan 40-50 + instance 2 vlan 10-20 + """, + ) + set_module_args( + dict( + config={ + "mst": { + "configuration": { + "name": "NAME", + "revision": 34, + "instances": [ + { + "instance": 1, + "vlan_list": "30-45", + }, + ], + }, + }, + }, + state="deleted", + ), + ) + commands = [ + "spanning-tree mst configuration", + "no name NAME", + "no revision 34", + "no instance 1 vlan 40-45", + "exit", + ] + result = self.execute_module(changed=True) + self.assertEqual(set(result["commands"]), set(commands)) + + def test_ios_spanning_tree_deleted_idempotent2(self): + self.execute_show_command.return_value = dedent( + """\ + spanning-tree mst configuration + name NAME + revision 34 + instance 1 vlan 40-50 + instance 2 vlan 10-20 + """, + ) + set_module_args( + dict( + config={ + "mst": { + "configuration": { + "name": "NAME", + "revision": 34, + "instances": [ + { + "instance": 1, + "vlan_list": "40-50", + }, + { + "instance": 2, + "vlan_list": "10-20", + }, + ], + }, + }, + }, + state="deleted", + ), + ) + commands = [ + "no spanning-tree mst configuration", + ] + result = self.execute_module(changed=True) + self.assertEqual(set(result["commands"]), set(commands)) + + def test_ios_spanning_tree_deleted_idempotent3(self): + self.execute_show_command.return_value = dedent( + """\ + spanning-tree mode rapid-pvst + no spanning-tree bridge assurance + no spanning-tree etherchannel guard misconfig + no spanning-tree mst simulate pvst global + """, + ) + set_module_args( + dict( + config={ + "bridge_assurance": False, + "etherchannel_guard_misconfig": False, + "mst": { + "simulate_pvst_global": False, + }, + }, + state="deleted", + ), + ) + commands = [ + "spanning-tree mst simulate pvst global", + "spanning-tree bridge assurance", + "spanning-tree etherchannel guard misconfig", + ] + result = self.execute_module(changed=True) + self.assertEqual(set(result["commands"]), set(commands)) + + def test_ios_spanning_tree_deleted_idempotent4(self): + self.execute_show_command.return_value = dedent( + """\ + spanning-tree mode mst + no spanning-tree bridge assurance + spanning-tree transmit hold-count 5 + spanning-tree loopguard default + spanning-tree logging + spanning-tree portfast edge default + spanning-tree portfast edge bpduguard default + spanning-tree portfast edge bpdufilter default + spanning-tree extend system-id + spanning-tree uplinkfast max-update-rate 32 + spanning-tree uplinkfast + spanning-tree backbonefast + spanning-tree pathcost method long + no spanning-tree mst simulate pvst global + spanning-tree mst configuration + name NAME + revision 34 + instance 1 vlan 40-50 + instance 2 vlan 10-20 + spanning-tree mst hello-time 4 + spanning-tree mst forward-time 25 + spanning-tree mst max-age 33 + spanning-tree mst max-hops 33 + spanning-tree mst 0 priority 12288 + spanning-tree mst 1 priority 4096 + spanning-tree mst 5,7-9 priority 57344 + spanning-tree vlan 1,3-5,7,9-11 priority 24576 + spanning-tree vlan 1,3,9 hello-time 4 + spanning-tree vlan 4,6-8 hello-time 5 + spanning-tree vlan 5 hello-time 6 + spanning-tree vlan 1,7-12,16-20 forward-time 20 + spanning-tree vlan 1-2,4-5 max-age 38 + """, + ) + set_module_args( + dict( + config={ + "bridge_assurance": False, + "transmit_hold_count": 5, + "uplinkfast": { + "enabled": True, + "max_update_rate": 32, + }, + "logging": False, + "portfast": { + "edge_bpdufilter_default": True, + "edge_bpduguard_default": True, + "edge_default": True, + }, + "backbonefast": True, + "etherchannel_guard_misconfig": True, + "pathcost_method": "long", + "forward_time": [ + {"value": 20, "vlan_list": "9-15,18-30"}, + ], + "priority": [ + {"value": 24576, "vlan_list": "7,8"}, + ], + "hello_time": [ + {"value": 4, "vlan_list": "1,3,9"}, + {"value": 5, "vlan_list": "4,6-8"}, + {"value": 6, "vlan_list": "5"}, + ], + "max_age": [ + {"value": 38, "vlan_list": "1-2,4-5"}, + ], + "mst": { + "forward_time": 25, + "hello_time": 4, + "max_age": 33, + "max_hops": 33, + "simulate_pvst_global": False, + "configuration": { + "instances": [ + { + "instance": 1, + "vlan_list": "40-50", + }, + ], + }, + }, + }, + state="deleted", + ), + ) + commands = [ + "spanning-tree bridge assurance", + "no spanning-tree transmit hold-count 5", + "no spanning-tree uplinkfast max-update-rate 32", + "no spanning-tree uplinkfast", + "no spanning-tree portfast edge bpdufilter default", + "no spanning-tree portfast edge default", + "no spanning-tree portfast edge bpduguard default", + "no spanning-tree backbonefast", + "no spanning-tree pathcost method long", + "no spanning-tree vlan 9-12,18-20 forward-time 20", + "no spanning-tree vlan 7 priority 24576", + "no spanning-tree vlan 5 hello-time 6", + "no spanning-tree vlan 1-2,4-5 max-age 38", + "no spanning-tree vlan 4,6-8 hello-time 5", + "no spanning-tree vlan 1,3,9 hello-time 4", + "spanning-tree mst simulate pvst global", + "no spanning-tree mst hello-time 4", + "no spanning-tree mst forward-time 25", + "no spanning-tree mst max-age 33", + "no spanning-tree mst max-hops 33", + "spanning-tree mst configuration", + "no instance 1 vlan 40-50", + "exit", + ] + result = self.execute_module(changed=True) + self.assertEqual(set(result["commands"]), set(commands)) + + def test_ios_spanning_tree_deleted_idempotent5(self): + self.execute_show_command.return_value = dedent( + """\ + spanning-tree mode rapid-pvst + spanning-tree extend system-id + spanning-tree transmit hold-count 5 + spanning-tree loopguard default + """, + ) + set_module_args( + dict( + config={}, + state="deleted", + ), + ) + commands = [ + "no spanning-tree transmit hold-count 5", + "no spanning-tree loopguard default", + ] + result = self.execute_module(changed=True) + self.assertEqual(set(result["commands"]), set(commands))