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}"