summaryrefslogtreecommitdiff
path: root/plugins/module_utils
diff options
context:
space:
mode:
authoromnom62 <75066712+omnom62@users.noreply.github.com>2026-03-04 19:51:16 +1000
committerGitHub <noreply@github.com>2026-03-04 09:51:16 +0000
commita8e5dac21090e9b0ddf876214a9a1522079ecde1 (patch)
tree9865a8b7ad796f8b201b8fea9e93f3c395a5e71f /plugins/module_utils
parente539028c2f9d062e7684abda5dd32affd92e9f08 (diff)
downloadvyos.vyos-a8e5dac21090e9b0ddf876214a9a1522079ecde1.tar.gz
vyos.vyos-a8e5dac21090e9b0ddf876214a9a1522079ecde1.zip
T6836 VRF module (#408)
* t6836 vfr module init * argspec facts module for VRF * T6834 static_route module to support interface_route (#398) * t6834 init * facts fixes, WIP * T6834 - facts updates * static_routes 1.3- config, facts, args and sanit and unit tests * changelog * changelog updates * static_routes 1.4+ * static_routes v1.4 unit tests * comment removed * T6829 (area interfaces in OSPFv3 1.3-1.5) re-init (#399) * T6829 re-init * re-init T6829 ospfv3 area interface for 1.3- * changelog * changelog fix * Doc is updated * adding 1.4+ support * ospfv3 area interfaces for 1.4+ * fixed ospfv3 facts and added area interface unit test for 1.4 * ospfv3 clean-up * doc updates * Resolve merge conflicts * vrf parsers set_table * more vrf paresers added * vrf facts main code * vrf data structures * simple vrf config push * simple vrf config push * some work on vrf * basic facts done * vrf instance parsers wif * ongoing parser testing * vrf dev work * testing with paresers * all basic settings * merged for vrf * more states for VRF * vrf deleted, overriden * overriden * sanity * rst for vrf * changelog * fix changelog * doc fix * vrf delete idempotent * deleted and overridden for VFR * overriden * overriden vrf idempotency * asdvanced vrf settings * afi for VRF facts * VRF AFI config * VRF AFI tests * vrf dev work * afi dev work * afi and deep_merge fixes for VRF * afi basic provisioning * changelog * vrf sanity fixes * VRF AFI protocols and route-maps * vrf parsed, gathered, rendered fixes * docstrings VRF * docstrings VRF * docstring VRF * module doc update * vrf module examples * VRF doc examples * linter * VRF unit tests init * VRF unit tests init * VRF unit tests updates * fixed VRF overridden and deleted unit test cases * VRF integration tests init * VRF int tests rtt, merged, replaced * VRF int testt gathered, rendered * VRF int tests deleted, rendered, overridden and empty_config * linter * VRF int test misc fixes * VRF global idepmotency * fixing py3.13 runner deps * fixing lint * fixing sanity * reverting previous fixes * VRF protocols load_module and provisioning flow * fixes to bgp VRF protocol methods * VRF protocol facts init * VRF protocol load_module works * reworked VRF load_module routine * vrf protocl static fixed * Fixed static and ospf VRF protocol provisioning * Fixed static and ospf VRF protocol provisioning * VRF doc update * VRF docs update * VRF doc updates * unit tests for VRF protocols * VRF BGP protcol sanity check * VRF protocol fixed sanity checks (DOCUMENTATION) * sanity and doc for VRF fixed * VRF unit tests mock * VRF protocol integration parsed, rtt and merged init * VRF protocols - rendered, overridden * VRF protocols replaced * fix accidental changes to test_vyos_config.py * PR description * fix image * README fix * Fixing linter * Mid-result UAT overriden VRF * Mid-result for VRF UAT * VRF uat * VRF obsolete features removed
Diffstat (limited to 'plugins/module_utils')
-rw-r--r--plugins/module_utils/network/vyos/argspec/vrf/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/argspec/vrf/vrf.py131
-rw-r--r--plugins/module_utils/network/vyos/config/vrf/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/config/vrf/vrf.old313
-rw-r--r--plugins/module_utils/network/vyos/config/vrf/vrf.py308
-rw-r--r--plugins/module_utils/network/vyos/facts/facts.py2
-rw-r--r--plugins/module_utils/network/vyos/facts/vrf/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/facts/vrf/vrf.py227
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/vrf.py249
-rw-r--r--plugins/module_utils/network/vyos/utils/utils.py22
10 files changed, 1252 insertions, 0 deletions
diff --git a/plugins/module_utils/network/vyos/argspec/vrf/__init__.py b/plugins/module_utils/network/vyos/argspec/vrf/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/argspec/vrf/__init__.py
diff --git a/plugins/module_utils/network/vyos/argspec/vrf/vrf.py b/plugins/module_utils/network/vyos/argspec/vrf/vrf.py
new file mode 100644
index 00000000..e24fd537
--- /dev/null
+++ b/plugins/module_utils/network/vyos/argspec/vrf/vrf.py
@@ -0,0 +1,131 @@
+# -*- coding: utf-8 -*-
+# Copyright 2024 Red Hat
+# 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 vyos_vrf module
+"""
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.bgp_global.bgp_global import (
+ Bgp_globalArgs,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.ospfv2.ospfv2 import (
+ Ospfv2Args,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.ospfv3.ospfv3 import (
+ Ospfv3Args,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.static_routes.static_routes import (
+ Static_routesArgs,
+)
+
+
+class VrfArgs(object): # pylint: disable=R0903
+ """The arg spec for the vyos_vrf module"""
+
+ bgp_argument_spec = Bgp_globalArgs.argument_spec["config"]
+ static_routes_argument_spec = Static_routesArgs.argument_spec["config"]
+ ospfv2_argument_spec = Ospfv2Args.argument_spec["config"]
+ ospfv3_argument_spec = Ospfv3Args.argument_spec["config"]
+
+ argument_spec = {
+ "config": {
+ "type": "dict",
+ "options": {
+ "bind_to_all": {"type": "bool", "default": False},
+ "instances": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"required": True, "type": "str"},
+ "description": {"type": "str"},
+ "disable": {
+ "aliases": ["disabled"],
+ "default": False,
+ "type": "bool",
+ },
+ "table_id": {"type": "int"},
+ "vni": {"type": "int"},
+ "address_family": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "afi": {
+ "type": "str",
+ "choices": ["ipv4", "ipv6"],
+ },
+ "disable_forwarding": {"type": "bool", "default": False},
+ "nht_no_resolve_via_default": {"type": "bool", "default": False},
+ "route_maps": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "rm_name": {"type": "str", "required": True},
+ "protocol": {
+ "type": "str",
+ "choices": [
+ "any",
+ "babel",
+ "bgp",
+ "eigrp",
+ "isis",
+ "ospf",
+ "rip",
+ "static",
+ ],
+ },
+ },
+ },
+ },
+ },
+ "protocols": {
+ # "type": "list", # sanity
+ # "elements": "dict",
+ "type": "dict",
+ "options": {
+ "bgp": bgp_argument_spec,
+ "ospf": ospfv2_argument_spec,
+ "ospfv3": ospfv3_argument_spec,
+ "static": static_routes_argument_spec,
+ },
+ },
+ },
+ },
+ },
+ },
+ "state": {
+ "type": "str",
+ "choices": [
+ "deleted",
+ "merged",
+ "replaced",
+ "overridden",
+ "gathered",
+ "rendered",
+ "parsed",
+ ],
+ "default": "merged",
+ },
+ "running_config": {"type": "str"},
+ } # pylint: disable=C0301
diff --git a/plugins/module_utils/network/vyos/config/vrf/__init__.py b/plugins/module_utils/network/vyos/config/vrf/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/config/vrf/__init__.py
diff --git a/plugins/module_utils/network/vyos/config/vrf/vrf.old b/plugins/module_utils/network/vyos/config/vrf/vrf.old
new file mode 100644
index 00000000..b9f56cc6
--- /dev/null
+++ b/plugins/module_utils/network/vyos/config/vrf/vrf.old
@@ -0,0 +1,313 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2021 Red Hat
+# 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 vyos_vrf 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.
+"""
+
+import importlib
+
+from copy import deepcopy
+
+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.vyos.vyos.plugins.module_utils.network.vyos.config.bgp_global.bgp_global import (
+ Bgp_global,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import Facts
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vrf import (
+ VrfTemplate,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import combine
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import (
+ LooseVersion,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version
+
+
+# from ansible.plugins.filter.core import combine
+
+
+class Vrf(ResourceModule):
+ """
+ The vyos_vrf config class
+ """
+
+ def __init__(self, module):
+ super(Vrf, self).__init__(
+ empty_fact_val={},
+ facts_module=Facts(module),
+ module=module,
+ resource="vrf",
+ tmplt=VrfTemplate(),
+ )
+ self.parsers = [
+ "bind_to_all",
+ ]
+ self.bgp = Bgp_global(module)
+
+ def _validate_template(self):
+ version = get_os_version(self._module)
+ if LooseVersion(version) >= LooseVersion("1.4"):
+ self._tmplt = VrfTemplate()
+ else:
+ self._module.fail_json(msg="VRF is not supported in this version of VyOS")
+
+ def parse(self):
+ """override parse to check template"""
+ self._validate_template()
+ return super().parse()
+
+ def get_parser(self, name):
+ """get_parsers"""
+ self._validate_template()
+ return super().get_parser(name)
+
+ 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 = {}
+ haved = {}
+ wantd = deepcopy(self.want)
+ haved = deepcopy(self.have)
+
+ # self._module.fail_json(msg="WanT: " + str(self.want) + "**** H: " + str(self.have))
+
+ # if state is merged, merge want onto have and then compare
+ if self.state in ["merged", "replaced"]:
+ # wantd = dict_merge(wantd, haved)
+ # wantd = haved | combine(wantd, recursive=True)
+ wantd = combine(haved, wantd, recursive=True)
+ # self._module.fail_json(msg="Want: " + str(wantd) + "**** H: " + str(haved))
+
+ # if state is deleted, delete and empty out wantd
+ if self.state == "deleted":
+ w = deepcopy(wantd)
+ if w == {} and haved != {}:
+ self.commands = ["delete vrf"]
+ return
+ for k, want in iteritems(w):
+ if not (k in haved and haved[k]):
+ del wantd[k]
+ else:
+ if isinstance(want, list):
+ for entry in want:
+ wname = entry.get("name")
+ haved["instances"] = [
+ i for i in haved.get("instances", []) if i.get("name") != wname
+ ]
+ self.commands.append("delete vrf name {}".format(wname))
+ else:
+ self.commands.append("delete vrf {}".format(k.replace("_", "-")))
+ del wantd[k]
+
+ if self.state == "overridden":
+ w = deepcopy(wantd)
+ h = deepcopy(haved)
+ for k, want in iteritems(w):
+ if k in haved and haved[k] != want:
+ if isinstance(want, list):
+ for entry in want:
+ wname = entry.get("name")
+ hdict = next(
+ (inst for inst in haved["instances"] if inst["name"] == wname),
+ None,
+ )
+ if entry != hdict:
+ # self._module.fail_json(msg="Want: " + str(entry) + "**** H: " + str(hdict))
+ haved["instances"] = [
+ i for i in haved.get("instances", []) if i.get("name") != wname
+ ]
+ self.commands.append("delete vrf name {}".format(wname))
+ self.commands.append("commit")
+
+ for k, want in iteritems(wantd):
+ if isinstance(want, list):
+ self._compare_instances(want=want, have=haved.pop(k, {}))
+ self.compare(
+ parsers=self.parsers,
+ want={k: want},
+ have={k: haved.pop(k, {})},
+ )
+ self._module.fail_json(msg=self.commands)
+
+ def _compare_instances(self, want, have):
+ """Compare the instances of the VRF"""
+ parsers = [
+ "table_id",
+ "vni",
+ "description",
+ "disable_vrf",
+ ]
+ # self._module.fail_json(msg="want: " + str(want) + "**** have: " + str(have))
+
+ for entry in want:
+ h = {}
+ wname = entry.get("name")
+ # h = next((vrf for vrf in have if vrf["name"] == wname), {})
+ h = {
+ k: v
+ for vrf in have
+ if vrf.get("name") == wname
+ for k, v in vrf.items()
+ if k != "address_family"
+ }
+ self.compare(parsers=parsers, want=entry, have=h)
+
+ if "address_family" in entry:
+ wafi = {"name": wname, "address_family": entry.get("address_family", [])}
+ # hdict = next((item for item in have if item["name"] == wname), None)
+ hdict = next((d for d in have if d.get("name") == wname), None)
+
+ hafi = {
+ "name": (hdict or {"name": wname})["name"],
+ "address_family": hdict.get("address_family", []) if hdict else [],
+ }
+
+ # self._module.fail_json(msg="wafi: " + str(wafi) + "**** hafi: " + str(hafi))
+
+ self._compare_addr_family(wafi, hafi)
+
+ if "protocols" in entry:
+ for protocol_name in entry["protocols"]:
+ protocol_module = self._load_protocol_module(protocol_name)
+ w_p_dict = entry["protocols"][protocol_name]
+ h_p_dict = next(
+ (
+ v.get("protocols", {}).get(protocol_name)
+ for v in have
+ if v.get("name") == wname
+ ),
+ {},
+ )
+ if protocol_name == "bgp":
+ protocol_module._validate_template()
+ protocol_module.want = w_p_dict
+ protocol_module.have = h_p_dict
+ protocol_module.generate_commands()
+ elif protocol_name in [
+ # "ospf",
+ # "ospfv3",
+ "static",
+ ]:
+ self._module.fail_json(msg=str(protocol_module))
+ protocol_module._module.params["config"] = w_p_dict
+ protocol_module.state = self.state
+ self._module.fail_json(msg=str(protocol_module.set_config(h_p_dict)))
+
+ protocol_module.commands = protocol_module.set_config(h_p_dict)
+ self.commands.extend(
+ [
+ cmd.replace("protocols", "vrf name " + wname + " protocols", 1)
+ for cmd in protocol_module.commands
+ ],
+ )
+ protocol_module = None # Clear the module to free resources
+
+ def _compare_addr_family(self, want, have):
+ """Compare the address families of the VRF"""
+ afi_parsers = [
+ # "address_family",
+ "disable_forwarding",
+ "disable_nht",
+ ]
+ # self._module.fail_json(msg="wAfi: " + str(want) + "**** hAfi: " + str(have))
+
+ wafi = self.afi_to_list(want)
+ hafi = self.afi_to_list(have)
+
+ lookup = {(d["name"], d["afi"]): d for d in hafi}
+ pairs = [(d1, lookup.get((d1["name"], d1["afi"]), {})) for d1 in wafi]
+
+ for wafd, hafd in pairs:
+ # self._module.fail_json(msg="wAfd: " + str(wafd) + "**** hAfd: " + str(hafd))
+ if "route_maps" in wafd:
+ self._compare_route_maps(wafd, hafd)
+ self.compare(parsers=afi_parsers, want=wafd, have=hafd)
+ # self.compare(parsers=afi_parsers, want=wafi, have=hafi)
+
+ def afi_to_list(self, data):
+ """Convert address family dict to list"""
+
+ return [
+ {"name": data["name"], **{**af, "afi": "ip" if af["afi"] == "ipv4" else af["afi"]}}
+ for af in data["address_family"]
+ ]
+
+ def _compare_route_maps(self, wafd, hafd):
+ want_rms = wafd.get("route_maps", [])
+ have_rms = hafd.get("route_maps", [])
+
+ for want in want_rms:
+ match = next(
+ (
+ h
+ for h in have_rms
+ if h["rm_name"] == want["rm_name"] and h["protocol"] == want["protocol"]
+ ),
+ {},
+ )
+ base = {"name": wafd["name"], "afi": wafd["afi"]}
+
+ self.compare(
+ parsers="route_maps",
+ want={**base, "route_maps": want},
+ have={**base, "route_maps": match},
+ )
+
+ def _load_protocol_module(self, protocol_name):
+ if protocol_name == "bgp":
+ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.bgp_global.bgp_global import (
+ Bgp_global,
+ )
+
+ return Bgp_global(self._module)
+ elif protocol_name == "ospf":
+ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.ospfv2.ospfv2 import (
+ Ospfv2,
+ )
+
+ return Ospfv2(self._module)
+ elif protocol_name == "ospfv3":
+ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.ospfv3.ospfv3 import (
+ Ospfv3,
+ )
+
+ return Ospfv3(self._module)
+ elif protocol_name == "static":
+ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.static_routes.static_routes import (
+ Static_routes,
+ )
+
+ return Static_routes(self._module)
+ else:
+ self._module.fail_json(msg="The protocol is not supported")
diff --git a/plugins/module_utils/network/vyos/config/vrf/vrf.py b/plugins/module_utils/network/vyos/config/vrf/vrf.py
new file mode 100644
index 00000000..aa4329d9
--- /dev/null
+++ b/plugins/module_utils/network/vyos/config/vrf/vrf.py
@@ -0,0 +1,308 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2021 Red Hat
+# 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 vyos_vrf 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 copy import deepcopy
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import (
+ ResourceModule,
+)
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.bgp_global.bgp_global import (
+ Bgp_global,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.ospfv2.ospfv2 import (
+ Ospfv2,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.ospfv3.ospfv3 import (
+ Ospfv3,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.static_routes.static_routes import (
+ Static_routes,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import Facts
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vrf import (
+ VrfTemplate,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import combine
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import (
+ LooseVersion,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version
+
+
+class Vrf(ResourceModule):
+ """
+ The vyos_vrf config class
+ """
+
+ def __init__(self, module):
+ super(Vrf, self).__init__(
+ empty_fact_val={},
+ facts_module=Facts(module),
+ module=module,
+ resource="vrf",
+ tmplt=VrfTemplate(),
+ )
+ self.parsers = [
+ "bind_to_all",
+ ]
+
+ def _validate_template(self):
+ version = get_os_version(self._module)
+ if LooseVersion(version) >= LooseVersion("1.4"):
+ self._tmplt = VrfTemplate()
+ else:
+ self._module.fail_json(msg="VRF is not supported in this version of VyOS")
+
+ def parse(self):
+ """override parse to check template"""
+ self._validate_template()
+ return super().parse()
+
+ def get_parser(self, name):
+ """get_parsers"""
+ self._validate_template()
+ return super().get_parser(name)
+
+ 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 = {}
+ haved = {}
+ wantd = deepcopy(self.want)
+ haved = deepcopy(self.have)
+
+ # if state is merged, merge want onto have and then compare
+ if self.state in ["merged", "replaced"]:
+
+ wantd = combine(haved, wantd, recursive=True)
+
+ # if state is deleted, delete and empty out wantd
+ if self.state == "deleted":
+ w = deepcopy(wantd)
+ if w == {} and haved != {}:
+ self.commands = ["delete vrf"]
+ return
+ for k, want in w.items():
+ if not (k in haved and haved[k]):
+ del wantd[k]
+ else:
+ if isinstance(want, list):
+ for entry in want:
+ wname = entry.get("name")
+ haved["instances"] = [
+ i for i in haved.get("instances", []) if i.get("name") != wname
+ ]
+ self.commands.append("delete vrf name {}".format(wname))
+ else:
+ self.commands.append("delete vrf {}".format(k.replace("_", "-")))
+ del wantd[k]
+
+ if self.state == "overridden":
+ w = deepcopy(wantd)
+ h = deepcopy(haved)
+ for k, want in w.items():
+ if k in haved and haved[k] != want:
+ if isinstance(want, list):
+ for entry in want:
+ wname = entry.get("name")
+ hdict = next(
+ (inst for inst in haved["instances"] if inst["name"] == wname),
+ None,
+ )
+ wantc = self._canonicalise(entry)
+ havec = self._canonicalise(hdict or {})
+
+ if wantc != havec:
+ haved["instances"] = [
+ i for i in haved.get("instances", []) if i.get("name") != wname
+ ]
+ self.commands.append("delete vrf name {}".format(wname))
+ self.commands.append("commit")
+
+ for k, want in wantd.items():
+ if isinstance(want, list):
+ self._compare_instances(want=want, have=haved.pop(k, {}))
+ self.compare(
+ parsers=self.parsers,
+ want={k: want},
+ have={k: haved.pop(k, {})},
+ )
+
+ def _compare_instances(self, want, have):
+ """Compare the instances of the VRF"""
+ parsers = [
+ "table_id",
+ "vni",
+ "description",
+ "disable_vrf",
+ ]
+
+ for entry in want:
+ h = {}
+ wname = entry.get("name")
+ h = {
+ k: v
+ for vrf in have
+ if vrf.get("name") == wname
+ for k, v in vrf.items()
+ if k != "address_family"
+ }
+ self.compare(parsers=parsers, want=entry, have=h)
+
+ if "address_family" in entry:
+ wafi = {"name": wname, "address_family": entry.get("address_family", [])}
+ hdict = next((d for d in have if d.get("name") == wname), None)
+
+ hafi = {
+ "name": (hdict or {"name": wname})["name"],
+ "address_family": hdict.get("address_family", []) if hdict else [],
+ }
+
+ self._compare_addr_family(wafi, hafi)
+
+ if "protocols" in entry:
+ for protocol_name in entry["protocols"]:
+
+ w_p_dict = entry["protocols"][protocol_name]
+
+ h_p_dict = next(
+ (
+ v.get("protocols", {}).get(protocol_name, {})
+ for v in have
+ if v.get("name") == wname
+ ),
+ {},
+ )
+ if protocol_name == "bgp":
+ bgp_module = Bgp_global(self._module)
+ bgp_module._validate_template()
+ bgp_module.want = w_p_dict
+ bgp_module.have = h_p_dict
+ bgp_module.generate_commands()
+ protocol_commands = bgp_module.commands
+ elif protocol_name == "ospf":
+ ospfv2_module = Ospfv2(self._module)
+ ospfv2_module._module.params["config"] = w_p_dict
+ ospfv2_module.state = self.state
+ protocol_commands = ospfv2_module.set_config(h_p_dict)
+ elif protocol_name == "ospfv3":
+ ospfv3_module = Ospfv3(self._module)
+ ospfv3_module._module.params["config"] = w_p_dict
+ ospfv3_module.state = self.state
+ protocol_commands = ospfv3_module.set_config(h_p_dict)
+ elif protocol_name == "static":
+ static_routes_module = Static_routes(self._module)
+ static_routes_module._module.params["config"] = w_p_dict
+ static_routes_module.state = self.state
+ protocol_commands = static_routes_module.set_config(h_p_dict)
+ else:
+ self._module.fail_json(
+ msg="The protocol {} is not supported".format(protocol_name),
+ )
+ self.commands.extend(
+ [
+ cmd.replace("protocols", "vrf name " + wname + " protocols", 1)
+ for cmd in protocol_commands
+ ],
+ )
+
+ def _compare_addr_family(self, want, have):
+ """Compare the address families of the VRF"""
+ afi_parsers = [
+ "disable_forwarding",
+ "disable_nht",
+ ]
+
+ wafi = self.afi_to_list(want)
+ hafi = self.afi_to_list(have)
+
+ lookup = {(d["name"], d["afi"]): d for d in hafi}
+ pairs = [(d1, lookup.get((d1["name"], d1["afi"]), {})) for d1 in wafi]
+
+ for wafd, hafd in pairs:
+ if "route_maps" in wafd:
+ self._compare_route_maps(wafd, hafd)
+ self.compare(parsers=afi_parsers, want=wafd, have=hafd)
+
+ def afi_to_list(self, data):
+ """Convert address family dict to list"""
+
+ return [
+ {"name": data["name"], **{**af, "afi": "ip" if af["afi"] == "ipv4" else af["afi"]}}
+ for af in data["address_family"]
+ ]
+
+ def _compare_route_maps(self, wafd, hafd):
+ want_rms = wafd.get("route_maps", [])
+ have_rms = hafd.get("route_maps", [])
+
+ for want in want_rms:
+ match = next(
+ (
+ h
+ for h in have_rms
+ if h["rm_name"] == want["rm_name"] and h["protocol"] == want["protocol"]
+ ),
+ {},
+ )
+ base = {"name": wafd["name"], "afi": wafd["afi"]}
+
+ self.compare(
+ parsers="route_maps",
+ want={**base, "route_maps": want},
+ have={**base, "route_maps": match},
+ )
+
+ def _canonicalise(self, obj):
+ if isinstance(obj, dict):
+ return {k: self._canonicalise(v) for k, v in obj.items()}
+
+ if isinstance(obj, list):
+ if not obj:
+ return obj
+
+ if not isinstance(obj[0], dict):
+ return sorted(obj)
+
+ canon = [self._canonicalise(i) for i in obj]
+
+ def sort_key(d):
+ for k in ("name", "address", "afi", "area_id", "neighbor_id", "dest"):
+ if k in d:
+ return d[k]
+ return tuple(sorted(d.items()))
+
+ return sorted(canon, key=sort_key)
+
+ return obj
diff --git a/plugins/module_utils/network/vyos/facts/facts.py b/plugins/module_utils/network/vyos/facts/facts.py
index 74bbda74..24c444dd 100644
--- a/plugins/module_utils/network/vyos/facts/facts.py
+++ b/plugins/module_utils/network/vyos/facts/facts.py
@@ -80,6 +80,7 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.snmp_
from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.static_routes.static_routes import (
Static_routesFacts,
)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.vrf.vrf import VrfFacts
FACT_LEGACY_SUBSETS = dict(default=Default, neighbors=Neighbors, config=Config)
@@ -104,6 +105,7 @@ FACT_RESOURCE_SUBSETS = dict(
ntp_global=Ntp_globalFacts,
snmp_server=Snmp_serverFacts,
hostname=HostnameFacts,
+ vrf=VrfFacts,
)
diff --git a/plugins/module_utils/network/vyos/facts/vrf/__init__.py b/plugins/module_utils/network/vyos/facts/vrf/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/facts/vrf/__init__.py
diff --git a/plugins/module_utils/network/vyos/facts/vrf/vrf.py b/plugins/module_utils/network/vyos/facts/vrf/vrf.py
new file mode 100644
index 00000000..6c7b8918
--- /dev/null
+++ b/plugins/module_utils/network/vyos/facts/vrf/vrf.py
@@ -0,0 +1,227 @@
+# -*- coding: utf-8 -*-
+# Copyright 2021 Red Hat
+# 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 vyos vrf 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.
+"""
+
+import re
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.vrf.vrf import VrfArgs
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.bgp_global.bgp_global import (
+ Bgp_globalFacts,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ospfv2.ospfv2 import (
+ Ospfv2Facts,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ospfv3.ospfv3 import (
+ Ospfv3Facts,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.static_routes.static_routes import (
+ Static_routesFacts,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vrf import (
+ VrfTemplate,
+)
+
+
+class VrfFacts(object):
+ """The vyos vrf facts class"""
+
+ def __init__(self, module, subspec="config", options="options"):
+ self._module = module
+ self.argument_spec = VrfArgs.argument_spec
+
+ def get_config(self, connection):
+ return connection.get("show configuration commands | match 'set vrf'")
+
+ def get_config_set(self, data, connection):
+ """To classify the configurations beased on vrf"""
+ config_dict = {}
+ for config_line in data.splitlines():
+ vrf_inst = re.search(r"set vrf name (\S+).*", config_line)
+ vrf_bta = re.search(r"set vrf bind-to-all", config_line)
+ if vrf_bta:
+ config_dict["bind_to_all"] = config_dict.get("bind_to_all", "") + config_line + "\n"
+ if vrf_inst:
+ config_dict[vrf_inst.group(1)] = (
+ config_dict.get(vrf_inst.group(1), "") + config_line + "\n"
+ )
+ return list(config_dict.values())
+
+ def populate_facts(self, connection, ansible_facts, data=None):
+ """Populate the facts for Vrf 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_config(connection)
+
+ vrf_facts = {}
+ instances = []
+ vrf_parser = VrfTemplate(lines=[], module=self._module)
+ resources = self.get_config_set(data, connection)
+
+ for resource in resources:
+ vrf_parser = VrfTemplate(
+ lines=resource.split("\n"),
+ module=self._module,
+ )
+ objs = vrf_parser.parse()
+
+ if objs and "protocols" in resource:
+
+ protocol_lines = []
+ for line in resource.strip().split("\n"):
+ if "protocols" in line:
+ idx = line.index("protocols")
+ protocol_lines.append("set " + line[idx:])
+ objs["protocols"] = self._parse_protocols("\n".join(protocol_lines))
+
+ if objs:
+ if "bind_to_all" in objs:
+ vrf_facts.update(objs)
+ if "name" in objs:
+ instances.append(self._normalise_instance(objs))
+
+ if instances:
+ vrf_facts.update({"instances": instances})
+
+ ansible_facts["ansible_network_resources"].pop("vrf_facts", None)
+ facts = {"vrf": []}
+
+ params = utils.remove_empties(
+ vrf_parser.validate_config(
+ self.argument_spec,
+ {"config": vrf_facts},
+ redact=True,
+ ),
+ )
+
+ if not resources:
+ params["config"].pop("bind_to_all", None)
+
+ if params.get("config"):
+ facts["vrf"] = params["config"]
+ ansible_facts["ansible_network_resources"].update(facts)
+ return ansible_facts
+
+ def _normalise_instance(self, instance):
+ n_inst = instance.copy()
+ af_map = {}
+
+ for af in instance.get("address_family", []):
+ afi = af.get("afi")
+ if not afi:
+ continue
+
+ if afi not in af_map:
+ af_map[afi] = {"afi": afi}
+
+ for k, v in af.items():
+ if k == "afi":
+ continue
+ elif k == "route_maps":
+ if "route_maps" not in af_map[afi]:
+ af_map[afi]["route_maps"] = []
+ af_map[afi]["route_maps"].extend(v)
+ else:
+ af_map[afi][k] = v
+
+ for afi_data in af_map.values():
+ if "route_maps" in afi_data:
+ seen = []
+ deduped = []
+ for item in afi_data["route_maps"]:
+ if item not in seen:
+ seen.append(item)
+ deduped.append(item)
+ afi_data["route_maps"] = deduped
+
+ n_inst["address_family"] = list(af_map.values())
+ return n_inst
+
+ def _parse_protocols(self, protocols):
+ """Parse protocols and return a dictionary"""
+
+ protocol_chunks = {}
+ parsed_protocols = {}
+
+ for line in protocols.split("\n"):
+ parts = line.split()
+ if len(parts) > 2 and parts[0] == "set" and parts[1] == "protocols":
+ protocol = parts[2]
+ protocol_chunks.setdefault(protocol, []).append(line)
+
+ protocol_strings = {proto: "\n".join(lines) for proto, lines in protocol_chunks.items()}
+
+ for protocol_name, protocol_string in protocol_strings.items():
+ protocol_dict = {}
+
+ if protocol_name == "bgp":
+ bgp_module = Bgp_globalFacts(self._module)
+ protocol_dict = bgp_module.populate_facts(
+ connection=self._module._connection,
+ ansible_facts={"ansible_network_resources": {}},
+ data=protocol_string,
+ )
+ parsed_protocols[protocol_name] = list(
+ protocol_dict.get("ansible_network_resources").values(),
+ )[0]
+
+ elif protocol_name == "ospf":
+ ospf_module = Ospfv2Facts(self._module)
+ protocol_dict = ospf_module.populate_facts(
+ connection=self._module._connection,
+ ansible_facts={"ansible_network_resources": {}},
+ data=protocol_string,
+ )
+ parsed_protocols[protocol_name] = list(
+ protocol_dict.get("ansible_network_resources").values(),
+ )[0]
+
+ elif protocol_name == "ospfv3":
+ ospfv3_module = Ospfv3Facts(self._module)
+ protocol_dict = ospfv3_module.populate_facts(
+ connection=self._module._connection,
+ ansible_facts={"ansible_network_resources": {}},
+ data=protocol_string,
+ )
+ parsed_protocols[protocol_name] = list(
+ protocol_dict.get("ansible_network_resources").values(),
+ )[0]
+
+ elif protocol_name == "static":
+ static_routes_module = Static_routesFacts(self._module)
+ protocol_dict = static_routes_module.populate_facts(
+ connection=self._module._connection,
+ ansible_facts={"ansible_network_resources": {}},
+ data=protocol_string,
+ )
+ parsed_protocols[protocol_name] = list(
+ protocol_dict.get("ansible_network_resources").values(),
+ )[0]
+ else:
+ self._module.fail_json(msg="The protocol is not supported" + protocol_name)
+
+ return parsed_protocols
diff --git a/plugins/module_utils/network/vyos/rm_templates/vrf.py b/plugins/module_utils/network/vyos/rm_templates/vrf.py
new file mode 100644
index 00000000..5171404e
--- /dev/null
+++ b/plugins/module_utils/network/vyos/rm_templates/vrf.py
@@ -0,0 +1,249 @@
+# -*- coding: utf-8 -*-
+# Copyright 2021 Red Hat
+# 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 VRF 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,
+)
+
+
+class VrfTemplate(NetworkTemplate):
+ def __init__(self, lines=None, module=None):
+ prefix = {"set": "set", "remove": "delete"}
+ super(VrfTemplate, self).__init__(lines=lines, tmplt=self, prefix=prefix, module=module)
+
+ # fmt: off
+ PARSERS = [
+ {
+ "name": "table_id",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+vrf
+ \s+name
+ \s+(?P<name>\S+)
+ \s+table
+ \s+'(?P<tid>\S+)'
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "vrf name {{ name }} table {{ table_id }}",
+ "result": {
+ "name": "{{ name }}",
+ "table_id": "{{ tid }}",
+ },
+ },
+ {
+ "name": "bind_to_all",
+ "getval": re.compile(
+ r"""
+ ^set
+ \svrf
+ \s(?P<bta>bind-to-all)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "vrf bind-to-all",
+ "result": {
+ "bind_to_all": "{{ True if bta is defined }}",
+ },
+ },
+ {
+ "name": "vni",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+vrf
+ \s+name
+ \s+(?P<name>\S+)
+ \s+vni
+ \s'(?P<vni>\S+)'
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "vrf name {{name}} vni {{vni}}",
+ "result": {
+ "name": "{{ name }}",
+ "vni": "{{ vni }}",
+ },
+ },
+ {
+ "name": "description",
+ "getval": re.compile(
+ r"""
+ ^set
+ \svrf
+ \sname
+ \s(?P<name>\S+)
+ \sdescription
+ \s(?P<desc>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "vrf name {{name}} description {{description}}",
+ "result": {
+ "name": "{{ name }}",
+ "description": "{{ desc }}",
+ },
+ },
+ {
+ "name": "disable_vrf",
+ "getval": re.compile(
+ r"""
+ ^set
+ \svrf
+ \sname
+ \s(?P<name>\S+)
+ \s(?P<disable>disable)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "vrf name {{name}} disable",
+ "compval": "disable",
+ "result": {
+ "name": "{{ name }}",
+ "disable": "{{ True if disable is defined }}",
+ },
+ },
+ # {
+ # "name": "address_family",
+ # "getval": re.compile(
+ # r"""
+ # ^set
+ # \svrf
+ # \sname
+ # \s(?P<name>\S+)
+ # \s(?P<af>ip|ipv6)
+ # $""",
+ # re.VERBOSE,
+ # ),
+ # "setval": "vrf name {{name}} {{ af }}",
+ # 'compval': "address_family",
+ # "result": {
+ # "name": "{{ name }}",
+ # "address_family": {
+ # '{{ "ipv4" if af == "ip" else "ipv6" }}': {
+ # "afi": '{{ "ipv4" if af == "ip" else "ipv6" }}',
+ # },
+ # },
+ # },
+ # },
+ # {
+ # "name": "address_family.disable_forwarding",
+ # "getval": re.compile(
+ # r"""
+ # ^set
+ # \svrf
+ # \sname
+ # \s(?P<name>\S+)
+ # \s(?P<af>ip|ipv6)
+ # \s(?P<df>disable-forwarding)
+ # $""",
+ # re.VERBOSE,
+ # ),
+ # "setval": "vrf name {{name}} {{ afi }} disable-forwarding",
+ # # "compval": "address_family.ipv6.disable_forwarding",
+ # "result": {
+ # "name": "{{ name }}",
+ # "address_family": {
+ # '{{ "ipv4" if af == "ip" else "ipv6" }}': {
+ # "afi": '{{ "ipv4" if af == "ip" else "ipv6" }}',
+ # "disable_forwarding": "{{ True if df is defined }}",
+ # },
+ # },
+ # },
+ # },
+ {
+ "name": "disable_forwarding",
+ "getval": re.compile(
+ r"""
+ ^set
+ \svrf
+ \sname
+ \s(?P<name>\S+)
+ \s(?P<af>ip|ipv6)
+ \s(?P<df>disable-forwarding)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "vrf name {{name}} {{ afi }} disable-forwarding",
+ "compval": "disable_forwarding",
+ "result": {
+ "name": "{{ name }}",
+ 'address_family': [{
+ "afi": '{{ "ipv4" if af == "ip" else "ipv6" }}',
+ "disable_forwarding": "{{ True if df is defined }}",
+ }],
+ },
+ },
+ {
+ "name": "disable_nht",
+ "getval": re.compile(
+ r"""
+ ^set
+ \svrf
+ \sname
+ \s(?P<name>\S+)
+ \s(?P<af>ip|ipv6)
+ \snht
+ \s(?P<nht>no-resolve-via-default)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "vrf name {{name}} {{ afi }} nht no-resolve-via-default",
+ "compval": "nht_no_resolve_via_default",
+ "result": {
+ "name": "{{ name }}",
+ "address_family": [{
+ "afi": '{{ "ipv4" if af == "ip" else "ipv6" }}',
+ "nht_no_resolve_via_default": "{{ True if nht is defined }}",
+ }],
+ },
+ },
+ {
+ "name": "route_maps",
+ "getval": re.compile(
+ r"""
+ ^set
+ \svrf
+ \sname
+ \s(?P<name>\S+)
+ \s(?P<af>ip|ipv6)
+ \sprotocol
+ \s(?P<proto>\S+)
+ \sroute-map
+ \s'(?P<rm>\S+)'
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "vrf name {{name}} {{ afi }} protocol {{ route_maps.protocol }} route-map {{ route_maps.rm_name }}",
+ "compval": "route_maps",
+ "remval": "vrf name {{name}} {{ afi }} protocol {{ route_maps.protocol }}",
+ "result": {
+ "name": "{{ name }}",
+ "address_family": [{
+ "afi": '{{ "ipv4" if af == "ip" else "ipv6" }}',
+ "route_maps": [{
+ "rm_name": "{{ rm }}",
+ "protocol": "{{ proto }}",
+ }],
+ }],
+ },
+ },
+ ]
+ # fmt: on
diff --git a/plugins/module_utils/network/vyos/utils/utils.py b/plugins/module_utils/network/vyos/utils/utils.py
index 6957f6cb..78445c02 100644
--- a/plugins/module_utils/network/vyos/utils/utils.py
+++ b/plugins/module_utils/network/vyos/utils/utils.py
@@ -285,3 +285,25 @@ def in_target_not_none(h, key):
:return: True/False.
"""
return True if h and key in h and h[key] is not None else False
+
+
+def combine(a, b, recursive=False, list_merge="replace"):
+ """
+ Merge two dictionaries (shallow or deep).
+ :param a: dict
+ :param b: dict
+ :param recursive: bool, deep merge
+ :param list_merge: str, only 'replace' is supported (default Ansible behavior)
+ """
+ if not isinstance(a, dict) or not isinstance(b, dict):
+ raise ValueError("combine expects two dictionaries")
+
+ result = a.copy()
+
+ for k, v in b.items():
+ if recursive and k in result and isinstance(result[k], dict) and isinstance(v, dict):
+ result[k] = combine(result[k], v, recursive=True, list_merge=list_merge)
+ else:
+ result[k] = v
+
+ return result