From 2d9cb711bee6beba5934c106a3e1de812d5e58e8 Mon Sep 17 00:00:00 2001 From: itdependsnetworks Date: Sat, 5 Apr 2025 12:42:12 -0400 Subject: [PATCH] Add ability to get the lib mapper in a primary dictionary and a function to munge the data in a os centric view versus a lib centric view. --- docs/user/lib_use_cases_lib_mapper.md | 16 ++++++- netutils/lib_mapper.py | 63 ++++++++++++++++++++++++++- tests/unit/test_lib_mapper.py | 49 ++++++++++++++++++++- 3 files changed, 123 insertions(+), 5 deletions(-) diff --git a/docs/user/lib_use_cases_lib_mapper.md b/docs/user/lib_use_cases_lib_mapper.md index d8f3a8da..55780b9d 100644 --- a/docs/user/lib_use_cases_lib_mapper.md +++ b/docs/user/lib_use_cases_lib_mapper.md @@ -20,7 +20,7 @@ sot_driver = device.platform.napalm_driver # Connect to device via Napalm -driver = napalm.get_network_driver("ios") +driver = napalm.get_network_driver(sot_driver) device = driver( hostname="device.name", @@ -38,6 +38,20 @@ net_con = NTC(host=device.name, username="demo", password="secret", device_type= Another use case could be using an example like the above in an Ansible filter. That would allow you to write a filter utilizing whichever automation library you needed without having to store the driver for each one in your Source of Truth. +There is also a dynamically built mapping that gives you all of the libraries given a normalized name, here is a condensed snippet to understand the data structure of `NAME_TO_ALL_LIB_MAPPER`: + +```python +{ + "cisco_ios": { + "ansible": "cisco.ios.ios", + "napalm": "ios", + }, + "cisco_nxos": { + "ansible": "cisco.nxos.nxos", + "napalm": "nxos", + } +} +``` ## Aerleon Mapper diff --git a/netutils/lib_mapper.py b/netutils/lib_mapper.py index 99cd9326..4b891be3 100644 --- a/netutils/lib_mapper.py +++ b/netutils/lib_mapper.py @@ -125,20 +125,26 @@ } # DNA Center | Normalized -DNA_CENTER_LIB_MAPPER = { +DNACENTER_LIB_MAPPER = { "IOS": "cisco_ios", "IOS-XE": "cisco_ios", "NX-OS": "cisco_nxos", "IOS-XR": "cisco_xr", } +# REMOVE IN 2.X, kept for backward compatibility +DNA_CENTER_LIB_MAPPER = copy.deepcopy(DNACENTER_LIB_MAPPER) + # Normalized | DNA Center -DNA_CENTER_LIB_MAPPER_REVERSE = { +DNACENTER_LIB_MAPPER_REVERSE = { "cisco_ios": "IOS", "cisco_nxos": "NX-OS", "cisco_xr": "IOS-XR", } +# REMOVE IN 2.X, kept for backward compatibility +DNA_CENTER_LIB_MAPPER_REVERSE = copy.deepcopy(DNACENTER_LIB_MAPPER_REVERSE) + # Normalized | Netmiko NETMIKO_LIB_MAPPER: t.Dict[str, str] = { "a10": "a10", @@ -641,3 +647,56 @@ _MAIN_LIB_MAPPER["watchguard_firebox"] = "watchguard_firebox" _MAIN_LIB_MAPPER["windows"] = "windows" MAIN_LIB_MAPPER: t.Dict[str, str] = {key: _MAIN_LIB_MAPPER[key] for key in sorted(_MAIN_LIB_MAPPER)} + +NAME_TO_LIB_MAPPER: t.Dict[str, t.Dict[str, str]] = { + "aerleon": AERLEON_LIB_MAPPER, + "ansible": ANSIBLE_LIB_MAPPER, + "capirca": CAPIRCA_LIB_MAPPER, + "dna_center": DNACENTER_LIB_MAPPER, + "forward_networks": FORWARDNETWORKS_LIB_MAPPER, + "hier_config": HIERCONFIG_LIB_MAPPER, + "napalm": NAPALM_LIB_MAPPER, + "netmiko": NETMIKO_LIB_MAPPER, + "netutils_parser": NETUTILSPARSER_LIB_MAPPER, + "nist": NIST_LIB_MAPPER, + "ntc_templates": NTCTEMPLATES_LIB_MAPPER, + "pyats": PYATS_LIB_MAPPER, + "pyntc": PYNTC_LIB_MAPPER, + "scrapli": SCRAPLI_LIB_MAPPER, +} + + +NAME_TO_LIB_MAPPER_REVERSE: t.Dict[str, t.Dict[str, str]] = { + "aerleon": AERLEON_LIB_MAPPER_REVERSE, + "ansible": ANSIBLE_LIB_MAPPER_REVERSE, + "capirca": CAPIRCA_LIB_MAPPER_REVERSE, + "dna_center": DNACENTER_LIB_MAPPER_REVERSE, + "forward_networks": FORWARDNETWORKS_LIB_MAPPER_REVERSE, + "hier_config": HIERCONFIG_LIB_MAPPER_REVERSE, + "napalm": NAPALM_LIB_MAPPER_REVERSE, + "netmiko": NETMIKO_LIB_MAPPER_REVERSE, + "netutils_parser": NETUTILSPARSER_LIB_MAPPER_REVERSE, + "nist": NIST_LIB_MAPPER_REVERSE, + "ntc_templates": NTCTEMPLATES_LIB_MAPPER_REVERSE, + "pyats": PYATS_LIB_MAPPER_REVERSE, + "pyntc": PYNTC_LIB_MAPPER_REVERSE, + "scrapli": SCRAPLI_LIB_MAPPER_REVERSE, +} + + +# Creates a structure like this: +# { +# "cisco_ios": { +# "ansible": "cisco.ios.ios", +# "napalm": "ios", +# }, +# "cisco_nxos": { +# "ansible": "cisco.nxos.nxos", +# "napalm": "nxos", +# }, +NAME_TO_ALL_LIB_MAPPER: t.Dict[str, t.Dict[str, str]] = {} + +for tool_name, mappings in NAME_TO_LIB_MAPPER_REVERSE.items(): + for normalized_name, mapped_name in mappings.items(): + NAME_TO_ALL_LIB_MAPPER.setdefault(normalized_name, {}) + NAME_TO_ALL_LIB_MAPPER[normalized_name][tool_name] = mapped_name diff --git a/tests/unit/test_lib_mapper.py b/tests/unit/test_lib_mapper.py index 3420dce8..878ec180 100644 --- a/tests/unit/test_lib_mapper.py +++ b/tests/unit/test_lib_mapper.py @@ -20,6 +20,24 @@ "SCRAPLI", ] +MAPPERS = {} +REVERSE_MAPPERS = {} + +# Collect all variables ending with _LIB_MAPPER and _LIB_MAPPER_REVERSE +for name in dir(lib_mapper): + value = getattr(lib_mapper, name) + + if not isinstance(value, dict) or any( + name.startswith(prefix) for prefix in ["NAME_TO", "KEY_TO", "_", "MAIN", "DNA_CENTER"] + ): + continue + if name.endswith("_LIB_MAPPER") and isinstance(value, dict): + lib_name = name.replace("_LIB_MAPPER", "").lower() + MAPPERS[lib_name] = value + elif name.endswith("_LIB_MAPPER_REVERSE") and isinstance(value, dict): + lib_name = name.replace("_LIB_MAPPER_REVERSE", "").lower() + REVERSE_MAPPERS[lib_name] = value + def test_lib_mapper(): assert len(lib_mapper.MAIN_LIB_MAPPER.keys()) > 40 @@ -96,6 +114,13 @@ def test_lib_mapper_ntctemplates_reverse_only(): assert lib_mapper.NTCTEMPLATES_LIB_MAPPER["cisco_xe"] == "cisco_xe" +def test_name_to_all_lib_mapper(): + """Test that the data structure returns as expected in NAME_TO_ALL_LIB_MAPPER.""" + assert lib_mapper.NAME_TO_ALL_LIB_MAPPER["arista_eos"]["ansible"] == "arista.eos.eos" + assert lib_mapper.NAME_TO_ALL_LIB_MAPPER["arista_eos"]["pyntc"] == "arista_eos_eapi" + assert lib_mapper.NAME_TO_ALL_LIB_MAPPER["cisco_ios"]["dna_center"] == "IOS" + + @pytest.mark.parametrize("lib", LIBRARIES) def test_lib_mapper_alpha(lib): original = list(getattr(lib_mapper, f"{lib}_LIB_MAPPER").keys()) @@ -117,5 +142,25 @@ def test_lib_mapper_normalized_name(lib): """Ensure that MAIN_LIB_MAPPER is kept up to date.""" for key in getattr(lib_mapper, f"{lib}_LIB_MAPPER_REVERSE").keys(): assert key in lib_mapper.MAIN_LIB_MAPPER - for value in getattr(lib_mapper, f"{lib}_LIB_MAPPER").values(): - assert value in lib_mapper.MAIN_LIB_MAPPER + for attr in getattr(lib_mapper, f"{lib}_LIB_MAPPER").values(): + assert attr in lib_mapper.MAIN_LIB_MAPPER + + +def test_all_mappers_included(): + """Ensure NAME_TO_LIB_MAPPER includes all _LIB_MAPPER dictionaries.""" + expected_libs = set(MAPPERS.keys()) + actual_libs = {lib.replace("_", "") for lib in lib_mapper.NAME_TO_LIB_MAPPER.keys()} + + # Check for missing libraries + missing = expected_libs - actual_libs + assert len(missing) == 0, f"NAME_TO_LIB_MAPPER is missing libraries: {missing}" + + +def test_all_reverse_mappers_included(): + """Ensure NAME_TO_LIB_MAPPER_REVERSE includes all _LIB_MAPPER_REVERSE dictionaries.""" + expected_libs = set(MAPPERS.keys()) + actual_libs = {lib.replace("_", "") for lib in lib_mapper.NAME_TO_LIB_MAPPER_REVERSE.keys()} + + # Check for missing libraries + missing = expected_libs - actual_libs + assert len(missing) == 0, f"NAME_TO_LIB_MAPPER_REVERSE is missing libraries: {missing}"