From 1bf981bd62d56e608cea8e7f0f0132af8cf25b70 Mon Sep 17 00:00:00 2001 From: Dylan Kaplan Date: Sat, 5 Jun 2021 13:14:46 -0700 Subject: [PATCH 01/74] Fix NXOS tests to allow mocked data to raise --- test/nxos/conftest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/nxos/conftest.py b/test/nxos/conftest.py index cd5f051a6..44ffe5656 100644 --- a/test/nxos/conftest.py +++ b/test/nxos/conftest.py @@ -2,10 +2,9 @@ from builtins import super import pytest +from napalm.base.mock import raise_exception from napalm.base.test import conftest as parent_conftest - from napalm.base.test.double import BaseTestDouble - from napalm.nxos import nxos @@ -74,6 +73,8 @@ def show(self, command, raw_text=False): result = self.read_txt_file(full_path) else: result = self.read_json_file(full_path) + if "exception" in result: + raise_exception(result) return result From 6b29a50e2f1ac7ce29063161b36823fbd9d0f48f Mon Sep 17 00:00:00 2001 From: Dylan Kaplan Date: Sun, 6 Jun 2021 12:27:19 -0700 Subject: [PATCH 02/74] Update NXOS response processing for more informative error messaging --- napalm/nxapi_plumbing/api_client.py | 24 ++++---- napalm/nxos/nxos.py | 2 +- test/nxapi_plumbing/conftest.py | 6 +- test/nxapi_plumbing/mock_device.py | 55 +++++++++++++++++-- test/nxapi_plumbing/test_config.py | 34 ++++++++++++ .../no_ipv6_support/show_ipv6_interface.json | 8 ++- 6 files changed, 108 insertions(+), 21 deletions(-) diff --git a/napalm/nxapi_plumbing/api_client.py b/napalm/nxapi_plumbing/api_client.py index d403a216d..55d399334 100644 --- a/napalm/nxapi_plumbing/api_client.py +++ b/napalm/nxapi_plumbing/api_client.py @@ -78,15 +78,7 @@ def _send_request(self, commands, method): ) raise NXAPIAuthError(msg) - if response.status_code not in [200]: - msg = """Invalid status code returned on NX-API POST -commands: {} -status_code: {}""".format( - commands, response.status_code - ) - raise NXAPIPostError(msg) - - return response.text + return response class RPCClient(RPCBase): @@ -139,7 +131,7 @@ def _process_api_response(self, response, commands, raw_text=False): structured data. """ - response_list = json.loads(response) + response_list = json.loads(response.text) if isinstance(response_list, dict): response_list = [response_list] @@ -150,7 +142,7 @@ def _process_api_response(self, response, commands, raw_text=False): new_response = [] for response in response_list: - # Dectect errors + # Detect errors self._error_check(response) # Some commands like "show run" can have a None result @@ -235,7 +227,15 @@ def _build_payload(self, commands, method, xml_version="1.0", version="1.0"): return payload def _process_api_response(self, response, commands, raw_text=False): - xml_root = etree.fromstring(response) + if response.status_code not in [200]: + msg = """Invalid status code returned on NX-API POST +commands: {} +status_code: {}""".format( + commands, response.status_code + ) + raise NXAPIPostError(msg) + + xml_root = etree.fromstring(response.text) response_list = xml_root.xpath("outputs/output") if len(commands) != len(response_list): raise NXAPIXMLError( diff --git a/napalm/nxos/nxos.py b/napalm/nxos/nxos.py index 67fdc0595..84e899888 100644 --- a/napalm/nxos/nxos.py +++ b/napalm/nxos/nxos.py @@ -1175,7 +1175,7 @@ def get_interfaces_ip(self): ipv6_interf_table_vrf = self._get_command_table( ipv6_command, "TABLE_intf", "ROW_intf" ) - except napalm.nxapi_plumbing.errors.NXAPIPostError: + except napalm.nxapi_plumbing.errors.NXAPICommandError: return interfaces_ip for interface in ipv6_interf_table_vrf: diff --git a/test/nxapi_plumbing/conftest.py b/test/nxapi_plumbing/conftest.py index 245319eaa..09fea1faa 100644 --- a/test/nxapi_plumbing/conftest.py +++ b/test/nxapi_plumbing/conftest.py @@ -39,6 +39,7 @@ def pytest_addoption(parser): @pytest.fixture(scope="module") def mock_pynxos_device(request): """Create a mock pynxos test device.""" + response_status_code = getattr(request, "param", 200) device = { "host": "nxos1.fake.com", "username": "admin", @@ -49,13 +50,14 @@ def mock_pynxos_device(request): "timeout": 60, "verify": False, } - conn = MockDevice(**device) + conn = MockDevice(response_status_code=response_status_code, **device) return conn @pytest.fixture(scope="module") def mock_pynxos_device_xml(request): """Create a mock pynxos test device.""" + response_status_code = getattr(request, "param", 200) device = { "host": "nxos1.fake.com", "username": "admin", @@ -66,7 +68,7 @@ def mock_pynxos_device_xml(request): "timeout": 60, "verify": False, } - conn = MockDevice(**device) + conn = MockDevice(response_status_code=response_status_code, **device) return conn diff --git a/test/nxapi_plumbing/mock_device.py b/test/nxapi_plumbing/mock_device.py index 852ebf272..9355b056a 100644 --- a/test/nxapi_plumbing/mock_device.py +++ b/test/nxapi_plumbing/mock_device.py @@ -61,6 +61,7 @@ def __init__( port=None, timeout=30, verify=True, + response_status_code=200, ): super().__init__( host, @@ -81,6 +82,7 @@ def __init__( port=port, timeout=timeout, verify=verify, + response_status_code=response_status_code, ) elif api_format == "xml": self.api = MockXMLClient( @@ -91,10 +93,33 @@ def __init__( port=port, timeout=timeout, verify=verify, + response_status_code=response_status_code, ) class MockRPCClient(RPCClient): + def __init__( + self, + host, + username, + password, + transport="https", + port=None, + timeout=30, + verify=True, + response_status_code=200, + ): + self.response_status_code = response_status_code + super().__init__( + host=host, + username=username, + password=password, + transport=transport, + port=port, + timeout=timeout, + verify=verify, + ) + def _send_request(self, commands, method="cli"): payload = self._build_payload(commands, method) @@ -112,12 +137,34 @@ def _send_request(self, commands, method="cli"): response_obj = FakeResponse() response_obj.text = mock_response - response_obj.status_code = 200 + response_obj.status_code = self.response_status_code - return response_obj.text + return response_obj class MockXMLClient(XMLClient): + def __init__( + self, + host, + username, + password, + transport="https", + port=None, + timeout=30, + verify=True, + response_status_code=200, + ): + self.response_status_code = response_status_code + super().__init__( + host=host, + username=username, + password=password, + transport=transport, + port=port, + timeout=timeout, + verify=verify, + ) + def _send_request(self, commands, method="cli_show"): payload = self._build_payload(commands, method) @@ -135,6 +182,6 @@ def _send_request(self, commands, method="cli_show"): response_obj = FakeResponse() response_obj.text = mock_response - response_obj.status_code = 200 + response_obj.status_code = self.response_status_code - return response_obj.text + return response_obj diff --git a/test/nxapi_plumbing/test_config.py b/test/nxapi_plumbing/test_config.py index 1b244b62d..1941fcca4 100644 --- a/test/nxapi_plumbing/test_config.py +++ b/test/nxapi_plumbing/test_config.py @@ -1,3 +1,9 @@ +import pytest + +from napalm.nxapi_plumbing import NXAPICommandError +from napalm.nxapi_plumbing.errors import NXAPIPostError + + def test_config_jsonrpc(mock_pynxos_device): result = mock_pynxos_device.config("logging history size 200") assert result is None @@ -36,3 +42,31 @@ def test_config_xml_list(mock_pynxos_device_xml): msg = element.find("./msg") assert status_code.text == "200" assert msg.text == "Success" + + +@pytest.mark.parametrize("mock_pynxos_device", [500], indirect=True) +def test_config_jsonrpc_raises_NXAPICommandError_on_non_200_config_error( + mock_pynxos_device, +): + with pytest.raises( + NXAPICommandError, match='The command "bogus command" gave the error' + ) as e: + result = mock_pynxos_device.config("bogus command") + + +@pytest.mark.parametrize("mock_pynxos_device_xml", [500], indirect=True) +def test_config_xml_raises_NXAPIPostError_on_non_200_post_error(mock_pynxos_device_xml): + with pytest.raises( + NXAPIPostError, match="Invalid status code returned on NX-API POST" + ) as e: + result = mock_pynxos_device_xml.config("logging history size 200") + + +@pytest.mark.parametrize("mock_pynxos_device_xml", [200], indirect=True) +def test_config_xml_raises_NXAPICommandError_on_200_config_error( + mock_pynxos_device_xml, +): + with pytest.raises( + NXAPICommandError, match='The command "bogus command" gave the error' + ) as e: + result = mock_pynxos_device_xml.config("bogus command") diff --git a/test/nxos/mocked_data/test_get_interfaces_ip/no_ipv6_support/show_ipv6_interface.json b/test/nxos/mocked_data/test_get_interfaces_ip/no_ipv6_support/show_ipv6_interface.json index d6e3d7429..c32daa6a3 100644 --- a/test/nxos/mocked_data/test_get_interfaces_ip/no_ipv6_support/show_ipv6_interface.json +++ b/test/nxos/mocked_data/test_get_interfaces_ip/no_ipv6_support/show_ipv6_interface.json @@ -1,3 +1,7 @@ { - "exception": "nxapi_plumbing.api_client.NXAPIPostError" -} + "exception": "napalm.nxapi_plumbing.errors.NXAPICommandError", + "kwargs": { + "command": "show ipv6 interface", + "message": "" + } +} \ No newline at end of file From e5f40cfa3a5c8f83e739c97bdf8f7a6f0fafd3df Mon Sep 17 00:00:00 2001 From: shenganzhang <51085557+shenganzhang@users.noreply.github.com> Date: Thu, 4 Nov 2021 00:36:00 -0500 Subject: [PATCH 03/74] Update test_unit.py --- test/base/validate/test_unit.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/base/validate/test_unit.py b/test/base/validate/test_unit.py index d9654f6e2..7f831eddf 100644 --- a/test/base/validate/test_unit.py +++ b/test/base/validate/test_unit.py @@ -1,5 +1,6 @@ """Tests for the validate methods.""" import pytest +import copy from napalm.base import validate @@ -403,7 +404,7 @@ class TestValidate: @pytest.mark.parametrize("src, dst, result", _compare_getter) def test__compare_getter_list(self, src, dst, result): """Test for _compare_getter_list.""" - assert validate.compare(src, dst) == result + assert validate.compare(copy.deepcopy(src), copy.deepcopy(dst)) == copy.deepcopy(result) def test_numeric_comparison(self): assert validate._compare_numeric("<2", 1) From 85a82096c1b22fdc6fa4dbed23b807823c7a731a Mon Sep 17 00:00:00 2001 From: Shengan Zhang Date: Thu, 2 Dec 2021 17:02:08 -0600 Subject: [PATCH 04/74] reformat by black --- test/base/validate/test_unit.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/base/validate/test_unit.py b/test/base/validate/test_unit.py index 7f831eddf..08b333f39 100644 --- a/test/base/validate/test_unit.py +++ b/test/base/validate/test_unit.py @@ -404,7 +404,9 @@ class TestValidate: @pytest.mark.parametrize("src, dst, result", _compare_getter) def test__compare_getter_list(self, src, dst, result): """Test for _compare_getter_list.""" - assert validate.compare(copy.deepcopy(src), copy.deepcopy(dst)) == copy.deepcopy(result) + assert validate.compare( + copy.deepcopy(src), copy.deepcopy(dst) + ) == copy.deepcopy(result) def test_numeric_comparison(self): assert validate._compare_numeric("<2", 1) From d2bc808d4f6480df6b5e9ef3cfa6101ce7b54478 Mon Sep 17 00:00:00 2001 From: OsirisS13 Date: Mon, 20 Dec 2021 14:57:33 +1000 Subject: [PATCH 05/74] Add support for optional_args to iosxr_netconf.py Added the ability to pass in optional arguments to the iosxr_netconf driver. Note these are used as supplied and not passed through the netmiko_helpers base function. --- napalm/iosxr_netconf/iosxr_netconf.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/napalm/iosxr_netconf/iosxr_netconf.py b/napalm/iosxr_netconf/iosxr_netconf.py index 654ae31cc..b43692f10 100644 --- a/napalm/iosxr_netconf/iosxr_netconf.py +++ b/napalm/iosxr_netconf/iosxr_netconf.py @@ -69,6 +69,7 @@ def __init__(self, hostname, username, password, timeout=60, optional_args=None) if optional_args is None: optional_args = {} + self.netmiko_optional_args = optional_args self.port = optional_args.get("port", 830) self.lock_on_connect = optional_args.get("config_lock", False) self.key_file = optional_args.get("key_file", None) @@ -91,6 +92,7 @@ def open(self): key_filename=self.key_file, timeout=self.timeout, device_params={"name": "iosxr"}, + **self.netmiko_optional_args, ) if self.lock_on_connect: self._lock() @@ -442,7 +444,7 @@ def get_interfaces(self): "is_up": False, "mac_address": "", "description": "", - "speed": -1.0, + "speed": -1, "last_flapped": -1.0, } @@ -489,15 +491,14 @@ def get_interfaces(self): napalm.base.helpers.mac, raw_mac, raw_mac ) speed = napalm.base.helpers.convert( - float, + int, napalm.base.helpers.convert( - float, + int, self._find_txt(interface_tree, "./int:bandwidth", namespaces=C.NS), 0, ) * 1e-3, ) - mtu = int( self._find_txt(interface_tree, "./int:mtu", default="", namespaces=C.NS) ) From cbae1b9a1b029216d6e864a7b85c9a5e4c226fa1 Mon Sep 17 00:00:00 2001 From: DavidVentura Date: Tue, 25 Jan 2022 21:45:11 +0100 Subject: [PATCH 06/74] Fix #1547 --- napalm/ios/ios.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/napalm/ios/ios.py b/napalm/ios/ios.py index bc8ddf6ad..6bbdc8299 100644 --- a/napalm/ios/ios.py +++ b/napalm/ios/ios.py @@ -3427,6 +3427,10 @@ def get_network_instances(self, name=""): else: return instances + if "Invalid input detected" in sh_vrf_detail: + # No VRF support + return instances + for vrf in sh_vrf_detail.split("\n\n"): first_part = vrf.split("Address family")[0] From 2c73a3af1e793b4142bd6ea05e009f2b1e7a62bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Feb 2022 16:34:46 +0000 Subject: [PATCH 07/74] Bump pytest-pythonpath from 0.7.3 to 0.7.4 Bumps [pytest-pythonpath](https://github.com/bigsassy/pytest-pythonpath) from 0.7.3 to 0.7.4. - [Release notes](https://github.com/bigsassy/pytest-pythonpath/releases) - [Commits](https://github.com/bigsassy/pytest-pythonpath/commits) --- updated-dependencies: - dependency-name: pytest-pythonpath dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index f7b3077f1..672e35301 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,7 +5,7 @@ flake8-import-order==0.18.1 pytest==5.4.3 pytest-cov==3.0.0 pytest-json==0.4.0 -pytest-pythonpath==0.7.3 +pytest-pythonpath==0.7.4 pylama==7.7.1 mock==4.0.3 tox==3.24.4 From c5bd6ad8dd9460616cbc5bd6a80b34ca07e1c33d Mon Sep 17 00:00:00 2001 From: OsirisS13 Date: Fri, 11 Feb 2022 15:03:18 +1000 Subject: [PATCH 08/74] Revert minor formatting changes in iosxr_netconf.py --- napalm/iosxr_netconf/iosxr_netconf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/napalm/iosxr_netconf/iosxr_netconf.py b/napalm/iosxr_netconf/iosxr_netconf.py index b43692f10..8f64151ac 100644 --- a/napalm/iosxr_netconf/iosxr_netconf.py +++ b/napalm/iosxr_netconf/iosxr_netconf.py @@ -444,7 +444,7 @@ def get_interfaces(self): "is_up": False, "mac_address": "", "description": "", - "speed": -1, + "speed": -1.0, "last_flapped": -1.0, } @@ -491,9 +491,9 @@ def get_interfaces(self): napalm.base.helpers.mac, raw_mac, raw_mac ) speed = napalm.base.helpers.convert( - int, + float, napalm.base.helpers.convert( - int, + float, self._find_txt(interface_tree, "./int:bandwidth", namespaces=C.NS), 0, ) From 9c344e523ff5082763641c2ad1e33b2b99bf2894 Mon Sep 17 00:00:00 2001 From: Denis Mulyalin Date: Fri, 11 Feb 2022 03:31:29 -0500 Subject: [PATCH 09/74] added parse_ttp helper function together with tests, updated requirements --- napalm/base/helpers.py | 78 +++++++++++++++++++ requirements-dev.txt | 4 +- requirements.txt | 2 + .../base/utils/ttp_templates/bad_template.txt | 5 ++ .../utils/ttp_templates/good_template.txt | 5 ++ 5 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 test/base/utils/ttp_templates/bad_template.txt create mode 100644 test/base/utils/ttp_templates/good_template.txt diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index bf605c333..9ab99db4b 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -18,6 +18,13 @@ from netaddr import IPAddress from netaddr import mac_unix +try: + from ttp import ttp, quick_parse as ttp_quick_parse + + TTP_INSTALLED = True +except ImportError: + TTP_INSTALLED = False + # local modules import napalm.base.exceptions from napalm.base import constants @@ -269,6 +276,77 @@ def textfsm_extractor( ) +def ttp_parse( + cls: "napalm.base.NetworkDriver", + template: str, + raw_text: str, + structure: str = "flat_list", +) -> List[Dict]: + """ + Applies a TTP template over a raw text and return the parsing results. + + Main usage of this method will be to extract data form a non-structured output + from a network device and return parsed values. + + :param cls: Instance of the driver class + :param template: Specifies the name or the content of the template to be used + :param raw_text: Text output as the devices prompts on the CLI + :param structure: Results structure to apply to parsing results + :return: parsing results structure + + ``template`` can be inline TTP template string, reference to TTP Templates + repository template in a form of ``ttp://path/to/template`` or name of template + file within ``{NAPALM_install_dir}/utils/ttp_templates/{template}.txt`` folder. + """ + if not TTP_INSTALLED: + msg = "\nTTP is not installed. Please PIP install ttp:\n" "pip install ttp\n" + raise napalm.base.exceptions.ModuleImportError(msg) + + for c in cls.__class__.mro(): + if c is object: + continue + module = sys.modules[c.__module__].__file__ + if module: + current_dir = os.path.dirname(os.path.abspath(module)) + else: + continue + template_dir_path = "{current_dir}/utils/ttp_templates".format( + current_dir=current_dir + ) + + # check if inline template given, use it as is + if "{{" in template and "}}" in template: + template = template + # check if template from ttp_templates repo, use it as is + elif template.startswith("ttp://"): + template = template + # default to using template in NAPALM folder + else: + template = "{template_dir_path}/{template}.txt".format( + template_dir_path=template_dir_path, template=template + ) + if not os.path.exists(template): + msg = "Template '{template}' not found".format(template=template) + logging.error(msg) + raise napalm.base.exceptions.TemplateRenderException(msg) + + # parse data + try: + return ttp_quick_parse( + data=str(raw_text), + template=template, + result_kwargs={"structure": structure}, + parse_kwargs={"one": True}, + ) + except Exception as e: + msg = "TTP template:\n'{template}'\nError: {error}".format( + template=template, error=e + ) + logging.exception(e) + logging.error(msg) + raise napalm.base.exceptions.TemplateRenderException(msg) + + def find_txt( xml_tree: etree._Element, path: str, diff --git a/requirements-dev.txt b/requirements-dev.txt index f12725676..cab81f7cd 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,4 +13,6 @@ mypy==0.931 types-requests==2.27.9 types-six==1.16.10 types-setuptools==57.4.9 -types-PyYAML==6.0.4 \ No newline at end of file +types-PyYAML==6.0.4 +ttp==0.8.4 +ttp_templates==0.1.3 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 539088f0d..25f5bc574 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,3 +14,5 @@ ciscoconfparse scp lxml>=4.3.0 ncclient +ttp +ttp_templates diff --git a/test/base/utils/ttp_templates/bad_template.txt b/test/base/utils/ttp_templates/bad_template.txt new file mode 100644 index 000000000..8797b2566 --- /dev/null +++ b/test/base/utils/ttp_templates/bad_template.txt @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/test/base/utils/ttp_templates/good_template.txt b/test/base/utils/ttp_templates/good_template.txt new file mode 100644 index 000000000..82a1e326f --- /dev/null +++ b/test/base/utils/ttp_templates/good_template.txt @@ -0,0 +1,5 @@ + +interface {{ interface }} + description {{ description | re(".+") }} +! {{ _end_ }} + \ No newline at end of file From f3ccdfff42686fa21419735e9e73f50fb629ba4f Mon Sep 17 00:00:00 2001 From: Denis Mulyalin Date: Fri, 11 Feb 2022 03:33:46 -0500 Subject: [PATCH 10/74] added ttp_parse tests --- test/base/test_helpers.py | 82 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/test/base/test_helpers.py b/test/base/test_helpers.py index 5b6d0b150..bb152a715 100644 --- a/test/base/test_helpers.py +++ b/test/base/test_helpers.py @@ -37,6 +37,20 @@ except ImportError: HAS_NETADDR = False +try: + from ttp import ttp # noqa + + HAS_TTP = True +except ImportError: + HAS_TTP = False + +try: + from ttp_templates import get_template # noqa + + HAS_TTP_TEMPLATES = True +except ImportError: + HAS_TTP_TEMPLATES = False + # NAPALM base import napalm.base.helpers import napalm.base.constants as C @@ -673,6 +687,74 @@ def test_sanitized_config(self): ret = napalm.base.helpers.sanitize_config(config, C.CISCO_SANITIZE_FILTERS) self.assertEqual(ret, expected) + def test_ttp_parse(self): + """ + Tests helper function ```ttp_parse```: + + * check if raises TemplateRenderException when template does not exist + * check if raises TemplateRenderException for /utils/ttp_templates/bad_template.txt + * check if returns expected result as output for ./utils/ttp_templates/good_template.txt + * check if returns expected result as output for inline template + * check if returns expected result as output for template from ttp templates + """ + + self.assertTrue( + HAS_TTP, "Install TTP: python3 -m pip install ttp" + ) # before anything else, let's see if TTP is available + _TTP_TEST_STRING = """ +interface Gi1/1 + description FOO +! +interface Gi1/2 + description BAR +! + """ + _TTP_TEST_TEMPLATE = '\ninterface {{ interface }}\n description {{ description | re(".+") }}\n! {{ _end_ }}\n' + _EXPECTED_RESULT = [ + {"description": "FOO", "interface": "Gi1/1"}, + {"description": "BAR", "interface": "Gi1/2"}, + ] + self.assertRaises( + napalm.base.exceptions.TemplateRenderException, + napalm.base.helpers.ttp_parse, + self.network_driver, + "__this_template_does_not_exist__", + _TTP_TEST_STRING, + ) + + self.assertRaises( + napalm.base.exceptions.TemplateRenderException, + napalm.base.helpers.ttp_parse, + self.network_driver, + "bad_template", + _TTP_TEST_STRING, + ) + + # use ./utils/ttp_templates/good_template.txt + result = napalm.base.helpers.ttp_parse( + self.network_driver, "good_template", _TTP_TEST_STRING + ) + self.assertEqual(result, _EXPECTED_RESULT) + + # use inline template + result = napalm.base.helpers.ttp_parse( + self.network_driver, _TTP_TEST_TEMPLATE, _TTP_TEST_STRING + ) + self.assertEqual(result, _EXPECTED_RESULT) + + self.assertTrue( + HAS_TTP_TEMPLATES, + "Install TTP Templates: python3 -m pip install ttp-templates", + ) # before anything else, let's see if TTP_Templates is available + + # use template from ttp templates package + result = napalm.base.helpers.ttp_parse( + self.network_driver, + "ttp://platform/test_platform_show_run_pipe_sec_interface.txt", + _TTP_TEST_STRING, + ) + self.assertEqual(result, _EXPECTED_RESULT) + class FakeNetworkDriver(NetworkDriver): def __init__(self): From e8a38cbacbd728ceab2cfaebdba0409f411c334b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 12 Feb 2022 16:20:25 +0000 Subject: [PATCH 11/74] Bump mypy from 0.910 to 0.931 Bumps [mypy](https://github.com/python/mypy) from 0.910 to 0.931. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.910...v0.931) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 672e35301..01911af13 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,4 +9,4 @@ pytest-pythonpath==0.7.4 pylama==7.7.1 mock==4.0.3 tox==3.24.4 -mypy==0.910 +mypy==0.931 From dc8a58552bd7b12576776439f2088d3db110454b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 12 Feb 2022 16:55:05 +0000 Subject: [PATCH 12/74] Bump tox from 3.24.4 to 3.24.5 Bumps [tox](https://github.com/tox-dev/tox) from 3.24.4 to 3.24.5. - [Release notes](https://github.com/tox-dev/tox/releases) - [Changelog](https://github.com/tox-dev/tox/blob/master/docs/changelog.rst) - [Commits](https://github.com/tox-dev/tox/compare/3.24.4...3.24.5) --- updated-dependencies: - dependency-name: tox dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 01911af13..7ccb27df2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,5 +8,5 @@ pytest-json==0.4.0 pytest-pythonpath==0.7.4 pylama==7.7.1 mock==4.0.3 -tox==3.24.4 +tox==3.24.5 mypy==0.931 From a0117431f678776478713c7b40932d065648fcbd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 12 Feb 2022 17:02:20 +0000 Subject: [PATCH 13/74] Bump tox from 3.23.1 to 3.24.3 Bumps [tox](https://github.com/tox-dev/tox) from 3.23.1 to 3.24.3. - [Release notes](https://github.com/tox-dev/tox/releases) - [Changelog](https://github.com/tox-dev/tox/blob/master/docs/changelog.rst) - [Commits](https://github.com/tox-dev/tox/compare/3.23.1...3.24.3) --- updated-dependencies: - dependency-name: tox dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 7ccb27df2..6f238e0c3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ -black==21.11b1 +black==22.1.0 coveralls==3.3.1 ddt==1.4.4 flake8-import-order==0.18.1 From 88480c24d4c8f344aa6d960dd96550602a14539b Mon Sep 17 00:00:00 2001 From: Mircea Ulinic Date: Sat, 12 Feb 2022 17:05:14 +0000 Subject: [PATCH 14/74] Black format for version 22.1.0 --- docs/conf.py | 8 +- test/base/validate/test_unit.py | 378 ++++++++++++++++---------------- 2 files changed, 193 insertions(+), 193 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 06e9baed6..cf56e0537 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -213,7 +213,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ("index", "napalm.tex", u"NAPALM Documentation", u"David Barroso", "manual") + ("index", "napalm.tex", "NAPALM Documentation", "David Barroso", "manual") ] # The name of an image file (relative to this directory) to place at the top of @@ -241,7 +241,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [("index", "napalm", u"NAPALM Documentation", [u"David Barroso"], 1)] +man_pages = [("index", "napalm", "NAPALM Documentation", ["David Barroso"], 1)] # If true, show URL addresses after external links. # man_show_urls = False @@ -256,8 +256,8 @@ ( "index", "napalm", - u"NAPALM Documentation", - u"David Barroso", + "NAPALM Documentation", + "David Barroso", "napalm", "One line description of project.", "Miscellaneous", diff --git a/test/base/validate/test_unit.py b/test/base/validate/test_unit.py index 08b333f39..108862994 100644 --- a/test/base/validate/test_unit.py +++ b/test/base/validate/test_unit.py @@ -8,96 +8,96 @@ ( {"list": [r"\d{2}", 1, 2]}, [1, 2, 33], - {u"complies": True, u"extra": [], u"missing": [], u"present": [r"\d{2}", 1, 2]}, + {"complies": True, "extra": [], "missing": [], "present": [r"\d{2}", 1, 2]}, ), ( {"list": [1, 2, 3]}, [1, 2, 3, 4, 5], - {u"complies": True, u"extra": [], u"missing": [], u"present": [1, 2, 3]}, + {"complies": True, "extra": [], "missing": [], "present": [1, 2, 3]}, ), ( {"list": [2, 1, 3]}, [3, 2, 1], - {u"complies": True, u"extra": [], u"missing": [], u"present": [2, 1, 3]}, + {"complies": True, "extra": [], "missing": [], "present": [2, 1, 3]}, ), ( {"list": [1, 2, {"list": [1, 2]}]}, [1, 2, [1, 2]], # {u'complies': True, u'extra': [], u'missing': [], u'present': [1, 2, [1, 2]]} { - u"complies": True, - u"extra": [], - u"missing": [], - u"present": [1, 2, {"list": [1, 2]}], + "complies": True, + "extra": [], + "missing": [], + "present": [1, 2, {"list": [1, 2]}], }, ), ( {"list": [r"\d{2}", 4, 3]}, [1, 2, 3], - {u"complies": False, u"extra": [], u"missing": [r"\d{2}", 4], u"present": [3]}, + {"complies": False, "extra": [], "missing": [r"\d{2}", 4], "present": [3]}, ), ( {"list": [{"list": [1, 2]}, 3]}, [1, 2, 3], { - u"complies": False, - u"extra": [], - u"missing": [{"list": [1, 2]}], - u"present": [3], + "complies": False, + "extra": [], + "missing": [{"list": [1, 2]}], + "present": [3], }, ), ( {"_mode": "strict", "list": [1, 2, 3]}, [1, 2, 3], - {u"complies": True, u"extra": [], u"missing": [], u"present": [1, 2, 3]}, + {"complies": True, "extra": [], "missing": [], "present": [1, 2, 3]}, ), ( {"_mode": "strict", "list": [1, 2, 3]}, [1, 2, 3, 4, 5], - {u"complies": False, u"extra": [4, 5], u"missing": [], u"present": [1, 2, 3]}, + {"complies": False, "extra": [4, 5], "missing": [], "present": [1, 2, 3]}, ), ( {"_mode": "strict", "list": [2, 1, 3]}, [3, 2, 1], - {u"complies": True, u"extra": [], u"missing": [], u"present": [2, 1, 3]}, + {"complies": True, "extra": [], "missing": [], "present": [2, 1, 3]}, ), ( {"_mode": "strict", "list": [1, 2, {"_mode": "strict", "list": [1, 2]}]}, [1, 2, [1, 2]], # {u'complies': True, u'extra': [], u'missing': [], u'present': [1, 2, [1, 2]]} { - u"complies": True, - u"extra": [], - u"missing": [], - u"present": [1, 2, {"list": [1, 2]}], + "complies": True, + "extra": [], + "missing": [], + "present": [1, 2, {"list": [1, 2]}], }, ), ( {"_mode": "strict", "list": [4, 3]}, [1, 2, 3], - {u"complies": False, u"extra": [1, 2], u"missing": [4], u"present": [3]}, + {"complies": False, "extra": [1, 2], "missing": [4], "present": [3]}, ), ( {"_mode": "strict", "list": [{"_mode": "strict", "list": [1, 2]}, 3]}, [1, 2, 3], { - u"complies": False, - u"extra": [1, 2], - u"missing": [{"list": [1, 2]}], - u"present": [3], + "complies": False, + "extra": [1, 2], + "missing": [{"list": [1, 2]}], + "present": [3], }, ), ( {"a": 1, "b": 2, "c": 3}, {"a": 1, "b": 2, "c": 3}, { - u"complies": True, - u"extra": [], - u"missing": [], - u"present": { - "a": {u"complies": True, u"nested": False}, - "b": {u"complies": True, u"nested": False}, - "c": {u"complies": True, u"nested": False}, + "complies": True, + "extra": [], + "missing": [], + "present": { + "a": {"complies": True, "nested": False}, + "b": {"complies": True, "nested": False}, + "c": {"complies": True, "nested": False}, }, }, ), @@ -105,18 +105,18 @@ {"a": 1, "b": 2, "c": 3}, {"a": 2, "b": 2, "c": 3}, { - u"complies": False, - u"extra": [], - u"missing": [], - u"present": { + "complies": False, + "extra": [], + "missing": [], + "present": { "a": { - u"actual_value": 2, - u"expected_value": 1, - u"complies": False, - u"nested": False, + "actual_value": 2, + "expected_value": 1, + "complies": False, + "nested": False, }, - "b": {u"complies": True, u"nested": False}, - "c": {u"complies": True, u"nested": False}, + "b": {"complies": True, "nested": False}, + "c": {"complies": True, "nested": False}, }, }, ), @@ -124,17 +124,17 @@ {"a": 1, "b": 2, "c": 3}, {"b": 1, "c": 3}, { - u"complies": False, - u"extra": [], - u"missing": ["a"], - u"present": { + "complies": False, + "extra": [], + "missing": ["a"], + "present": { "b": { - u"actual_value": 1, - u"expected_value": 2, - u"complies": False, - u"nested": False, + "actual_value": 1, + "expected_value": 2, + "complies": False, + "nested": False, }, - "c": {u"complies": True, u"nested": False}, + "c": {"complies": True, "nested": False}, }, }, ), @@ -142,13 +142,13 @@ {"a": 1, "b": 2, "c": {"A": 1, "B": 2}}, {"a": 1, "b": 2, "c": {"A": 1, "B": 2}}, { - u"complies": True, - u"extra": [], - u"missing": [], - u"present": { - "a": {u"complies": True, u"nested": False}, - "b": {u"complies": True, u"nested": False}, - "c": {u"complies": True, u"nested": True}, + "complies": True, + "extra": [], + "missing": [], + "present": { + "a": {"complies": True, "nested": False}, + "b": {"complies": True, "nested": False}, + "c": {"complies": True, "nested": True}, }, }, ), @@ -156,12 +156,12 @@ {"a": 1, "b": 2, "c": {"A": 1, "B": 2}}, {"a": 1, "b": 2, "d": {"A": 1, "B": 2}}, { - u"complies": False, - u"extra": [], - u"missing": ["c"], - u"present": { - "a": {u"complies": True, u"nested": False}, - "b": {u"complies": True, u"nested": False}, + "complies": False, + "extra": [], + "missing": ["c"], + "present": { + "a": {"complies": True, "nested": False}, + "b": {"complies": True, "nested": False}, }, }, ), @@ -169,29 +169,29 @@ {"a": 1, "b": 2, "c": {"A": 3, "B": 2}}, {"a": 1, "b": 2, "c": {"A": 1, "B": 2}}, { - u"complies": False, - u"extra": [], - u"missing": [], - u"present": { - "a": {u"complies": True, u"nested": False}, - "b": {u"complies": True, u"nested": False}, + "complies": False, + "extra": [], + "missing": [], + "present": { + "a": {"complies": True, "nested": False}, + "b": {"complies": True, "nested": False}, "c": { - u"complies": False, - u"diff": { - u"complies": False, - u"extra": [], - u"missing": [], - u"present": { + "complies": False, + "diff": { + "complies": False, + "extra": [], + "missing": [], + "present": { "A": { - u"actual_value": 1, - u"expected_value": 3, - u"complies": False, - u"nested": False, + "actual_value": 1, + "expected_value": 3, + "complies": False, + "nested": False, }, - "B": {u"complies": True, u"nested": False}, + "B": {"complies": True, "nested": False}, }, }, - u"nested": True, + "nested": True, }, }, }, @@ -200,28 +200,28 @@ {"a": 1, "b": 2, "c": {"A": 3, "B": 2}}, {"a": 1, "b": 2, "c": {"A": 1}}, { - u"complies": False, - u"extra": [], - u"missing": [], - u"present": { - "a": {u"complies": True, u"nested": False}, - "b": {u"complies": True, u"nested": False}, + "complies": False, + "extra": [], + "missing": [], + "present": { + "a": {"complies": True, "nested": False}, + "b": {"complies": True, "nested": False}, "c": { - u"complies": False, - u"diff": { - u"complies": False, - u"extra": [], - u"missing": ["B"], - u"present": { + "complies": False, + "diff": { + "complies": False, + "extra": [], + "missing": ["B"], + "present": { "A": { - u"actual_value": 1, - u"expected_value": 3, - u"complies": False, - u"nested": False, + "actual_value": 1, + "expected_value": 3, + "complies": False, + "nested": False, } }, }, - u"nested": True, + "nested": True, }, }, }, @@ -230,13 +230,13 @@ {"_mode": "strict", "a": 1, "b": 2, "c": 3}, {"a": 1, "b": 2, "c": 3}, { - u"complies": True, - u"extra": [], - u"missing": [], - u"present": { - "a": {u"complies": True, u"nested": False}, - "b": {u"complies": True, u"nested": False}, - "c": {u"complies": True, u"nested": False}, + "complies": True, + "extra": [], + "missing": [], + "present": { + "a": {"complies": True, "nested": False}, + "b": {"complies": True, "nested": False}, + "c": {"complies": True, "nested": False}, }, }, ), @@ -244,18 +244,18 @@ {"_mode": "strict", "a": 1, "b": 2, "c": 3}, {"a": 2, "b": 2, "c": 3}, { - u"complies": False, - u"extra": [], - u"missing": [], - u"present": { + "complies": False, + "extra": [], + "missing": [], + "present": { "a": { - u"actual_value": 2, - u"expected_value": 1, - u"complies": False, - u"nested": False, + "actual_value": 2, + "expected_value": 1, + "complies": False, + "nested": False, }, - "b": {u"complies": True, u"nested": False}, - "c": {u"complies": True, u"nested": False}, + "b": {"complies": True, "nested": False}, + "c": {"complies": True, "nested": False}, }, }, ), @@ -263,17 +263,17 @@ {"_mode": "strict", "a": 1, "b": 2, "c": 3}, {"b": 1, "c": 3}, { - u"complies": False, - u"extra": [], - u"missing": ["a"], - u"present": { + "complies": False, + "extra": [], + "missing": ["a"], + "present": { "b": { - u"actual_value": 1, - u"expected_value": 2, - u"complies": False, - u"nested": False, + "actual_value": 1, + "expected_value": 2, + "complies": False, + "nested": False, }, - "c": {u"complies": True, u"nested": False}, + "c": {"complies": True, "nested": False}, }, }, ), @@ -281,13 +281,13 @@ {"_mode": "strict", "a": 1, "b": 2, "c": {"_mode": "strict", "A": 1, "B": 2}}, {"a": 1, "b": 2, "c": {"A": 1, "B": 2}}, { - u"complies": True, - u"extra": [], - u"missing": [], - u"present": { - "a": {u"complies": True, u"nested": False}, - "b": {u"complies": True, u"nested": False}, - "c": {u"complies": True, u"nested": True}, + "complies": True, + "extra": [], + "missing": [], + "present": { + "a": {"complies": True, "nested": False}, + "b": {"complies": True, "nested": False}, + "c": {"complies": True, "nested": True}, }, }, ), @@ -295,12 +295,12 @@ {"_mode": "strict", "a": 1, "b": 2, "c": {"_mode": "strict", "A": 1, "B": 2}}, {"a": 1, "b": 2, "d": {"A": 1, "B": 2}}, { - u"complies": False, - u"extra": ["d"], - u"missing": ["c"], - u"present": { - "a": {u"complies": True, u"nested": False}, - "b": {u"complies": True, u"nested": False}, + "complies": False, + "extra": ["d"], + "missing": ["c"], + "present": { + "a": {"complies": True, "nested": False}, + "b": {"complies": True, "nested": False}, }, }, ), @@ -308,29 +308,29 @@ {"_mode": "strict", "a": 1, "b": 2, "c": {"_mode": "strict", "A": 3, "B": 2}}, {"a": 1, "b": 2, "c": {"A": 1, "B": 2}}, { - u"complies": False, - u"extra": [], - u"missing": [], - u"present": { - "a": {u"complies": True, u"nested": False}, - "b": {u"complies": True, u"nested": False}, + "complies": False, + "extra": [], + "missing": [], + "present": { + "a": {"complies": True, "nested": False}, + "b": {"complies": True, "nested": False}, "c": { - u"complies": False, - u"diff": { - u"complies": False, - u"extra": [], - u"missing": [], - u"present": { + "complies": False, + "diff": { + "complies": False, + "extra": [], + "missing": [], + "present": { "A": { - u"actual_value": 1, - u"expected_value": 3, - u"complies": False, - u"nested": False, + "actual_value": 1, + "expected_value": 3, + "complies": False, + "nested": False, }, - "B": {u"complies": True, u"nested": False}, + "B": {"complies": True, "nested": False}, }, }, - u"nested": True, + "nested": True, }, }, }, @@ -339,28 +339,28 @@ {"_mode": "strict", "a": 1, "b": 2, "c": {"_mode": "strict", "A": 3, "B": 2}}, {"a": 1, "b": 2, "c": {"A": 1, "C": 4}}, { - u"complies": False, - u"extra": [], - u"missing": [], - u"present": { - "a": {u"complies": True, u"nested": False}, - "b": {u"complies": True, u"nested": False}, + "complies": False, + "extra": [], + "missing": [], + "present": { + "a": {"complies": True, "nested": False}, + "b": {"complies": True, "nested": False}, "c": { - u"complies": False, - u"diff": { - u"complies": False, - u"extra": ["C"], - u"missing": ["B"], - u"present": { + "complies": False, + "diff": { + "complies": False, + "extra": ["C"], + "missing": ["B"], + "present": { "A": { - u"actual_value": 1, - u"expected_value": 3, - u"complies": False, - u"nested": False, + "actual_value": 1, + "expected_value": 3, + "complies": False, + "nested": False, } }, }, - u"nested": True, + "nested": True, }, }, }, @@ -369,28 +369,28 @@ {"_mode": "strict", "a": 1, "b": 2, "c": {"_mode": "strict", "A": 3, "B": 2}}, {"a": 1, "b": 2, "c": {"A": 1, "C": 4}}, { - u"complies": False, - u"extra": [], - u"missing": [], - u"present": { - "a": {u"complies": True, u"nested": False}, - "b": {u"complies": True, u"nested": False}, + "complies": False, + "extra": [], + "missing": [], + "present": { + "a": {"complies": True, "nested": False}, + "b": {"complies": True, "nested": False}, "c": { - u"complies": False, - u"diff": { - u"complies": False, - u"extra": ["C"], - u"missing": ["B"], - u"present": { + "complies": False, + "diff": { + "complies": False, + "extra": ["C"], + "missing": ["B"], + "present": { "A": { - u"actual_value": 1, - u"expected_value": 3, - u"complies": False, - u"nested": False, + "actual_value": 1, + "expected_value": 3, + "complies": False, + "nested": False, } }, }, - u"nested": True, + "nested": True, }, }, }, From 32969bc6adde4f2866ebca4719da5a4c620bc783 Mon Sep 17 00:00:00 2001 From: Mircea Ulinic Date: Sat, 12 Feb 2022 18:28:20 +0000 Subject: [PATCH 15/74] Make pylama happy --- test/nxapi_plumbing/test_config.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/nxapi_plumbing/test_config.py b/test/nxapi_plumbing/test_config.py index 1941fcca4..5fbc193b3 100644 --- a/test/nxapi_plumbing/test_config.py +++ b/test/nxapi_plumbing/test_config.py @@ -50,16 +50,16 @@ def test_config_jsonrpc_raises_NXAPICommandError_on_non_200_config_error( ): with pytest.raises( NXAPICommandError, match='The command "bogus command" gave the error' - ) as e: - result = mock_pynxos_device.config("bogus command") + ): + mock_pynxos_device.config("bogus command") @pytest.mark.parametrize("mock_pynxos_device_xml", [500], indirect=True) def test_config_xml_raises_NXAPIPostError_on_non_200_post_error(mock_pynxos_device_xml): with pytest.raises( NXAPIPostError, match="Invalid status code returned on NX-API POST" - ) as e: - result = mock_pynxos_device_xml.config("logging history size 200") + ): + mock_pynxos_device_xml.config("logging history size 200") @pytest.mark.parametrize("mock_pynxos_device_xml", [200], indirect=True) @@ -68,5 +68,5 @@ def test_config_xml_raises_NXAPICommandError_on_200_config_error( ): with pytest.raises( NXAPICommandError, match='The command "bogus command" gave the error' - ) as e: - result = mock_pynxos_device_xml.config("bogus command") + ): + mock_pynxos_device_xml.config("bogus command") From 82ee0ccff95a460ed2d2d4b61f9038f28a04d96f Mon Sep 17 00:00:00 2001 From: Mircea Ulinic Date: Sat, 12 Feb 2022 18:39:11 +0000 Subject: [PATCH 16/74] Release version 3.4.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b6857fa2e..3290b713b 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ setup( name="napalm", - version="3.3.1", + version="3.4.0", packages=find_packages(exclude=("test*",)), test_suite="test_base", author="David Barroso, Kirk Byers, Mircea Ulinic", From be14dc13868ff8f3af4f5abdbd97fada2deb558d Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Mon, 22 Mar 2021 15:12:46 +0100 Subject: [PATCH 17/74] Add models and annotate NetworkDriver class with types --- napalm/base/base.py | 186 ++++++++++++-------- napalm/base/test/base.py | 80 ++++----- napalm/base/test/getters.py | 84 ++++----- napalm/base/test/models.py | 302 ++++++++++++++++++++------------ test/nxapi_plumbing/conftest.py | 2 +- test/nxos/test_getters.py | 2 +- test/nxos_ssh/test_getters.py | 2 +- 7 files changed, 389 insertions(+), 269 deletions(-) diff --git a/napalm/base/base.py b/napalm/base/base.py index 4e245e1ca..b922aab88 100644 --- a/napalm/base/base.py +++ b/napalm/base/base.py @@ -12,7 +12,11 @@ # License for the specific language governing permissions and limitations under # the License. +from __future__ import annotations + import sys +from types import TracebackType +from typing import Optional, Dict, Type, Any, Literal, List from netmiko import ConnectHandler, NetMikoTimeoutException @@ -22,26 +26,35 @@ from napalm.base import constants as c from napalm.base import validate from napalm.base.exceptions import ConnectionException +from napalm.base.test import models +from napalm.base.test.models import NTPStats class NetworkDriver(object): - def __init__(self, hostname, username, password, timeout=60, optional_args=None): + def __init__( + self, + hostname: str, + username: str, + password: str, + timeout: int = 60, + optional_args: Dict = None, + ) -> None: """ This is the base class you have to inherit from when writing your own Network Driver to manage any device. You will, in addition, have to override all the methods specified on this class. Make sure you follow the guidelines for every method and that you return the correct data. - :param hostname: (str) IP or FQDN of the device you want to connect to. - :param username: (str) Username you want to use - :param password: (str) Password - :param timeout: (int) Time in seconds to wait for the device to respond. - :param optional_args: (dict) Pass additional arguments to underlying driver + :param hostname: IP or FQDN of the device you want to connect to. + :param username: Username you want to use + :param password: Password + :param timeout: Time in seconds to wait for the device to respond. + :param optional_args: Pass additional arguments to underlying driver :return: """ raise NotImplementedError - def __enter__(self): + def __enter__(self) -> "NetworkDriver": try: self.open() return self @@ -52,7 +65,12 @@ def __enter__(self): else: raise - def __exit__(self, exc_type, exc_value, exc_traceback): + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + exc_traceback: Optional[TracebackType], + ) -> Optional[Literal[False]]: self.close() if exc_type is not None and ( exc_type.__name__ not in dir(napalm.base.exceptions) @@ -66,7 +84,7 @@ def __exit__(self, exc_type, exc_value, exc_traceback): print(epilog) return False - def __del__(self): + def __del__(self) -> None: """ This method is used to cleanup when the program is terminated suddenly. We need to make sure the connection is closed properly and the configuration DB @@ -78,7 +96,9 @@ def __del__(self): except Exception: pass - def _netmiko_open(self, device_type, netmiko_optional_args=None): + def _netmiko_open( + self, device_type: str, netmiko_optional_args: Optional[Dict] = None + ) -> ConnectHandler: """Standardized method of creating a Netmiko connection using napalm attributes.""" if netmiko_optional_args is None: netmiko_optional_args = {} @@ -104,26 +124,26 @@ def _netmiko_open(self, device_type, netmiko_optional_args=None): return self._netmiko_device - def _netmiko_close(self): + def _netmiko_close(self) -> None: """Standardized method of closing a Netmiko connection.""" if getattr(self, "_netmiko_device", None): self._netmiko_device.disconnect() self._netmiko_device = None self.device = None - def open(self): + def open(self) -> None: """ Opens a connection to the device. """ raise NotImplementedError - def close(self): + def close(self) -> None: """ Closes the connection to the device. """ raise NotImplementedError - def is_alive(self): + def is_alive(self) -> models.AliveDict: """ Returns a flag with the connection state. Depends on the nature of API used by each driver. @@ -133,7 +153,7 @@ def is_alive(self): """ raise NotImplementedError - def pre_connection_tests(self): + def pre_connection_tests(self) -> None: """ This is a helper function used by the cli tool cl_napalm_show_tech. Drivers can override this method to do some tests, show information, enable debugging, etc. @@ -141,7 +161,7 @@ def pre_connection_tests(self): """ raise NotImplementedError - def connection_tests(self): + def connection_tests(self) -> None: """ This is a helper function used by the cli tool cl_napalm_show_tech. Drivers can override this method to do some tests, show information, enable debugging, etc. @@ -149,7 +169,7 @@ def connection_tests(self): """ raise NotImplementedError - def post_connection_tests(self): + def post_connection_tests(self) -> None: """ This is a helper function used by the cli tool cl_napalm_show_tech. Drivers can override this method to do some tests, show information, enable debugging, etc. @@ -158,8 +178,12 @@ def post_connection_tests(self): raise NotImplementedError def load_template( - self, template_name, template_source=None, template_path=None, **template_vars - ): + self, + template_name: str, + template_source: Optional[str] = None, + template_path: Optional[str] = None, + **template_vars: Any + ) -> None: """ Will load a templated configuration on the device. @@ -185,7 +209,9 @@ def load_template( **template_vars ) - def load_replace_candidate(self, filename=None, config=None): + def load_replace_candidate( + self, filename: Optional[str] = None, config: Optional[str] = None + ) -> None: """ Populates the candidate configuration. You can populate it from a file or from a string. If you send both a filename and a string containing the configuration, the file takes @@ -201,7 +227,9 @@ def load_replace_candidate(self, filename=None, config=None): """ raise NotImplementedError - def load_merge_candidate(self, filename=None, config=None): + def load_merge_candidate( + self, filename: Optional[str] = None, config: Optional[str] = None + ) -> None: """ Populates the candidate configuration. You can populate it from a file or from a string. If you send both a filename and a string containing the configuration, the file takes @@ -217,7 +245,7 @@ def load_merge_candidate(self, filename=None, config=None): """ raise NotImplementedError - def compare_config(self): + def compare_config(self) -> str: """ :return: A string showing the difference between the running configuration and the \ candidate configuration. The running_config is loaded automatically just before doing the \ @@ -225,7 +253,7 @@ def compare_config(self): """ raise NotImplementedError - def commit_config(self, message="", revert_in=None): + def commit_config(self, message: str = "", revert_in: Optional[int] = None) -> None: """ Commits the changes requested by the method load_replace_candidate or load_merge_candidate. @@ -242,7 +270,7 @@ def commit_config(self, message="", revert_in=None): """ raise NotImplementedError - def confirm_commit(self): + def confirm_commit(self) -> None: """ Confirm the changes requested via commit_config when commit_confirm=True. @@ -250,19 +278,19 @@ def confirm_commit(self): """ raise NotImplementedError - def has_pending_commit(self): + def has_pending_commit(self) -> bool: """ :return Boolean indicating if a commit_config that needs confirmed is in process. """ raise NotImplementedError - def discard_config(self): + def discard_config(self) -> None: """ Discards the configuration loaded into the candidate. """ raise NotImplementedError - def rollback(self): + def rollback(self) -> None: """ If changes were made, revert changes to the original state. @@ -270,7 +298,7 @@ def rollback(self): """ raise NotImplementedError - def get_facts(self): + def get_facts(self) -> models.FactsDict: """ Returns a dictionary containing the following information: * uptime - Uptime of the device in seconds. @@ -298,7 +326,7 @@ def get_facts(self): """ raise NotImplementedError - def get_interfaces(self): + def get_interfaces(self) -> Dict[str, models.InterfaceDict]: """ Returns a dictionary of dictionaries. The keys for the first dictionary will be the \ interfaces in the devices. The inner dictionary will containing the following data for \ @@ -359,7 +387,7 @@ def get_interfaces(self): """ raise NotImplementedError - def get_lldp_neighbors(self): + def get_lldp_neighbors(self) -> Dict[str, models.LLDPNeighborDict]: """ Returns a dictionary where the keys are local ports and the value is a list of \ dictionaries with the following information: @@ -405,7 +433,7 @@ def get_lldp_neighbors(self): """ raise NotImplementedError - def get_bgp_neighbors(self): + def get_bgp_neighbors(self) -> Dict[str, models.BGPStateNeighborsPerVRFDict]: """ Returns a dictionary of dictionaries. The keys for the first dictionary will be the vrf (global if no vrf). The inner dictionary will contain the following data for each vrf: @@ -462,7 +490,7 @@ def get_bgp_neighbors(self): """ raise NotImplementedError - def get_environment(self): + def get_environment(self) -> models.EnvironmentDict: """ Returns a dictionary where: @@ -484,7 +512,7 @@ def get_environment(self): """ raise NotImplementedError - def get_interfaces_counters(self): + def get_interfaces_counters(self) -> Dict[str, models.InterfaceCounterDict]: """ Returns a dictionary of dictionaries where the first key is an interface name and the inner dictionary contains the following keys: @@ -551,7 +579,9 @@ def get_interfaces_counters(self): """ raise NotImplementedError - def get_lldp_neighbors_detail(self, interface=""): + def get_lldp_neighbors_detail( + self, interface: str = "" + ) -> models.LLDPNeighborsDetailDict: """ Returns a detailed view of the LLDP neighbors as a dictionary containing lists of dictionaries for each interface. @@ -599,7 +629,9 @@ def get_lldp_neighbors_detail(self, interface=""): """ raise NotImplementedError - def get_bgp_config(self, group="", neighbor=""): + def get_bgp_config( + self, group: str = "", neighbor: str = "" + ) -> models.BPGConfigGroupDict: """ Returns a dictionary containing the BGP configuration. Can return either the whole config, either the config only for a group or neighbor. @@ -698,7 +730,7 @@ def get_bgp_config(self, group="", neighbor=""): """ raise NotImplementedError - def cli(self, commands): + def cli(self, commands: List[str]) -> Dict[str, str]: """ Will execute a list of commands and return the output in a dictionary format. @@ -726,7 +758,9 @@ def cli(self, commands): """ raise NotImplementedError - def get_bgp_neighbors_detail(self, neighbor_address=""): + def get_bgp_neighbors_detail( + self, neighbor_address: str = "" + ) -> Dict[str, models.PeerDetailsDict]: """ Returns a detailed view of the BGP neighbors as a dictionary of lists. @@ -821,7 +855,7 @@ def get_bgp_neighbors_detail(self, neighbor_address=""): """ raise NotImplementedError - def get_arp_table(self, vrf=""): + def get_arp_table(self, vrf: str = "") -> models.ARPTableDict: """ Returns a list of dictionaries having the following set of keys: @@ -856,7 +890,7 @@ def get_arp_table(self, vrf=""): """ raise NotImplementedError - def get_ntp_peers(self): + def get_ntp_peers(self) -> Dict[str, models.NTPPeerDict]: """ Returns the NTP peers configuration as dictionary. @@ -876,7 +910,7 @@ def get_ntp_peers(self): raise NotImplementedError - def get_ntp_servers(self): + def get_ntp_servers(self) -> Dict[str, models.NTPServerDict]: """ Returns the NTP servers configuration as dictionary. @@ -896,7 +930,7 @@ def get_ntp_servers(self): raise NotImplementedError - def get_ntp_stats(self): + def get_ntp_stats(self) -> List[NTPStats]: """ Returns a list of NTP synchronization statistics. @@ -933,7 +967,7 @@ def get_ntp_stats(self): """ raise NotImplementedError - def get_interfaces_ip(self): + def get_interfaces_ip(self) -> models.InterfacesIPDict: """ Returns all configured IP addresses on all interfaces as a dictionary of dictionaries. @@ -987,7 +1021,7 @@ def get_interfaces_ip(self): """ raise NotImplementedError - def get_mac_address_table(self): + def get_mac_address_table(self) -> List[models.MACAdressTable]: """ Returns a lists of dictionaries. Each dictionary represents an entry in the MAC Address @@ -1038,7 +1072,9 @@ def get_mac_address_table(self): """ raise NotImplementedError - def get_route_to(self, destination="", protocol="", longer=False): + def get_route_to( + self, destination: str = "", protocol: str = "", longer: bool = False + ) -> Dict[str, models.RouteDict]: """ Returns a dictionary of dictionaries containing details of all available routes to a @@ -1113,7 +1149,7 @@ def get_route_to(self, destination="", protocol="", longer=False): """ raise NotImplementedError - def get_snmp_information(self): + def get_snmp_information(self) -> Dict[str, models.SNMPDict]: """ Returns a dict of dicts containing SNMP configuration. @@ -1157,7 +1193,7 @@ def get_snmp_information(self): """ raise NotImplementedError - def get_probes_config(self): + def get_probes_config(self) -> Dict[str, models.ProbeTestDict]: """ Returns a dictionary with the probes configured on the device. Probes can be either RPM on JunOS devices, either SLA on IOS-XR. Other vendors do not @@ -1195,7 +1231,7 @@ def get_probes_config(self): """ raise NotImplementedError - def get_probes_results(self): + def get_probes_results(self) -> Dict[str, models.ProbeTestResultDict]: """ Returns a dictionary with the results of the probes. The keys of the main dictionary represent the name of the probes. @@ -1266,15 +1302,15 @@ def get_probes_results(self): def ping( self, - destination, - source=c.PING_SOURCE, - ttl=c.PING_TTL, - timeout=c.PING_TIMEOUT, - size=c.PING_SIZE, - count=c.PING_COUNT, - vrf=c.PING_VRF, - source_interface=c.PING_SOURCE_INTERFACE, - ): + destination: str, + source: str = c.PING_SOURCE, + ttl: int = c.PING_TTL, + timeout: int = c.PING_TIMEOUT, + size: int = c.PING_SIZE, + count: int = c.PING_COUNT, + vrf: str = c.PING_VRF, + source_interface: str=c.PING_SOURCE_INTERFACE, + ) -> models.PingResultDict: """ Executes ping on the device and returns a dictionary with the result @@ -1348,12 +1384,12 @@ def ping( def traceroute( self, - destination, - source=c.TRACEROUTE_SOURCE, - ttl=c.TRACEROUTE_TTL, - timeout=c.TRACEROUTE_TIMEOUT, - vrf=c.TRACEROUTE_VRF, - ): + destination: str, + source: str = c.TRACEROUTE_SOURCE, + ttl: int = c.TRACEROUTE_TTL, + timeout: int = c.TRACEROUTE_TIMEOUT, + vrf: str = c.TRACEROUTE_VRF, + ) -> models.TracerouteResultDict: """ Executes traceroute on the device and returns a dictionary with the result. @@ -1465,7 +1501,7 @@ def traceroute( """ raise NotImplementedError - def get_users(self): + def get_users(self) -> Dict[str, models.UsersDict]: """ Returns a dictionary with the configured users. The keys of the main dictionary represents the username. The values represent the details @@ -1497,7 +1533,7 @@ def get_users(self): """ raise NotImplementedError - def get_optics(self): + def get_optics(self) -> Dict[str, models.OpticsDict]: """Fetches the power usage on the various transceivers installed on the switch (in dbm), and returns a view that conforms with the openconfig model openconfig-platform-transceiver.yang @@ -1561,7 +1597,9 @@ def get_optics(self): """ raise NotImplementedError - def get_config(self, retrieve="all", full=False, sanitized=False): + def get_config( + self, retrieve: str = "all", full: bool = False, sanitized: bool = False + ) -> models.ConfigDict: """ Return the configuration of a device. @@ -1584,7 +1622,9 @@ def get_config(self, retrieve="all", full=False, sanitized=False): """ raise NotImplementedError - def get_network_instances(self, name=""): + def get_network_instances( + self, name: str = "" + ) -> Dict[str, models.NetworkInstanceDict]: """ Return a dictionary of network instances (VRFs) configured, including default/global @@ -1636,7 +1676,7 @@ def get_network_instances(self, name=""): """ raise NotImplementedError - def get_firewall_policies(self): + def get_firewall_policies(self) -> Dict[str, List[models.FirewallPolicyDict]]: """ Returns a dictionary of lists of dictionaries where the first key is an unique policy name and the inner dictionary contains the following keys: @@ -1677,7 +1717,7 @@ def get_firewall_policies(self): """ raise NotImplementedError - def get_ipv6_neighbors_table(self): + def get_ipv6_neighbors_table(self) -> List[models.IPV6NeighborDict]: """ Get IPv6 neighbors table information. @@ -1710,7 +1750,7 @@ def get_ipv6_neighbors_table(self): """ raise NotImplementedError - def get_vlans(self): + def get_vlans(self) -> Dict[str, models.VlanDict]: """ Return structure being spit balled is as follows. * vlan_id (int) @@ -1732,7 +1772,11 @@ def get_vlans(self): """ raise NotImplementedError - def compliance_report(self, validation_file=None, validation_source=None): + def compliance_report( + self, + validation_file: Optional[str] = None, + validation_source: Optional[str] = None, + ) -> Dict: """ Return a compliance report. @@ -1748,7 +1792,7 @@ def compliance_report(self, validation_file=None, validation_source=None): self, validation_file=validation_file, validation_source=validation_source ) - def _canonical_int(self, interface): + def _canonical_int(self, interface: str) -> str: """Expose the helper function within this class.""" if self.use_canonical_interface is True: return napalm.base.helpers.canonical_interface_name( diff --git a/napalm/base/test/base.py b/napalm/base/test/base.py index 9a90b81d4..17e72f009 100644 --- a/napalm/base/test/base.py +++ b/napalm/base/test/base.py @@ -192,7 +192,7 @@ def test_get_firewall_policies(self): for policy_name, policy_details in policies.items(): for policy_term in policy_details: result = result and self._test_model( - models.firewall_policies, policy_term + models.FirewallPolicyDict, policy_term ) self.assertTrue(result) @@ -202,7 +202,7 @@ def test_is_alive(self): alive = self.device.is_alive() except NotImplementedError: raise SkipTest() - result = self._test_model(models.alive, alive) + result = self._test_model(models.AliveDict, alive) self.assertTrue(result) def test_get_facts(self): @@ -210,7 +210,7 @@ def test_get_facts(self): facts = self.device.get_facts() except NotImplementedError: raise SkipTest() - result = self._test_model(models.facts, facts) + result = self._test_model(models.FactsDict, facts) self.assertTrue(result) def test_get_interfaces(self): @@ -221,7 +221,7 @@ def test_get_interfaces(self): result = len(get_interfaces) > 0 for interface, interface_data in get_interfaces.items(): - result = result and self._test_model(models.interface, interface_data) + result = result and self._test_model(models.InterfaceDict, interface_data) self.assertTrue(result) @@ -234,7 +234,7 @@ def test_get_lldp_neighbors(self): for interface, neighbor_list in get_lldp_neighbors.items(): for neighbor in neighbor_list: - result = result and self._test_model(models.lldp_neighbors, neighbor) + result = result and self._test_model(models.LLDPNeighborDict, neighbor) self.assertTrue(result) @@ -247,7 +247,7 @@ def test_get_interfaces_counters(self): for interface, interface_data in get_interfaces_counters.items(): result = result and self._test_model( - models.interface_counters, interface_data + models.InterfaceCounterDict, interface_data ) self.assertTrue(result) @@ -260,18 +260,18 @@ def test_get_environment(self): result = len(environment) > 0 for fan, fan_data in environment["fans"].items(): - result = result and self._test_model(models.fan, fan_data) + result = result and self._test_model(models.FanDict, fan_data) for power, power_data in environment["power"].items(): - result = result and self._test_model(models.power, power_data) + result = result and self._test_model(models.PowerDict, power_data) for temperature, temperature_data in environment["temperature"].items(): - result = result and self._test_model(models.temperature, temperature_data) + result = result and self._test_model(models.TemperatureDict, temperature_data) for cpu, cpu_data in environment["cpu"].items(): - result = result and self._test_model(models.cpu, cpu_data) + result = result and self._test_model(models.CPUDict, cpu_data) - result = result and self._test_model(models.memory, environment["memory"]) + result = result and self._test_model(models.MemoryDict, environment["memory"]) self.assertTrue(result) @@ -291,10 +291,10 @@ def test_get_bgp_neighbors(self): print("router_id is not {}".format(str)) for peer, peer_data in vrf_data["peers"].items(): - result = result and self._test_model(models.peer, peer_data) + result = result and self._test_model(models.PeerDict, peer_data) for af, af_data in peer_data["address_family"].items(): - result = result and self._test_model(models.af, af_data) + result = result and self._test_model(models.AFDict, af_data) self.assertTrue(result) @@ -308,7 +308,7 @@ def test_get_lldp_neighbors_detail(self): for interface, neighbor_list in get_lldp_neighbors_detail.items(): for neighbor in neighbor_list: result = result and self._test_model( - models.lldp_neighbors_detail, neighbor + models.LLDPNeighborsDetailDict, neighbor ) self.assertTrue(result) @@ -321,10 +321,10 @@ def test_get_bgp_config(self): result = len(get_bgp_config) > 0 for bgp_group in get_bgp_config.values(): - result = result and self._test_model(models.bgp_config_group, bgp_group) + result = result and self._test_model(models.BPGConfigGroupDict, bgp_group) for bgp_neighbor in bgp_group.get("neighbors", {}).values(): result = result and self._test_model( - models.bgp_config_neighbor, bgp_neighbor + models.BGPConfigNeighborDict, bgp_neighbor ) self.assertTrue(result) @@ -342,7 +342,7 @@ def test_get_bgp_neighbors_detail(self): for remote_as, neighbor_list in vrf_ases.items(): result = result and isinstance(remote_as, int) for neighbor in neighbor_list: - result = result and self._test_model(models.peer_details, neighbor) + result = result and self._test_model(models.PeerDetailsDict, neighbor) self.assertTrue(result) @@ -354,7 +354,7 @@ def test_get_arp_table(self): result = len(get_arp_table) > 0 for arp_entry in get_arp_table: - result = result and self._test_model(models.arp_table, arp_entry) + result = result and self._test_model(models.ARPTableDict, arp_entry) self.assertTrue(result) @@ -366,7 +366,7 @@ def test_get_arp_table_with_vrf(self): result = len(get_arp_table) > 0 for arp_entry in get_arp_table: - result = result and self._test_model(models.arp_table, arp_entry) + result = result and self._test_model(models.ARPTableDict, arp_entry) self.assertTrue(result) @@ -379,7 +379,7 @@ def test_get_ntp_peers(self): for peer, peer_details in get_ntp_peers.items(): result = result and isinstance(peer, str) - result = result and self._test_model(models.ntp_peer, peer_details) + result = result and self._test_model(models.NTPPeerDict, peer_details) self.assertTrue(result) @@ -393,7 +393,7 @@ def test_get_ntp_servers(self): for server, server_details in get_ntp_servers.items(): result = result and isinstance(server, str) - result = result and self._test_model(models.ntp_server, server_details) + result = result and self._test_model(models.NTPServerDict, server_details) self.assertTrue(result) @@ -405,7 +405,7 @@ def test_get_ntp_stats(self): result = len(get_ntp_stats) > 0 for ntp_peer_details in get_ntp_stats: - result = result and self._test_model(models.ntp_stats, ntp_peer_details) + result = result and self._test_model(models.NTPStats, ntp_peer_details) self.assertTrue(result) @@ -420,9 +420,9 @@ def test_get_interfaces_ip(self): ipv4 = interface_details.get("ipv4", {}) ipv6 = interface_details.get("ipv6", {}) for ip, ip_details in ipv4.items(): - result = result and self._test_model(models.interfaces_ip, ip_details) + result = result and self._test_model(models.InterfacesIPDictEntry, ip_details) for ip, ip_details in ipv6.items(): - result = result and self._test_model(models.interfaces_ip, ip_details) + result = result and self._test_model(models.InterfacesIPDictEntry, ip_details) self.assertTrue(result) @@ -435,7 +435,7 @@ def test_get_mac_address_table(self): for mac_table_entry in get_mac_address_table: result = result and self._test_model( - models.mac_address_table, mac_table_entry + models.MACAdressTable, mac_table_entry ) self.assertTrue(result) @@ -454,7 +454,7 @@ def test_get_route_to(self): for prefix, routes in get_route_to.items(): for route in routes: - result = result and self._test_model(models.route, route) + result = result and self._test_model(models.RouteDict, route) self.assertTrue(result) def test_get_snmp_information(self): @@ -466,10 +466,10 @@ def test_get_snmp_information(self): result = len(get_snmp_information) > 0 for snmp_entry in get_snmp_information: - result = result and self._test_model(models.snmp, get_snmp_information) + result = result and self._test_model(models.SNMPDict, get_snmp_information) for community, community_data in get_snmp_information["community"].items(): - result = result and self._test_model(models.snmp_community, community_data) + result = result and self._test_model(models.SNMPCommunityDict, community_data) self.assertTrue(result) @@ -483,7 +483,7 @@ def test_get_probes_config(self): for probe_name, probe_tests in get_probes_config.items(): for test_name, test_config in probe_tests.items(): - result = result and self._test_model(models.probe_test, test_config) + result = result and self._test_model(models.ProbeTestDict, test_config) self.assertTrue(result) @@ -497,7 +497,7 @@ def test_get_probes_results(self): for probe_name, probe_tests in get_probes_results.items(): for test_name, test_results in probe_tests.items(): result = result and self._test_model( - models.probe_test_results, test_results + models.ProbeTestResultDict, test_results ) self.assertTrue(result) @@ -511,10 +511,10 @@ def test_ping(self): result = isinstance(get_ping.get("success"), dict) ping_results = get_ping.get("success", {}) - result = result and self._test_model(models.ping, ping_results) + result = result and self._test_model(models.PingDict, ping_results) for ping_result in ping_results.get("results", []): - result = result and self._test_model(models.ping_result, ping_result) + result = result and self._test_model(models.PingResultDictEntry, ping_result) self.assertTrue(result) @@ -530,7 +530,7 @@ def test_traceroute(self): for hope_id, hop_result in traceroute_results.items(): for probe_id, probe_result in hop_result.get("probes", {}).items(): - result = result and self._test_model(models.traceroute, probe_result) + result = result and self._test_model(models.TracerouteDict, probe_result) self.assertTrue(result) @@ -543,7 +543,7 @@ def test_get_users(self): result = len(get_users) for user, user_details in get_users.items(): - result = result and self._test_model(models.users, user_details) + result = result and self._test_model(models.UsersDict, user_details) result = result and (0 <= user_details.get("level") <= 15) self.assertTrue(result) @@ -578,7 +578,7 @@ def test_get_config(self): raise SkipTest() assert isinstance(get_config, dict) - assert self._test_model(models.config, get_config) + assert self._test_model(models.ConfigDict, get_config) def test_get_config_filtered(self): """Test get_config method.""" @@ -602,13 +602,13 @@ def test_get_network_instances(self): result = isinstance(get_network_instances, dict) for network_instance_name, network_instance in get_network_instances.items(): result = result and self._test_model( - models.network_instance, network_instance + models.NetworkInstanceDict, network_instance ) result = result and self._test_model( - models.network_instance_state, network_instance["state"] + models.NetworkInstanceStateDict, network_instance["state"] ) result = result and self._test_model( - models.network_instance_interfaces, network_instance["interfaces"] + models.NetworkInstanceInterfacesDict, network_instance["interfaces"] ) self.assertTrue(result) @@ -621,7 +621,7 @@ def test_get_ipv6_neighbors_table(self): result = len(get_ipv6_neighbors_table) > 0 for entry in get_ipv6_neighbors_table: - result = result and self._test_model(models.ipv6_neighbor, entry) + result = result and self._test_model(models.IPV6NeighborDict, entry) self.assertTrue(result) @@ -633,6 +633,6 @@ def test_get_vlans(self): result = len(get_vlans) > 0 for vlan, vlan_data in get_vlans.items(): - result = result and self._test_model(models.vlan, vlan_data) + result = result and self._test_model(models.VlanDict, vlan_data) self.assertTrue(result) diff --git a/napalm/base/test/getters.py b/napalm/base/test/getters.py index 058546ae0..e7596cd5e 100644 --- a/napalm/base/test/getters.py +++ b/napalm/base/test/getters.py @@ -141,14 +141,14 @@ def test_method_signatures(self): def test_is_alive(self, test_case): """Test is_alive method.""" alive = self.device.is_alive() - assert helpers.test_model(models.alive, alive) + assert helpers.test_model(models.AliveDict, alive) return alive @wrap_test_cases def test_get_facts(self, test_case): """Test get_facts method.""" facts = self.device.get_facts() - assert helpers.test_model(models.facts, facts) + assert helpers.test_model(models.FactsDict, facts) return facts @wrap_test_cases @@ -158,7 +158,7 @@ def test_get_interfaces(self, test_case): assert len(get_interfaces) > 0 for interface, interface_data in get_interfaces.items(): - assert helpers.test_model(models.interface, interface_data) + assert helpers.test_model(models.InterfaceDict, interface_data) return get_interfaces @@ -170,7 +170,7 @@ def test_get_lldp_neighbors(self, test_case): for interface, neighbor_list in get_lldp_neighbors.items(): for neighbor in neighbor_list: - assert helpers.test_model(models.lldp_neighbors, neighbor) + assert helpers.test_model(models.LLDPNeighborDict, neighbor) return get_lldp_neighbors @@ -181,7 +181,7 @@ def test_get_interfaces_counters(self, test_case): assert len(self.device.get_interfaces_counters()) > 0 for interface, interface_data in get_interfaces_counters.items(): - assert helpers.test_model(models.interface_counters, interface_data) + assert helpers.test_model(models.InterfaceCounterDict, interface_data) return get_interfaces_counters @@ -192,18 +192,18 @@ def test_get_environment(self, test_case): assert len(environment) > 0 for fan, fan_data in environment["fans"].items(): - assert helpers.test_model(models.fan, fan_data) + assert helpers.test_model(models.FanDict, fan_data) for power, power_data in environment["power"].items(): - assert helpers.test_model(models.power, power_data) + assert helpers.test_model(models.PowerDict, power_data) for temperature, temperature_data in environment["temperature"].items(): - assert helpers.test_model(models.temperature, temperature_data) + assert helpers.test_model(models.TemperatureDict, temperature_data) for cpu, cpu_data in environment["cpu"].items(): - assert helpers.test_model(models.cpu, cpu_data) + assert helpers.test_model(models.CPUDict, cpu_data) - assert helpers.test_model(models.memory, environment["memory"]) + assert helpers.test_model(models.MemoryDict, environment["memory"]) return environment @@ -218,10 +218,10 @@ def test_get_bgp_neighbors(self, test_case): assert isinstance(vrf_data["router_id"], str) for peer, peer_data in vrf_data["peers"].items(): - assert helpers.test_model(models.peer, peer_data) + assert helpers.test_model(models.PeerDict, peer_data) for af, af_data in peer_data["address_family"].items(): - assert helpers.test_model(models.af, af_data) + assert helpers.test_model(models.AFDict, af_data) return get_bgp_neighbors @@ -233,7 +233,7 @@ def test_get_lldp_neighbors_detail(self, test_case): for interface, neighbor_list in get_lldp_neighbors_detail.items(): for neighbor in neighbor_list: - assert helpers.test_model(models.lldp_neighbors_detail, neighbor) + assert helpers.test_model(models.LLDPNeighborDetailDict, neighbor) return get_lldp_neighbors_detail @@ -244,9 +244,9 @@ def test_get_bgp_config(self, test_case): assert len(get_bgp_config) > 0 for bgp_group in get_bgp_config.values(): - assert helpers.test_model(models.bgp_config_group, bgp_group) + assert helpers.test_model(models.BPGConfigGroupDict, bgp_group) for bgp_neighbor in bgp_group.get("neighbors", {}).values(): - assert helpers.test_model(models.bgp_config_neighbor, bgp_neighbor) + assert helpers.test_model(models.BGPConfigNeighborDict, bgp_neighbor) return get_bgp_config @@ -262,7 +262,7 @@ def test_get_bgp_neighbors_detail(self, test_case): for remote_as, neighbor_list in vrf_ases.items(): assert isinstance(remote_as, int) for neighbor in neighbor_list: - assert helpers.test_model(models.peer_details, neighbor) + assert helpers.test_model(models.PeerDetailsDict, neighbor) return get_bgp_neighbors_detail @@ -273,7 +273,7 @@ def test_get_arp_table(self, test_case): assert len(get_arp_table) > 0 for arp_entry in get_arp_table: - assert helpers.test_model(models.arp_table, arp_entry) + assert helpers.test_model(models.ARPTableDict, arp_entry) return get_arp_table @@ -284,7 +284,7 @@ def test_get_arp_table_with_vrf(self, test_case): assert len(get_arp_table) > 0 for arp_entry in get_arp_table: - assert helpers.test_model(models.arp_table, arp_entry) + assert helpers.test_model(models.ARPTableDict, arp_entry) return get_arp_table @@ -294,7 +294,7 @@ def test_get_ipv6_neighbors_table(self, test_case): get_ipv6_neighbors_table = self.device.get_ipv6_neighbors_table() for entry in get_ipv6_neighbors_table: - assert helpers.test_model(models.ipv6_neighbor, entry) + assert helpers.test_model(models.IPV6NeighborDict, entry) return get_ipv6_neighbors_table @@ -306,7 +306,7 @@ def test_get_ntp_peers(self, test_case): for peer, peer_details in get_ntp_peers.items(): assert isinstance(peer, str) - assert helpers.test_model(models.ntp_peer, peer_details) + assert helpers.test_model(models.NTPPeerDict, peer_details) return get_ntp_peers @@ -318,7 +318,7 @@ def test_get_ntp_servers(self, test_case): for server, server_details in get_ntp_servers.items(): assert isinstance(server, str) - assert helpers.test_model(models.ntp_server, server_details) + assert helpers.test_model(models.NTPServerDict, server_details) return get_ntp_servers @@ -329,7 +329,7 @@ def test_get_ntp_stats(self, test_case): assert len(get_ntp_stats) > 0 for ntp_peer_details in get_ntp_stats: - assert helpers.test_model(models.ntp_stats, ntp_peer_details) + assert helpers.test_model(models.NTPStats, ntp_peer_details) return get_ntp_stats @@ -343,9 +343,9 @@ def test_get_interfaces_ip(self, test_case): ipv4 = interface_details.get("ipv4", {}) ipv6 = interface_details.get("ipv6", {}) for ip, ip_details in ipv4.items(): - assert helpers.test_model(models.interfaces_ip, ip_details) + assert helpers.test_model(models.InterfacesIPDictEntry, ip_details) for ip, ip_details in ipv6.items(): - assert helpers.test_model(models.interfaces_ip, ip_details) + assert helpers.test_model(models.InterfacesIPDictEntry, ip_details) return get_interfaces_ip @@ -356,7 +356,7 @@ def test_get_mac_address_table(self, test_case): assert len(get_mac_address_table) > 0 for mac_table_entry in get_mac_address_table: - assert helpers.test_model(models.mac_address_table, mac_table_entry) + assert helpers.test_model(models.MACAdressTable, mac_table_entry) return get_mac_address_table @@ -373,7 +373,7 @@ def test_get_route_to(self, test_case): for prefix, routes in get_route_to.items(): for route in routes: - assert helpers.test_model(models.route, route) + assert helpers.test_model(models.RouteDict, route) return get_route_to @@ -391,7 +391,7 @@ def test_get_route_to_longer(self, test_case): for prefix, routes in get_route_to.items(): for route in routes: - assert helpers.test_model(models.route, route) + assert helpers.test_model(models.RouteDict, route) return get_route_to @@ -403,10 +403,10 @@ def test_get_snmp_information(self, test_case): assert len(get_snmp_information) > 0 for snmp_entry in get_snmp_information: - assert helpers.test_model(models.snmp, get_snmp_information) + assert helpers.test_model(models.SNMPDict, get_snmp_information) for community, community_data in get_snmp_information["community"].items(): - assert helpers.test_model(models.snmp_community, community_data) + assert helpers.test_model(models.SNMPCommunityDict, community_data) return get_snmp_information @@ -419,7 +419,7 @@ def test_get_probes_config(self, test_case): for probe_name, probe_tests in get_probes_config.items(): for test_name, test_config in probe_tests.items(): - assert helpers.test_model(models.probe_test, test_config) + assert helpers.test_model(models.ProbeTestDict, test_config) return get_probes_config @@ -431,7 +431,7 @@ def test_get_probes_results(self, test_case): for probe_name, probe_tests in get_probes_results.items(): for test_name, test_results in probe_tests.items(): - assert helpers.test_model(models.probe_test_results, test_results) + assert helpers.test_model(models.ProbeTestResultDict, test_results) return get_probes_results @@ -443,10 +443,10 @@ def test_ping(self, test_case): assert isinstance(get_ping.get("success"), dict) ping_results = get_ping.get("success", {}) - assert helpers.test_model(models.ping, ping_results) + assert helpers.test_model(models.PingDict, ping_results) for ping_result in ping_results.get("results", []): - assert helpers.test_model(models.ping_result, ping_result) + assert helpers.test_model(models.PingResultDictEntry, ping_result) return get_ping @@ -460,7 +460,7 @@ def test_traceroute(self, test_case): for hope_id, hop_result in traceroute_results.items(): for probe_id, probe_result in hop_result.get("probes", {}).items(): - assert helpers.test_model(models.traceroute, probe_result) + assert helpers.test_model(models.TracerouteDict, probe_result) return get_traceroute @@ -471,7 +471,7 @@ def test_get_users(self, test_case): assert len(get_users) for user, user_details in get_users.items(): - assert helpers.test_model(models.users, user_details) + assert helpers.test_model(models.UsersDict, user_details) assert (0 <= user_details.get("level") <= 15) or ( user_details.get("level") == 20 ) @@ -505,7 +505,7 @@ def test_get_config(self, test_case): get_config = self.device.get_config() assert isinstance(get_config, dict) - assert helpers.test_model(models.config, get_config) + assert helpers.test_model(models.ConfigDict, get_config) return get_config @@ -529,7 +529,7 @@ def test_get_config_sanitized(self, test_case): get_config = self.device.get_config(sanitized=True) assert isinstance(get_config, dict) - assert helpers.test_model(models.config, get_config) + assert helpers.test_model(models.ConfigDict, get_config) return get_config @@ -540,12 +540,12 @@ def test_get_network_instances(self, test_case): assert isinstance(get_network_instances, dict) for network_instance_name, network_instance in get_network_instances.items(): - assert helpers.test_model(models.network_instance, network_instance) + assert helpers.test_model(models.NetworkInstanceDict, network_instance) assert helpers.test_model( - models.network_instance_state, network_instance["state"] + models.NetworkInstanceStateDict, network_instance["state"] ) assert helpers.test_model( - models.network_instance_interfaces, network_instance["interfaces"] + models.NetworkInstanceInterfacesDict, network_instance["interfaces"] ) return get_network_instances @@ -557,7 +557,7 @@ def test_get_firewall_policies(self, test_case): assert len(get_firewall_policies) > 0 for policy_name, policy_details in get_firewall_policies.items(): for policy_term in policy_details: - assert helpers.test_model(models.firewall_policies, policy_term) + assert helpers.test_model(models.FirewallPolicyDict, policy_term) return get_firewall_policies @wrap_test_cases @@ -568,6 +568,6 @@ def test_get_vlans(self, test_case): assert len(get_vlans) > 0 for vlan, vlan_data in get_vlans.items(): - assert helpers.test_model(models.vlan, vlan_data) + assert helpers.test_model(models.VlanDict, vlan_data) return get_vlans diff --git a/napalm/base/test/models.py b/napalm/base/test/models.py index adf0de3fd..d147ac6be 100644 --- a/napalm/base/test/models.py +++ b/napalm/base/test/models.py @@ -1,20 +1,22 @@ +from typing import Dict, Literal, Union, List, Optional + try: from typing import TypedDict except ImportError: from typing_extensions import TypedDict -configuration = TypedDict( - "configuration", {"running": str, "candidate": str, "startup": str} +ConfigurationDict = TypedDict( + "ConfigurationDict", {"running": str, "candidate": str, "startup": str} ) -alive = TypedDict("alive", {"is_alive": bool}) +AliveDict = TypedDict("AliveDict", {"is_alive": bool}) -facts = TypedDict( - "facts", +FactsDict = TypedDict( + "FactsDict", { "os_version": str, "uptime": int, - "interface_list": list, + "interface_list": List, "vendor": str, "serial_number": str, "model": str, @@ -23,8 +25,8 @@ }, ) -interface = TypedDict( - "interface", +InterfaceDict = TypedDict( + "InterfaceDict", { "is_up": bool, "is_enabled": bool, @@ -36,10 +38,26 @@ }, ) -lldp_neighbors = TypedDict("lldp_neighbors", {"hostname": str, "port": str}) +LLDPNeighborDict = TypedDict("LLDPNeighborDict", {"hostname": str, "port": str}) -interface_counters = TypedDict( - "interface_counters", +LLDPNeighborDetailDict = TypedDict( + "LLDPNeighborDetailDict", + { + "parent_interface": str, + "remote_port": str, + "remote_chassis_id": str, + "remote_port_description": str, + "remote_system_name": str, + "remote_system_description": str, + "remote_system_capab": List, + "remote_system_enable_capab": List, + }, +) + +LLDPNeighborsDetailDict = Dict[str, LLDPNeighborDetailDict] + +InterfaceCounterDict = TypedDict( + "InterfaceCounterDict", { "tx_errors": int, "rx_errors": int, @@ -56,20 +74,31 @@ }, ) -temperature = TypedDict( - "temperature", {"is_alert": bool, "is_critical": bool, "temperature": float} +TemperatureDict = TypedDict( + "TemperatureDict", {"is_alert": bool, "is_critical": bool, "temperature": float} ) -power = TypedDict("power", {"status": bool, "output": float, "capacity": float}) +PowerDict = TypedDict("PowerDict", {"status": bool, "output": float, "capacity": float}) -memory = TypedDict("memory", {"used_ram": int, "available_ram": int}) +MemoryDict = TypedDict("MemoryDict", {"used_ram": int, "available_ram": int}) -fan = TypedDict("fan", {"status": bool}) +FanDict = TypedDict("FanDict", {"status": bool}) -cpu = TypedDict("cpu", {"%usage": float}) +CPUDict = TypedDict("CPUDict", {"%usage": float}) -peer = TypedDict( - "peer", +EnvironmentDict = TypedDict( + "EnvironmentDict", + { + "fans": Dict[str, FanDict], + "temperature": Dict[str, TemperatureDict], + "power": Dict[str, PowerDict], + "cpu": Dict[str, CPUDict], + "memory": Dict[str, MemoryDict], + }, +) + +PeerDict = TypedDict( + "PeerDict", { "is_enabled": bool, "uptime": int, @@ -82,30 +111,57 @@ }, ) -af = TypedDict( - "af", {"sent_prefixes": int, "accepted_prefixes": int, "received_prefixes": int} -) - -lldp_neighbors_detail = TypedDict( - "lldp_neighbors_detail", +PeerDetailsDict = TypedDict( + "PeerDetailsDict", { - "parent_interface": str, - "remote_port": str, - "remote_chassis_id": str, - "remote_port_description": str, - "remote_system_name": str, - "remote_system_description": str, - "remote_system_capab": list, - "remote_system_enable_capab": list, + "up": bool, + "local_as": int, + "remote_as": int, + "router_id": str, + "local_address": str, + "routing_table": str, + "local_address_configured": bool, + "local_port": int, + "remote_address": str, + "remote_port": int, + "multihop": bool, + "multipath": bool, + "remove_private_as": bool, + "import_policy": str, + "export_policy": str, + "input_messages": int, + "output_messages": int, + "input_updates": int, + "output_updates": int, + "messages_queued_out": int, + "connection_state": str, + "previous_connection_state": str, + "last_event": str, + "suppress_4byte_as": bool, + "local_as_prepend": bool, + "holdtime": int, + "configured_holdtime": int, + "keepalive": int, + "configured_keepalive": int, + "active_prefix_count": int, + "received_prefix_count": int, + "accepted_prefix_count": int, + "suppressed_prefix_count": int, + "advertised_prefix_count": int, + "flap_count": int, }, ) -bgp_config_group = TypedDict( - "bgp_config_group", +AFDict = TypedDict( + "AFDict", {"sent_prefixes": int, "accepted_prefixes": int, "received_prefixes": int} +) + +BPGConfigGroupDict = TypedDict( + "BPGConfigGroupDict", { "type": str, "description": str, - "apply_groups": list, + "apply_groups": List, "multihop_ttl": int, "multipath": bool, "local_address": str, @@ -119,8 +175,8 @@ }, ) -bgp_config_neighbor = TypedDict( - "bgp_config_neighbor", +BGPConfigNeighborDict = TypedDict( + "BGPConfigNeighborDict", { "description": str, "import_policy": str, @@ -135,72 +191,55 @@ }, ) -peer_details = TypedDict( - "peer_details", +BGPStateAdressFamilyDict = TypedDict( + "BGPStateAdressFamilyDict", + {"received_prefixes": int, "accepted_prefixes": int, "sent_prefixes": int}, +) + +BGPStateNeighborDict = TypedDict( + "BGPStateNeighborDict", { - "up": bool, "local_as": int, "remote_as": int, - "router_id": str, - "local_address": str, - "routing_table": str, - "local_address_configured": bool, - "local_port": int, - "remote_address": str, - "remote_port": int, - "multihop": bool, - "multipath": bool, - "remove_private_as": bool, - "import_policy": str, - "export_policy": str, - "input_messages": int, - "output_messages": int, - "input_updates": int, - "output_updates": int, - "messages_queued_out": int, - "connection_state": str, - "previous_connection_state": str, - "last_event": str, - "suppress_4byte_as": bool, - "local_as_prepend": bool, - "holdtime": int, - "configured_holdtime": int, - "keepalive": int, - "configured_keepalive": int, - "active_prefix_count": int, - "received_prefix_count": int, - "accepted_prefix_count": int, - "suppressed_prefix_count": int, - "advertised_prefix_count": int, - "flap_count": int, + "remote_id": str, + "is_up": bool, + "is_enabled": bool, + "description": str, + "uptime": int, + "address_family": Dict[str, BGPStateAdressFamilyDict], }, ) -arp_table = TypedDict( - "arp_table", {"interface": str, "mac": str, "ip": str, "age": float} +BGPStateNeighborsPerVRFDict = TypedDict( + "BGPStateNeighborsPerVRFDict", + {"router_id": str, "peers": Dict[str, BGPStateNeighborDict]}, ) -ipv6_neighbor = TypedDict( - "ipv6_neighbor", +ARPTableDict = TypedDict( + "ARPTableDict", {"interface": str, "mac": str, "ip": str, "age": float} +) + +IPV6NeighborDict = TypedDict( + "IPV6NeighborDict", {"interface": str, "mac": str, "ip": str, "age": float, "state": str}, ) -ntp_peer = TypedDict( - "ntp_peer", +NTPPeerDict = TypedDict( + "NTPPeerDict", { # will populate it in the future wit potential keys }, ) -ntp_server = TypedDict( - "ntp_server", +NTPServerDict = TypedDict( + "NTPServerDict", { # will populate it in the future wit potential keys }, ) -ntp_stats = TypedDict( - "ntp_stats", +NTPStats = TypedDict( + "NTPStats", { "remote": str, "referenceid": str, @@ -216,10 +255,14 @@ }, ) -interfaces_ip = TypedDict("interfaces_ip", {"prefix_length": int}) +InterfacesIPDictEntry = TypedDict("InterfacesIPDictEntry", {"prefix_length": int}) -mac_address_table = TypedDict( - "mac_address_table", +InterfacesIPDict = Dict[ + str, Dict[Union[Literal["ipv4"], Literal["ipv6"]], Dict[str, InterfacesIPDictEntry]] +] + +MACAdressTable = TypedDict( + "MACAdressTable", { "mac": str, "interface": str, @@ -231,8 +274,8 @@ }, ) -route = TypedDict( - "route", +RouteDict = TypedDict( + "RouteDict", { "protocol": str, "current_active": bool, @@ -248,14 +291,14 @@ }, ) -snmp = TypedDict( - "snmp", {"chassis_id": str, "community": dict, "contact": str, "location": str} +SNMPDict = TypedDict( + "SNMPDict", {"chassis_id": str, "community": dict, "contact": str, "location": str} ) -snmp_community = TypedDict("snmp_community", {"acl": str, "mode": str}) +SNMPCommunityDict = TypedDict("SNMPCommunityDict", {"acl": str, "mode": str}) -probe_test = TypedDict( - "probe_test", +ProbeTestDict = TypedDict( + "ProbeTestDict", { "probe_type": str, "target": str, @@ -265,8 +308,8 @@ }, ) -probe_test_results = TypedDict( - "probe_test_results", +ProbeTestResultDict = TypedDict( + "ProbeTestResultDict", { "target": str, "source": str, @@ -287,8 +330,10 @@ }, ) -ping = TypedDict( - "ping", +PingResultDictEntry = TypedDict("PingResultDictEntry", {"ip_address": str, "rtt": float}) + +PingDict = TypedDict( + "PingDict", { "probes_sent": int, "packet_loss": int, @@ -300,34 +345,65 @@ }, ) -ping_result = TypedDict("ping_result", {"ip_address": str, "rtt": float}) +PingResultDict = TypedDict( + "PingResultDict", {"success": Optional[PingDict], "error": Optional[str]} +) + +TracerouteDict = TypedDict( + "TracerouteDict", {"rtt": float, "ip_address": str, "host_name": str} +) + +TracerouteResultDictEntry = TypedDict("TracerouteResultDictEntry", {"probes": Dict[int, TracerouteDict]}) + +TracerouteResultDict = TypedDict( + "TracerouteResultDict", + {"success": Optional[Dict[int, TracerouteResultDictEntry]], "error": Optional[str]}, +) + +UsersDict = TypedDict("UsersDict", {"level": int, "password": str, "sshkeys": List}) -traceroute = TypedDict( - "traceroute", {"rtt": float, "ip_address": str, "host_name": str} +OpticsStateDict = TypedDict( + "OpticsStateDict", {"instant": float, "avg": float, "min": float, "max": float} ) -users = TypedDict("users", {"level": int, "password": str, "sshkeys": list}) +OpticsStatePerChannelDict = TypedDict("OpticsStatePerChannelDict", { + "input_power": OpticsStateDict, + "output_power": OpticsStateDict, + "laser_bias_current": OpticsStateDict +}) -optics_state = TypedDict( - "optics_state", {"instant": float, "avg": float, "min": float, "max": float} +OpticsPerChannelDict = TypedDict("OpticsPerChannelDict", { + "index": int, + "state": OpticsStatePerChannelDict +}) + +OpticsPhysicalChannelsDict = TypedDict("OpticsPhysicalChannelsDict", { + "channels": OpticsPerChannelDict +}) + +OpticsDict = TypedDict( + "OpticsDict", + { + "physical_channels": OpticsPhysicalChannelsDict + } ) -config = TypedDict("config", {"running": str, "startup": str, "candidate": str}) +ConfigDict = TypedDict("ConfigDict", {"running": str, "startup": str, "candidate": str}) -network_instance = TypedDict( - "network_instance", {"name": str, "type": str, "state": dict, "interfaces": dict} +NetworkInstanceDict = TypedDict( + "NetworkInstanceDict", {"name": str, "type": str, "state": dict, "interfaces": dict} ) -network_instance_state = TypedDict( - "network_instance_state", {"route_distinguisher": str} +NetworkInstanceStateDict = TypedDict( + "NetworkInstanceStateDict", {"route_distinguisher": str} ) -network_instance_interfaces = TypedDict( - "network_instance_interfaces", {"interface": dict} +NetworkInstanceInterfacesDict = TypedDict( + "NetworkInstanceInterfacesDict", {"interface": dict} ) -firewall_policies = TypedDict( - "firewall_policies", +FirewallPolicyDict = TypedDict( + "FirewallPolicyDict", { "position": int, "packet_hits": int, @@ -345,4 +421,4 @@ }, ) -vlan = TypedDict("vlan", {"name": str, "interfaces": list}) +VlanDict = TypedDict("VlanDict", {"name": str, "interfaces": List}) diff --git a/test/nxapi_plumbing/conftest.py b/test/nxapi_plumbing/conftest.py index 245319eaa..2bc29249e 100644 --- a/test/nxapi_plumbing/conftest.py +++ b/test/nxapi_plumbing/conftest.py @@ -73,7 +73,7 @@ def mock_pynxos_device_xml(request): @pytest.fixture(scope="module") def pynxos_device(request): """Create a real pynxos test device.""" - device_under_test = request.config.getoption("test_device") + device_under_test = request.ConfigDict.getoption("test_device") test_devices = parse_yaml(PWD + "/etc/test_devices.yml") device = test_devices[device_under_test] conn = Device(**device) diff --git a/test/nxos/test_getters.py b/test/nxos/test_getters.py index 48a993d94..365dc44c2 100644 --- a/test/nxos/test_getters.py +++ b/test/nxos/test_getters.py @@ -24,6 +24,6 @@ def test_get_interfaces(self, test_case): assert len(get_interfaces) > 0 for interface, interface_data in get_interfaces.items(): - assert helpers.test_model(models.interface, interface_data) + assert helpers.test_model(models.InterfaceDict, interface_data) return get_interfaces diff --git a/test/nxos_ssh/test_getters.py b/test/nxos_ssh/test_getters.py index 48a993d94..365dc44c2 100644 --- a/test/nxos_ssh/test_getters.py +++ b/test/nxos_ssh/test_getters.py @@ -24,6 +24,6 @@ def test_get_interfaces(self, test_case): assert len(get_interfaces) > 0 for interface, interface_data in get_interfaces.items(): - assert helpers.test_model(models.interface, interface_data) + assert helpers.test_model(models.InterfaceDict, interface_data) return get_interfaces From 49791a9495af0d1b8176ac013c80790bbac3f6c1 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Mon, 22 Mar 2021 17:24:41 +0100 Subject: [PATCH 18/74] Make tests pass by ignoring the annotations part of the arg spec --- napalm/base/base.py | 6 +- napalm/base/test/base.py | 32 ++++--- napalm/base/test/getters.py | 13 +-- napalm/base/test/models.py | 44 +++++----- napalm/nxos/nxos.py | 166 +++++++++++++++++++++--------------- 5 files changed, 153 insertions(+), 108 deletions(-) diff --git a/napalm/base/base.py b/napalm/base/base.py index b922aab88..01a804a0f 100644 --- a/napalm/base/base.py +++ b/napalm/base/base.py @@ -16,7 +16,7 @@ import sys from types import TracebackType -from typing import Optional, Dict, Type, Any, Literal, List +from typing import Optional, Dict, Type, Any, Literal, List, Union from netmiko import ConnectHandler, NetMikoTimeoutException @@ -129,7 +129,7 @@ def _netmiko_close(self) -> None: if getattr(self, "_netmiko_device", None): self._netmiko_device.disconnect() self._netmiko_device = None - self.device = None + self.device: Optional[ConnectHandler] = None def open(self) -> None: """ @@ -730,7 +730,7 @@ def get_bgp_config( """ raise NotImplementedError - def cli(self, commands: List[str]) -> Dict[str, str]: + def cli(self, commands: List[str]) -> Dict[str, Union[str, Dict[str, Any]]]: """ Will execute a list of commands and return the output in a dictionary format. diff --git a/napalm/base/test/base.py b/napalm/base/test/base.py index 17e72f009..f005101ca 100644 --- a/napalm/base/test/base.py +++ b/napalm/base/test/base.py @@ -266,7 +266,9 @@ def test_get_environment(self): result = result and self._test_model(models.PowerDict, power_data) for temperature, temperature_data in environment["temperature"].items(): - result = result and self._test_model(models.TemperatureDict, temperature_data) + result = result and self._test_model( + models.TemperatureDict, temperature_data + ) for cpu, cpu_data in environment["cpu"].items(): result = result and self._test_model(models.CPUDict, cpu_data) @@ -342,7 +344,9 @@ def test_get_bgp_neighbors_detail(self): for remote_as, neighbor_list in vrf_ases.items(): result = result and isinstance(remote_as, int) for neighbor in neighbor_list: - result = result and self._test_model(models.PeerDetailsDict, neighbor) + result = result and self._test_model( + models.PeerDetailsDict, neighbor + ) self.assertTrue(result) @@ -420,9 +424,13 @@ def test_get_interfaces_ip(self): ipv4 = interface_details.get("ipv4", {}) ipv6 = interface_details.get("ipv6", {}) for ip, ip_details in ipv4.items(): - result = result and self._test_model(models.InterfacesIPDictEntry, ip_details) + result = result and self._test_model( + models.InterfacesIPDictEntry, ip_details + ) for ip, ip_details in ipv6.items(): - result = result and self._test_model(models.InterfacesIPDictEntry, ip_details) + result = result and self._test_model( + models.InterfacesIPDictEntry, ip_details + ) self.assertTrue(result) @@ -434,9 +442,7 @@ def test_get_mac_address_table(self): result = len(get_mac_address_table) > 0 for mac_table_entry in get_mac_address_table: - result = result and self._test_model( - models.MACAdressTable, mac_table_entry - ) + result = result and self._test_model(models.MACAdressTable, mac_table_entry) self.assertTrue(result) @@ -469,7 +475,9 @@ def test_get_snmp_information(self): result = result and self._test_model(models.SNMPDict, get_snmp_information) for community, community_data in get_snmp_information["community"].items(): - result = result and self._test_model(models.SNMPCommunityDict, community_data) + result = result and self._test_model( + models.SNMPCommunityDict, community_data + ) self.assertTrue(result) @@ -514,7 +522,9 @@ def test_ping(self): result = result and self._test_model(models.PingDict, ping_results) for ping_result in ping_results.get("results", []): - result = result and self._test_model(models.PingResultDictEntry, ping_result) + result = result and self._test_model( + models.PingResultDictEntry, ping_result + ) self.assertTrue(result) @@ -530,7 +540,9 @@ def test_traceroute(self): for hope_id, hop_result in traceroute_results.items(): for probe_id, probe_result in hop_result.get("probes", {}).items(): - result = result and self._test_model(models.TracerouteDict, probe_result) + result = result and self._test_model( + models.TracerouteDict, probe_result + ) self.assertTrue(result) diff --git a/napalm/base/test/getters.py b/napalm/base/test/getters.py index e7596cd5e..4e8334429 100644 --- a/napalm/base/test/getters.py +++ b/napalm/base/test/getters.py @@ -109,7 +109,10 @@ class BaseTestGetters(object): """Base class for testing drivers.""" def test_method_signatures(self): - """Test that all methods have the same signature.""" + """ + Test that all methods have the same signature. + + The type hint annotations are ignored here because the import paths might differ.""" errors = {} cls = self.driver # Create fictional driver instance (py3 needs bound methods) @@ -121,17 +124,17 @@ def test_method_signatures(self): continue try: orig = getattr(NetworkDriver, attr) - orig_spec = inspect.getfullargspec(orig) + orig_spec = inspect.getfullargspec(orig)[:4] except AttributeError: orig_spec = "Method does not exist in napalm.base" - func_spec = inspect.getfullargspec(func) + func_spec = inspect.getfullargspec(func)[:4] if orig_spec != func_spec: errors[attr] = (orig_spec, func_spec) EXTRA_METHODS = ["__init__"] for method in EXTRA_METHODS: - orig_spec = inspect.getfullargspec(getattr(NetworkDriver, method)) - func_spec = inspect.getfullargspec(getattr(cls, method)) + orig_spec = inspect.getfullargspec(getattr(NetworkDriver, method))[:4] + func_spec = inspect.getfullargspec(getattr(cls, method))[:4] if orig_spec != func_spec: errors[attr] = (orig_spec, func_spec) diff --git a/napalm/base/test/models.py b/napalm/base/test/models.py index d147ac6be..f4192896b 100644 --- a/napalm/base/test/models.py +++ b/napalm/base/test/models.py @@ -330,7 +330,9 @@ }, ) -PingResultDictEntry = TypedDict("PingResultDictEntry", {"ip_address": str, "rtt": float}) +PingResultDictEntry = TypedDict( + "PingResultDictEntry", {"ip_address": str, "rtt": float} +) PingDict = TypedDict( "PingDict", @@ -346,18 +348,21 @@ ) PingResultDict = TypedDict( - "PingResultDict", {"success": Optional[PingDict], "error": Optional[str]} + "PingResultDict", {"success": Optional[PingDict], "error": Optional[str]}, total=False ) TracerouteDict = TypedDict( "TracerouteDict", {"rtt": float, "ip_address": str, "host_name": str} ) -TracerouteResultDictEntry = TypedDict("TracerouteResultDictEntry", {"probes": Dict[int, TracerouteDict]}) +TracerouteResultDictEntry = TypedDict( + "TracerouteResultDictEntry", {"probes": Dict[int, TracerouteDict]} +) TracerouteResultDict = TypedDict( "TracerouteResultDict", {"success": Optional[Dict[int, TracerouteResultDictEntry]], "error": Optional[str]}, + total=False ) UsersDict = TypedDict("UsersDict", {"level": int, "password": str, "sshkeys": List}) @@ -366,28 +371,25 @@ "OpticsStateDict", {"instant": float, "avg": float, "min": float, "max": float} ) -OpticsStatePerChannelDict = TypedDict("OpticsStatePerChannelDict", { - "input_power": OpticsStateDict, - "output_power": OpticsStateDict, - "laser_bias_current": OpticsStateDict -}) - -OpticsPerChannelDict = TypedDict("OpticsPerChannelDict", { - "index": int, - "state": OpticsStatePerChannelDict -}) +OpticsStatePerChannelDict = TypedDict( + "OpticsStatePerChannelDict", + { + "input_power": OpticsStateDict, + "output_power": OpticsStateDict, + "laser_bias_current": OpticsStateDict, + }, +) -OpticsPhysicalChannelsDict = TypedDict("OpticsPhysicalChannelsDict", { - "channels": OpticsPerChannelDict -}) +OpticsPerChannelDict = TypedDict( + "OpticsPerChannelDict", {"index": int, "state": OpticsStatePerChannelDict} +) -OpticsDict = TypedDict( - "OpticsDict", - { - "physical_channels": OpticsPhysicalChannelsDict - } +OpticsPhysicalChannelsDict = TypedDict( + "OpticsPhysicalChannelsDict", {"channels": OpticsPerChannelDict} ) +OpticsDict = TypedDict("OpticsDict", {"physical_channels": OpticsPhysicalChannelsDict}) + ConfigDict = TypedDict("ConfigDict", {"running": str, "startup": str, "candidate": str}) NetworkInstanceDict = TypedDict( diff --git a/napalm/nxos/nxos.py b/napalm/nxos/nxos.py index d3e311f1e..9992d1b46 100644 --- a/napalm/nxos/nxos.py +++ b/napalm/nxos/nxos.py @@ -23,10 +23,14 @@ from collections import defaultdict # import third party lib +from typing import Optional, Dict, List, Union, Any + from requests.exceptions import ConnectionError from netaddr import IPAddress from netaddr.core import AddrFormatError from netmiko import file_transfer + +from napalm.base.test import models from napalm.nxapi_plumbing import Device as NXOSDevice from napalm.nxapi_plumbing import ( NXAPIAuthError, @@ -72,7 +76,14 @@ def wrap_function(self, filename=None, config=None): class NXOSDriverBase(NetworkDriver): """Common code shared between nx-api and nxos_ssh.""" - def __init__(self, hostname, username, password, timeout=60, optional_args=None): + def __init__( + self, + hostname: str, + username: str, + password: str, + timeout: int = 60, + optional_args: Optional[Dict] = None, + ) -> None: if optional_args is None: optional_args = {} self.hostname = hostname @@ -88,10 +99,12 @@ def __init__(self, hostname, username, password, timeout=60, optional_args=None) self._dest_file_system = optional_args.pop("dest_file_system", "bootflash:") self.force_no_enable = optional_args.get("force_no_enable", False) self.netmiko_optional_args = netmiko_args(optional_args) - self.device = None + self.device: Optional[NXOSDevice] = None @ensure_netmiko_conn - def load_replace_candidate(self, filename=None, config=None): + def load_replace_candidate( + self, filename: Optional[str] = None, config: Optional[str] = None + ) -> None: if not filename and not config: raise ReplaceConfigException( @@ -129,7 +142,9 @@ def load_replace_candidate(self, filename=None, config=None): if config and os.path.isfile(tmp_file): os.remove(tmp_file) - def load_merge_candidate(self, filename=None, config=None): + def load_merge_candidate( + self, filename: Optional[str] = None, config: Optional[str] = None + ) -> None: if not filename and not config: raise MergeConfigException("filename or config param must be provided.") @@ -142,10 +157,10 @@ def load_merge_candidate(self, filename=None, config=None): self.replace = False self.loaded = True - def _send_command(self, command, raw_text=False): + def _send_command(self, command: str, raw_text: bool = False) -> str: raise NotImplementedError - def _commit_merge(self): + def _commit_merge(self) -> None: try: output = self._send_config(self.merge_candidate) if output and "Invalid command" in output: @@ -161,7 +176,7 @@ def _commit_merge(self): # clear the merge buffer self.merge_candidate = "" - def _get_merge_diff(self): + def _get_merge_diff(self) -> str: """ The merge diff is not necessarily what needs to be loaded for example under NTP, even though the 'ntp commit' command might be @@ -180,7 +195,7 @@ def _get_merge_diff(self): diff.append(line) return "\n".join(diff) - def _get_diff(self): + def _get_diff(self) -> str: """Get a diff between running config and a proposed file.""" diff = [] self._create_sot_file() @@ -206,7 +221,7 @@ def _get_diff(self): ) return "\n".join(diff) - def compare_config(self): + def compare_config(self) -> str: if self.loaded: if not self.replace: return self._get_merge_diff() @@ -214,7 +229,7 @@ def compare_config(self): return diff return "" - def commit_config(self, message="", revert_in=None): + def commit_config(self, message: str = "", revert_in: Optional[int] = None) -> None: if revert_in is not None: raise NotImplementedError( "Commit confirm has not been implemented on this platform." @@ -243,7 +258,7 @@ def commit_config(self, message="", revert_in=None): else: raise ReplaceConfigException("No config loaded.") - def discard_config(self): + def discard_config(self) -> None: if self.loaded: # clear the buffer self.merge_candidate = "" @@ -251,7 +266,7 @@ def discard_config(self): self._delete_file(self.candidate_cfg) self.loaded = False - def _create_sot_file(self): + def _create_sot_file(self) -> None: """Create Source of Truth file to compare.""" # Bug on on NX-OS 6.2.16 where overwriting sot_file would take exceptionally long time @@ -269,15 +284,15 @@ def _create_sot_file(self): def ping( self, - destination, - source=c.PING_SOURCE, - ttl=c.PING_TTL, - timeout=c.PING_TIMEOUT, - size=c.PING_SIZE, - count=c.PING_COUNT, - vrf=c.PING_VRF, - source_interface=c.PING_SOURCE_INTERFACE, - ): + destination: str, + source: str = c.PING_SOURCE, + ttl: int = c.PING_TTL, + timeout: int = c.PING_TIMEOUT, + size: int = c.PING_SIZE, + count: int = c.PING_COUNT, + vrf: str = c.PING_VRF, + source_interface: str =c.PING_SOURCE_INTERFACE, + ) -> models.PingResultDict: """ Execute ping on the device and returns a dictionary with the result. Output dictionary has one of following keys: @@ -380,12 +395,12 @@ def ping( def traceroute( self, - destination, - source=c.TRACEROUTE_SOURCE, - ttl=c.TRACEROUTE_TTL, - timeout=c.TRACEROUTE_TIMEOUT, - vrf=c.TRACEROUTE_VRF, - ): + destination: str, + source: str = c.TRACEROUTE_SOURCE, + ttl: int = c.TRACEROUTE_TTL, + timeout: int = c.TRACEROUTE_TIMEOUT, + vrf: str = c.TRACEROUTE_VRF, + ) -> models.TracerouteResultDict: _HOP_ENTRY_PROBE = [ r"\s+", @@ -478,7 +493,7 @@ def traceroute( previous_probe_ip_address = ip_address return traceroute_result - def _get_checkpoint_file(self): + def _get_checkpoint_file(self) -> str: filename = "temp_cp_file_from_napalm" self._set_checkpoint(filename) command = "show file {}".format(filename) @@ -486,7 +501,7 @@ def _get_checkpoint_file(self): self._delete_file(filename) return output - def _set_checkpoint(self, filename): + def _set_checkpoint(self, filename: str) -> None: commands = [ "terminal dont-ask", "checkpoint file {}".format(filename), @@ -494,7 +509,7 @@ def _set_checkpoint(self, filename): ] self._send_command_list(commands) - def _save_to_checkpoint(self, filename): + def _save_to_checkpoint(self, filename: str) -> None: """Save the current running config to the given file.""" commands = [ "terminal dont-ask", @@ -503,7 +518,7 @@ def _save_to_checkpoint(self, filename): ] self._send_command_list(commands) - def _delete_file(self, filename): + def _delete_file(self, filename: str) -> None: commands = [ "terminal dont-ask", "delete {}".format(filename), @@ -512,7 +527,7 @@ def _delete_file(self, filename): self._send_command_list(commands) @staticmethod - def _create_tmp_file(config): + def _create_tmp_file(config: str) -> str: tmp_dir = tempfile.gettempdir() rand_fname = str(uuid.uuid4()) filename = os.path.join(tmp_dir, rand_fname) @@ -520,10 +535,12 @@ def _create_tmp_file(config): fobj.write(config) return filename - def _disable_confirmation(self): + def _disable_confirmation(self) -> str: self._send_command_list(["terminal dont-ask"]) - def get_config(self, retrieve="all", full=False, sanitized=False): + def get_config( + self, retrieve: str = "all", full: bool = False, sanitized: bool = False + ) -> models.ConfigDict: # NX-OS adds some extra, unneeded lines that should be filtered. filter_strings = [ @@ -555,7 +572,7 @@ def get_config(self, retrieve="all", full=False, sanitized=False): return config - def get_lldp_neighbors(self): + def get_lldp_neighbors(self) -> Dict[str, models.LLDPNeighborDict]: """IOS implementation of get_lldp_neighbors.""" lldp = {} neighbors_detail = self.get_lldp_neighbors_detail() @@ -572,8 +589,10 @@ def get_lldp_neighbors(self): return lldp - def get_lldp_neighbors_detail(self, interface=""): - lldp = {} + def get_lldp_neighbors_detail( + self, interface: str = "" + ) -> models.LLDPNeighborsDetailDict: + lldp: Dict[str, str] = {} lldp_interfaces = [] if interface: @@ -616,7 +635,9 @@ def get_lldp_neighbors_detail(self, interface=""): return lldp @staticmethod - def _get_table_rows(parent_table, table_name, row_name): + def _get_table_rows( + parent_table: Optional[Dict], table_name: str, row_name: str + ) -> List: """ Inconsistent behavior: {'TABLE_intf': [{'ROW_intf': { @@ -637,16 +658,18 @@ def _get_table_rows(parent_table, table_name, row_name): _table_rows = [_table_rows] return _table_rows - def _get_reply_table(self, result, table_name, row_name): + def _get_reply_table( + self, result: Optional[Dict], table_name: str, row_name: str + ) -> List: return self._get_table_rows(result, table_name, row_name) - def _get_command_table(self, command, table_name, row_name): + def _get_command_table(self, command: str, table_name: str, row_name: str) -> List: json_output = self._send_command(command) if type(json_output) is not dict and json_output: json_output = json.loads(json_output) return self._get_reply_table(json_output, table_name, row_name) - def _parse_vlan_ports(self, vlan_s): + def _parse_vlan_ports(self, vlan_s: Union[str, List]) -> List: vlans = [] find_regexp = r"^([A-Za-z\/-]+|.*\/)(\d+)-(\d+)$" vlan_str = "" @@ -671,7 +694,7 @@ def _parse_vlan_ports(self, vlan_s): class NXOSDriver(NXOSDriverBase): - def __init__(self, hostname, username, password, timeout=60, optional_args=None): + def __init__(self, hostname: str, username: str, password: str, timeout: int = 60, optional_args: Optional[Dict] = None) -> None: super().__init__( hostname, username, password, timeout=timeout, optional_args=optional_args ) @@ -690,7 +713,7 @@ def __init__(self, hostname, username, password, timeout=60, optional_args=None) self.ssl_verify = optional_args.get("ssl_verify", False) self.platform = "nxos" - def open(self): + def open(self) -> None: try: self.device = NXOSDevice( host=self.hostname, @@ -707,10 +730,10 @@ def open(self): # unable to open connection raise ConnectionException("Cannot connect to {}".format(self.hostname)) - def close(self): + def close(self) -> None: self.device = None - def _send_command(self, command, raw_text=False): + def _send_command(self, command: str, raw_text: bool = False) -> Dict[str, Union[str, Dict[str, Any]]]: """ Wrapper for NX-API show method. @@ -718,17 +741,17 @@ def _send_command(self, command, raw_text=False): """ return self.device.show(command, raw_text=raw_text) - def _send_command_list(self, commands): + def _send_command_list(self, commands: List[str]) -> List[str]: return self.device.config_list(commands) - def _send_config(self, commands): + def _send_config(self, commands: Union[str, List]) -> List[str]: if isinstance(commands, str): # Has to be a list generator and not generator expression (not JSON serializable) commands = [command for command in commands.splitlines() if command] return self.device.config_list(commands) @staticmethod - def _compute_timestamp(stupid_cisco_output): + def _compute_timestamp(stupid_cisco_output: str) -> float: """ Some fields such `uptime` are returned as: 23week(s) 3day(s) This method will determine the epoch of the event. @@ -769,19 +792,19 @@ def _compute_timestamp(stupid_cisco_output): ) return time.time() - delta - def is_alive(self): + def is_alive(self) -> models.AliveDict: if self.device: return {"is_alive": True} else: return {"is_alive": False} - def _copy_run_start(self): + def _copy_run_start(self) -> None: results = self.device.save(filename="startup-config") if not results: msg = "Unable to save running-config to startup-config!" raise CommandErrorException(msg) - def _load_cfg_from_checkpoint(self): + def _load_cfg_from_checkpoint(self) -> None: commands = [ "terminal dont-ask", "rollback running-config file {}".format(self.candidate_cfg), @@ -810,13 +833,13 @@ def _load_cfg_from_checkpoint(self): elif rollback_result == []: raise ReplaceConfigException - def rollback(self): + def rollback(self) -> None: if self.changed: self.device.rollback(self.rollback_cfg) self._copy_run_start() self.changed = False - def get_facts(self): + def get_facts(self) -> models.FactsDict: facts = {} facts["vendor"] = "Cisco" @@ -866,7 +889,7 @@ def get_facts(self): return facts - def get_interfaces(self): + def get_interfaces(self) -> Dict[str, models.InterfaceDict]: interfaces = {} iface_cmd = "show interface" interfaces_out = self._send_command(iface_cmd) @@ -923,7 +946,7 @@ def get_interfaces(self): } return interfaces - def get_bgp_neighbors(self): + def get_bgp_neighbors(self) -> Dict[str, models.BGPStateNeighborsPerVRFDict]: results = {} bgp_state_dict = { "Idle": {"is_up": False, "is_enabled": True}, @@ -1003,8 +1026,8 @@ def get_bgp_neighbors(self): results[vrf_name] = result_vrf_dict return results - def cli(self, commands): - cli_output = {} + def cli(self, commands: List[str]) -> Dict[str, Union[str, Dict[str, Any]]]: + cli_output: Dict[str, Union[str, Dict[str, Any]]] = {} if type(commands) is not list: raise TypeError("Please enter a valid list of commands!") @@ -1013,7 +1036,7 @@ def cli(self, commands): cli_output[str(command)] = command_output return cli_output - def get_arp_table(self, vrf=""): + def get_arp_table(self, vrf: str = "") -> Dict[str, models.ARPTableDict]: if vrf: msg = "VRF support has not been added for this getter on this platform." raise NotImplementedError(msg) @@ -1059,7 +1082,7 @@ def get_arp_table(self, vrf=""): ) return arp_table - def _get_ntp_entity(self, peer_type): + def _get_ntp_entity(self, peer_type: str) -> Dict[str, Dict]: ntp_entities = {} command = "show ntp peers" ntp_peers_table = self._get_command_table(command, "TABLE_peers", "ROW_peers") @@ -1072,13 +1095,13 @@ def _get_ntp_entity(self, peer_type): return ntp_entities - def get_ntp_peers(self): + def get_ntp_peers(self) -> Dict[str, models.NTPPeerDict]: return self._get_ntp_entity("Peer") - def get_ntp_servers(self): + def get_ntp_servers(self) -> Dict[str, models.NTPServerDict]: return self._get_ntp_entity("Server") - def get_ntp_stats(self): + def get_ntp_stats(self) -> Dict[str, models.NTPStats]: ntp_stats = [] command = "show ntp peer-status" ntp_stats_table = self._get_command_table( @@ -1109,7 +1132,7 @@ def get_ntp_stats(self): ) return ntp_stats - def get_interfaces_ip(self): + def get_interfaces_ip(self) -> Dict[str, models.InterfacesIPDict]: interfaces_ip = {} ipv4_command = "show ip interface" ipv4_interf_table_vrf = self._get_command_table( @@ -1221,7 +1244,7 @@ def get_interfaces_ip(self): ) return interfaces_ip - def get_mac_address_table(self): + def get_mac_address_table(self) -> List[models.MACAdressTable]: mac_table = [] command = "show mac address-table" mac_table_raw = self._get_command_table( @@ -1252,7 +1275,7 @@ def get_mac_address_table(self): ) return mac_table - def get_snmp_information(self): + def get_snmp_information(self) -> models.SNMPDict: snmp_information = {} snmp_command = "show running-config" snmp_raw_output = self.cli([snmp_command]).get(snmp_command, "") @@ -1296,7 +1319,7 @@ def get_snmp_information(self): snmp_information["community"][community_name]["mode"] = mode return snmp_information - def get_users(self): + def get_users(self) -> Dict[str, models.UsersDict]: _CISCO_TO_CISCO_MAP = {"network-admin": 15, "network-operator": 5} _DEFAULT_USER_DICT = {"password": "", "level": 0, "sshkeys": []} @@ -1338,8 +1361,13 @@ def get_users(self): users[username]["sshkeys"].append(str(sshkeyvalue)) return users +<<<<<<< HEAD def get_network_instances(self, name=""): """get_network_instances implementation for NX-OS""" +======= + def get_network_instances(self, name: str = "") -> Dict[str, models.NetworkInstanceDict]: + """ get_network_instances implementation for NX-OS """ +>>>>>>> b9ee5a2a... Make tests pass by ignoring the annotations part of the arg spec # command 'show vrf detail' returns all VRFs with detailed information # format: list of dictionaries with keys such as 'vrf_name' and 'rd' @@ -1356,7 +1384,7 @@ def get_network_instances(self, name=""): for intf in intf_table_raw: vrf_intfs[intf["vrf_name"]].append(str(intf["if_name"])) - vrfs = {} + vrfs: Dict[str, Any] = {} for vrf in vrf_table_raw: vrf_name = str(vrf.get("vrf_name")) vrfs[vrf_name] = {} @@ -1388,7 +1416,7 @@ def get_network_instances(self, name=""): else: return vrfs - def get_environment(self): + def get_environment(self) -> models.EnvironmentDict: def _process_pdus(power_data): normalized = defaultdict(dict) # some nexus devices have keys postfixed with the shorthand device series name (ie n3k) @@ -1506,7 +1534,7 @@ def _process_memory(memory_data): "memory": _process_memory(memory_raw), } - def get_vlans(self): + def get_vlans(self) -> Dict[str, models.VlanDict]: vlans = {} command = "show vlan brief" vlan_table_raw = self._get_command_table( From ba7b56ffc52224f516eeb9d6d8cc6159d73dfae3 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Mon, 22 Mar 2021 17:55:36 +0100 Subject: [PATCH 19/74] Unpack optional in NXOSDevice.load_merge_candidate() --- napalm/nxos/nxos.py | 1 + 1 file changed, 1 insertion(+) diff --git a/napalm/nxos/nxos.py b/napalm/nxos/nxos.py index 9992d1b46..4a0fbb1b3 100644 --- a/napalm/nxos/nxos.py +++ b/napalm/nxos/nxos.py @@ -153,6 +153,7 @@ def load_merge_candidate( with open(filename, "r") as f: self.merge_candidate += f.read() else: + assert isinstance(config, str) self.merge_candidate += config self.replace = False self.loaded = True From 4df5ff2099bf67c54a3edc9290471946d8e298a6 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Mon, 22 Mar 2021 22:00:52 +0100 Subject: [PATCH 20/74] Add lots of type hints, asserts and some abstract methods. Enable type checking for nxapi_plumbing --- mypy.ini | 6 + napalm/base/base.py | 8 +- napalm/base/test/models.py | 25 ++-- napalm/nxapi_plumbing/api_client.py | 88 +++++++++--- napalm/nxapi_plumbing/device.py | 35 ++--- napalm/nxos/nxos.py | 201 +++++++++++++++++----------- 6 files changed, 235 insertions(+), 128 deletions(-) diff --git a/mypy.ini b/mypy.ini index 52d4dcba2..ad9b3c264 100644 --- a/mypy.ini +++ b/mypy.ini @@ -5,6 +5,12 @@ allow_redefinition = True [mypy-napalm.base.*] disallow_untyped_defs = True +[mypy-napalm.nxos.*] +disallow_untyped_defs = True + +[mypy-napalm.nxapi_plumbing.*] +disallow_untyped_defs = True + [mypy-napalm.base.test.*] ignore_errors = True diff --git a/napalm/base/base.py b/napalm/base/base.py index 01a804a0f..e02093f75 100644 --- a/napalm/base/base.py +++ b/napalm/base/base.py @@ -387,7 +387,7 @@ def get_interfaces(self) -> Dict[str, models.InterfaceDict]: """ raise NotImplementedError - def get_lldp_neighbors(self) -> Dict[str, models.LLDPNeighborDict]: + def get_lldp_neighbors(self) -> Dict[str, List[models.LLDPNeighborDict]]: """ Returns a dictionary where the keys are local ports and the value is a list of \ dictionaries with the following information: @@ -855,7 +855,7 @@ def get_bgp_neighbors_detail( """ raise NotImplementedError - def get_arp_table(self, vrf: str = "") -> models.ARPTableDict: + def get_arp_table(self, vrf: str = "") -> List[models.ARPTableDict]: """ Returns a list of dictionaries having the following set of keys: @@ -967,7 +967,7 @@ def get_ntp_stats(self) -> List[NTPStats]: """ raise NotImplementedError - def get_interfaces_ip(self) -> models.InterfacesIPDict: + def get_interfaces_ip(self) -> Dict[str, models.InterfacesIPDict]: """ Returns all configured IP addresses on all interfaces as a dictionary of dictionaries. @@ -1149,7 +1149,7 @@ def get_route_to( """ raise NotImplementedError - def get_snmp_information(self) -> Dict[str, models.SNMPDict]: + def get_snmp_information(self) -> models.SNMPDict: """ Returns a dict of dicts containing SNMP configuration. diff --git a/napalm/base/test/models.py b/napalm/base/test/models.py index f4192896b..ecfc2ddc9 100644 --- a/napalm/base/test/models.py +++ b/napalm/base/test/models.py @@ -54,7 +54,7 @@ }, ) -LLDPNeighborsDetailDict = Dict[str, LLDPNeighborDetailDict] +LLDPNeighborsDetailDict = Dict[str, List[LLDPNeighborDetailDict]] InterfaceCounterDict = TypedDict( "InterfaceCounterDict", @@ -92,8 +92,8 @@ "fans": Dict[str, FanDict], "temperature": Dict[str, TemperatureDict], "power": Dict[str, PowerDict], - "cpu": Dict[str, CPUDict], - "memory": Dict[str, MemoryDict], + "cpu": Dict[int, CPUDict], + "memory": MemoryDict, }, ) @@ -229,6 +229,7 @@ { # will populate it in the future wit potential keys }, + total=False, ) NTPServerDict = TypedDict( @@ -236,6 +237,7 @@ { # will populate it in the future wit potential keys }, + total=False, ) NTPStats = TypedDict( @@ -255,10 +257,13 @@ }, ) -InterfacesIPDictEntry = TypedDict("InterfacesIPDictEntry", {"prefix_length": int}) +InterfacesIPDictEntry = TypedDict( + "InterfacesIPDictEntry", {"prefix_length": int}, total=False +) InterfacesIPDict = Dict[ - str, Dict[Union[Literal["ipv4"], Literal["ipv6"]], Dict[str, InterfacesIPDictEntry]] + str, + Dict[Union[Literal["ipv4"], Literal["ipv6"]], InterfacesIPDictEntry], ] MACAdressTable = TypedDict( @@ -348,7 +353,9 @@ ) PingResultDict = TypedDict( - "PingResultDict", {"success": Optional[PingDict], "error": Optional[str]}, total=False + "PingResultDict", + {"success": PingDict, "error": str}, + total=False, ) TracerouteDict = TypedDict( @@ -356,13 +363,13 @@ ) TracerouteResultDictEntry = TypedDict( - "TracerouteResultDictEntry", {"probes": Dict[int, TracerouteDict]} + "TracerouteResultDictEntry", {"probes": Dict[int, TracerouteDict]}, total=False ) TracerouteResultDict = TypedDict( "TracerouteResultDict", - {"success": Optional[Dict[int, TracerouteResultDictEntry]], "error": Optional[str]}, - total=False + {"success": Dict[int, TracerouteResultDictEntry], "error": str}, + total=False, ) UsersDict = TypedDict("UsersDict", {"level": int, "password": str, "sshkeys": List}) diff --git a/napalm/nxapi_plumbing/api_client.py b/napalm/nxapi_plumbing/api_client.py index 798e449a7..d9fb16359 100644 --- a/napalm/nxapi_plumbing/api_client.py +++ b/napalm/nxapi_plumbing/api_client.py @@ -6,6 +6,8 @@ from __future__ import print_function, unicode_literals from builtins import super +from typing import Optional, List, Dict, Union, Any + import requests from requests.auth import HTTPBasicAuth from requests.exceptions import ConnectionError @@ -31,13 +33,13 @@ class RPCBase(object): def __init__( self, - host, - username, - password, - transport="https", - port=None, - timeout=30, - verify=True, + host: str, + username: str, + password: str, + transport: str = "https", + port: Optional[int] = None, + timeout: int = 30, + verify: bool = True, ): if transport not in ["http", "https"]: raise NXAPIError("'{}' is an invalid transport.".format(transport)) @@ -53,11 +55,36 @@ def __init__( self.password = password self.timeout = timeout self.verify = verify + self.cmd_method: str + self.cmd_method_conf: str + self.cmd_method_raw: str + self.headers: Dict + + def _process_api_response( + self, response: str, commands: List[str], raw_text: bool = False + ) -> List[Any]: + raise NotImplementedError("Method must be implemented in child class") - def _process_api_response(self, response, commands, raw_text=False): + def _nxapi_command_conf( + self, commands: List[str], method: Optional[str] = None + ) -> List[Any]: + raise NotImplementedError("Method must be implemented in child class") + + def _build_payload( + self, + commands: List[str], + method: str, + rpc_version: str = "2.0", + api_version: str = "1.0", + ) -> str: raise NotImplementedError("Method must be implemented in child class") - def _send_request(self, commands, method): + def _nxapi_command( + self, commands: List[str], method: Optional[str] = None + ) -> List[Any]: + raise NotImplementedError("Method must be implemented in child class") + + def _send_request(self, commands: List[str], method: str) -> str: payload = self._build_payload(commands, method) try: @@ -94,7 +121,7 @@ def _send_request(self, commands, method): class RPCClient(RPCBase): - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) self.headers = {"content-type": "application/json-rpc"} self.api = "jsonrpc" @@ -102,25 +129,30 @@ def __init__(self, *args, **kwargs): self.cmd_method_conf = "cli" self.cmd_method_raw = "cli_ascii" - def _nxapi_command(self, commands, method=None): + def _nxapi_command( + self, commands: List[str], method: Optional[str] = None + ) -> List[Any]: """Send a command down the NX-API channel.""" if method is None: method = self.cmd_method if isinstance(commands, string_types): commands = [commands] + raw_text = True if method == "cli_ascii" else False response = self._send_request(commands, method=method) api_response = self._process_api_response(response, commands, raw_text=raw_text) return api_response - def _nxapi_command_conf(self, commands, method=None): + def _nxapi_command_conf( + self, commands: List[str], method: Optional[str] = None + ) -> List[Any]: if method is None: method = self.cmd_method_conf return self._nxapi_command(commands=commands, method=method) - def _build_payload(self, commands, method, rpc_version="2.0", api_version=1.0): + def _build_payload(self, commands: List[str], method: str, rpc_version: str = "2.0", api_version: float = 1.0) -> str: """Construct the JSON-RPC payload for NX-API.""" payload_list = [] id_num = 1 @@ -136,7 +168,9 @@ def _build_payload(self, commands, method, rpc_version="2.0", api_version=1.0): return json.dumps(payload_list) - def _process_api_response(self, response, commands, raw_text=False): + def _process_api_response( + self, response: str, commands: List[str], raw_text: bool = False + ) -> List[Any]: """ Normalize the API response including handling errors; adding the sent command into the returned data strucutre; make response structure consistent for raw_text and @@ -174,7 +208,7 @@ def _process_api_response(self, response, commands, raw_text=False): return new_response - def _error_check(self, command_response): + def _error_check(self, command_response: Dict) -> None: error = command_response.get("error") if error: command = command_response.get("command") @@ -185,7 +219,7 @@ def _error_check(self, command_response): class XMLClient(RPCBase): - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.headers = {"content-type": "application/xml"} self.api = "xml" @@ -193,7 +227,9 @@ def __init__(self, *args, **kwargs): self.cmd_method_conf = "cli_conf" self.cmd_method_raw = "cli_show_ascii" - def _nxapi_command(self, commands, method=None): + def _nxapi_command( + self, commands: List[str], method: Optional[str] = None + ) -> List[Any]: """Send a command down the NX-API channel.""" if method is None: method = self.cmd_method @@ -207,12 +243,20 @@ def _nxapi_command(self, commands, method=None): self._error_check(command_response) return api_response - def _nxapi_command_conf(self, commands, method=None): + def _nxapi_command_conf( + self, commands: List[str], method: Optional[str] = None + ) -> List[Any]: if method is None: method = self.cmd_method_conf return self._nxapi_command(commands=commands, method=method) - def _build_payload(self, commands, method, xml_version="1.0", version="1.0"): + def _build_payload( + self, + commands: List[str], + method: str, + xml_version: str = "1.0", + version: str = "1.0", + ) -> str: xml_commands = "" for command in commands: if not xml_commands: @@ -238,7 +282,9 @@ def _build_payload(self, commands, method, xml_version="1.0", version="1.0"): ) return payload - def _process_api_response(self, response, commands, raw_text=False): + def _process_api_response( + self, response: str, commands: List[str], raw_text: bool = False + ) -> List[Any]: xml_root = etree.fromstring(response) response_list = xml_root.xpath("outputs/output") if len(commands) != len(response_list): @@ -248,7 +294,7 @@ def _process_api_response(self, response, commands, raw_text=False): return response_list - def _error_check(self, command_response): + def _error_check(self, command_response: etree) -> None: """commmand_response will be an XML Etree object.""" error_list = command_response.find("./clierror") command_obj = command_response.find("./input") diff --git a/napalm/nxapi_plumbing/device.py b/napalm/nxapi_plumbing/device.py index 188af45f1..02f8ce4a8 100644 --- a/napalm/nxapi_plumbing/device.py +++ b/napalm/nxapi_plumbing/device.py @@ -6,21 +6,23 @@ from __future__ import print_function, unicode_literals +from typing import List, Optional, Dict, Any, Union + from napalm.nxapi_plumbing.errors import NXAPIError, NXAPICommandError -from napalm.nxapi_plumbing.api_client import RPCClient, XMLClient +from napalm.nxapi_plumbing.api_client import RPCClient, XMLClient, RPCBase class Device(object): def __init__( self, - host, - username, - password, - transport="http", - api_format="jsonrpc", - port=None, - timeout=30, - verify=True, + host: str, + username: str, + password: str, + transport: str = "http", + api_format: str = "jsonrpc", + port: Optional[int] = None, + timeout: int = 30, + verify: bool = True, ): self.host = host self.username = username @@ -30,6 +32,7 @@ def __init__( self.verify = verify self.port = port + self.api: RPCBase if api_format == "xml": self.api = XMLClient( host, @@ -51,7 +54,7 @@ def __init__( verify=verify, ) - def show(self, command, raw_text=False): + def show(self, command: str, raw_text: bool = False) -> Union[str, List]: """Send a non-configuration command. Args: @@ -79,7 +82,7 @@ def show(self, command, raw_text=False): return result - def show_list(self, commands, raw_text=False): + def show_list(self, commands: List[str], raw_text: bool = False) -> List[Any]: """Send a list of non-configuration commands. Args: @@ -94,7 +97,7 @@ def show_list(self, commands, raw_text=False): cmd_method = self.api.cmd_method_raw if raw_text else self.api.cmd_method return self.api._nxapi_command(commands, method=cmd_method) - def config(self, command): + def config(self, command: str) -> Union[str, List]: """Send a configuration command. Args: @@ -120,7 +123,7 @@ def config(self, command): return result - def config_list(self, commands): + def config_list(self, commands: List[str]) -> List[Any]: """Send a list of configuration commands. Args: @@ -131,7 +134,7 @@ def config_list(self, commands): """ return self.api._nxapi_command_conf(commands) - def save(self, filename="startup-config"): + def save(self, filename: str = "startup-config") -> bool: """Save a device's running configuration. Args: @@ -148,7 +151,7 @@ def save(self, filename="startup-config"): raise return True - def rollback(self, filename): + def rollback(self, filename: str) -> None: """Rollback to a checkpoint file. Args: @@ -158,7 +161,7 @@ def rollback(self, filename): cmd = "rollback running-config file {}".format(filename) self.show(cmd, raw_text=True) - def checkpoint(self, filename): + def checkpoint(self, filename: str) -> None: """Save a checkpoint of the running configuration to the device. Args: diff --git a/napalm/nxos/nxos.py b/napalm/nxos/nxos.py index 4a0fbb1b3..d76e0f3e6 100644 --- a/napalm/nxos/nxos.py +++ b/napalm/nxos/nxos.py @@ -13,49 +13,50 @@ # License for the specific language governing permissions and limitations under # the License. -# import stdlib -from builtins import super +import json import os import re -import time import tempfile +import time import uuid +# import stdlib +from abc import abstractmethod +from builtins import super from collections import defaultdict - # import third party lib -from typing import Optional, Dict, List, Union, Any +from typing import Optional, Dict, List, Union, Any, cast, Callable, TypeVar -from requests.exceptions import ConnectionError from netaddr import IPAddress from netaddr.core import AddrFormatError -from netmiko import file_transfer - -from napalm.base.test import models -from napalm.nxapi_plumbing import Device as NXOSDevice -from napalm.nxapi_plumbing import ( - NXAPIAuthError, - NXAPIConnectionError, - NXAPICommandError, -) -import json +from netmiko import file_transfer, ConnectHandler +from requests.exceptions import ConnectionError +import napalm.base.constants as c # import NAPALM Base import napalm.base.helpers from napalm.base import NetworkDriver +from napalm.base.exceptions import CommandErrorException from napalm.base.exceptions import ConnectionException from napalm.base.exceptions import MergeConfigException -from napalm.base.exceptions import CommandErrorException from napalm.base.exceptions import ReplaceConfigException -from napalm.base.helpers import generate_regex_or from napalm.base.helpers import as_number +from napalm.base.helpers import generate_regex_or from napalm.base.netmiko_helpers import netmiko_args -import napalm.base.constants as c +from napalm.base.test import models +from napalm.nxapi_plumbing import Device as NXOSDevice +from napalm.nxapi_plumbing import ( + NXAPIAuthError, + NXAPIConnectionError, + NXAPICommandError, +) +F = TypeVar('F', bound=Callable[..., Any]) -def ensure_netmiko_conn(func): + +def ensure_netmiko_conn(func: F) -> F: """Decorator that ensures Netmiko connection exists.""" - def wrap_function(self, filename=None, config=None): + def wrap_function(self, filename=None, config=None): # type: ignore try: netmiko_object = self._netmiko_device if netmiko_object is None: @@ -70,7 +71,7 @@ def wrap_function(self, filename=None, config=None): ) func(self, filename=filename, config=config) - return wrap_function + return cast(F, wrap_function) class NXOSDriverBase(NetworkDriver): @@ -112,6 +113,7 @@ def load_replace_candidate( ) if not filename: + assert isinstance(config, str) tmp_file = self._create_tmp_file(config) filename = tmp_file else: @@ -158,7 +160,9 @@ def load_merge_candidate( self.replace = False self.loaded = True - def _send_command(self, command: str, raw_text: bool = False) -> str: + def _send_command( + self, command: str, raw_text: bool = False + ) -> Dict[str, Union[str, Dict[str, Any]]]: raise NotImplementedError def _commit_merge(self) -> None: @@ -198,7 +202,7 @@ def _get_merge_diff(self) -> str: def _get_diff(self) -> str: """Get a diff between running config and a proposed file.""" - diff = [] + diff: List[str] = [] self._create_sot_file() diff_out = self._send_command( "show diff rollback-patch file {} file {}".format( @@ -206,11 +210,12 @@ def _get_diff(self) -> str: ), raw_text=True, ) + assert isinstance(diff_out, str) try: diff_out = ( diff_out.split("Generating Rollback Patch")[1] - .replace("Rollback Patch is Empty", "") - .strip() + .replace("Rollback Patch is Empty", "") + .strip() ) for line in diff_out.splitlines(): if line: @@ -311,7 +316,7 @@ def ping( * ip_address (str) * rtt (float) """ - ping_dict = {} + ping_dict: models.PingResultDict = {} version = "" try: @@ -334,6 +339,7 @@ def ping( if vrf != "": command += " vrf {}".format(vrf) output = self._send_command(command, raw_text=True) + assert isinstance(output, str) if "connect:" in output: ping_dict["error"] = output @@ -383,12 +389,12 @@ def ping( fields[3] ) elif "min/avg/max" in line: - m = fields[3].split("/") + split_fields = fields[3].split("/") ping_dict["success"].update( { - "rtt_min": float(m[0]), - "rtt_avg": float(m[1]), - "rtt_max": float(m[2]), + "rtt_min": float(split_fields[0]), + "rtt_avg": float(split_fields[1]), + "rtt_max": float(split_fields[2]), } ) ping_dict["success"].update({"results": results_array}) @@ -422,7 +428,7 @@ def traceroute( _HOP_ENTRY = [r"\s?", r"(\d+)"] # space before hop index? # hop index - traceroute_result = {} + traceroute_result: models.TracerouteResultDict = {} timeout = 5 # seconds probes = 3 # 3 probes/jop and this cannot be changed on NXOS! @@ -453,6 +459,8 @@ def traceroute( "error": "Cannot execute traceroute on the device: {}".format(command) } + assert isinstance(traceroute_raw_output, str) + hop_regex = "".join(_HOP_ENTRY + _HOP_ENTRY_PROBE * probes) traceroute_result["success"] = {} if traceroute_raw_output: @@ -471,9 +479,9 @@ def traceroute( ip_address = napalm.base.helpers.convert( napalm.base.helpers.ip, ip_address_raw, ip_address_raw ) - rtt = hop_details[5 + probe_index * 5] - if rtt: - rtt = float(rtt) + rtt_as_string = hop_details[5 + probe_index * 5] + if rtt_as_string: + rtt = float(rtt_as_string) else: rtt = timeout * 1000.0 if not host_name: @@ -485,7 +493,7 @@ def traceroute( ip_address = "*" traceroute_result["success"][hop_index]["probes"][ probe_index + 1 - ] = { + ] = { "host_name": str(host_name), "ip_address": str(ip_address), "rtt": rtt, @@ -499,6 +507,7 @@ def _get_checkpoint_file(self) -> str: self._set_checkpoint(filename) command = "show file {}".format(filename) output = self._send_command(command, raw_text=True) + assert isinstance(output, str) self._delete_file(filename) return output @@ -536,7 +545,7 @@ def _create_tmp_file(config: str) -> str: fobj.write(config) return filename - def _disable_confirmation(self) -> str: + def _disable_confirmation(self) -> None: self._send_command_list(["terminal dont-ask"]) def get_config( @@ -551,7 +560,7 @@ def get_config( ] filter_pattern = generate_regex_or(filter_strings) - config = {"startup": "", "running": "", "candidate": ""} # default values + config: models.ConfigDict = {"startup": "", "running": "", "candidate": ""} # default values # NX-OS only supports "all" on "show run" run_full = " all" if full else "" @@ -573,9 +582,9 @@ def get_config( return config - def get_lldp_neighbors(self) -> Dict[str, models.LLDPNeighborDict]: + def get_lldp_neighbors(self) -> Dict[str, List[models.LLDPNeighborDict]]: """IOS implementation of get_lldp_neighbors.""" - lldp = {} + lldp: Dict[str, List[models.LLDPNeighborDict]] = {} neighbors_detail = self.get_lldp_neighbors_detail() for intf_name, entries in neighbors_detail.items(): lldp[intf_name] = [] @@ -585,7 +594,7 @@ def get_lldp_neighbors(self) -> Dict[str, models.LLDPNeighborDict]: # When lacking a system name (in show lldp neighbors) if hostname == "N/A": hostname = lldp_entry["remote_chassis_id"] - lldp_dict = {"port": lldp_entry["remote_port"], "hostname": hostname} + lldp_dict: models.LLDPNeighborDict = {"port": lldp_entry["remote_port"], "hostname": hostname} lldp[intf_name].append(lldp_dict) return lldp @@ -593,8 +602,8 @@ def get_lldp_neighbors(self) -> Dict[str, models.LLDPNeighborDict]: def get_lldp_neighbors_detail( self, interface: str = "" ) -> models.LLDPNeighborsDetailDict: - lldp: Dict[str, str] = {} - lldp_interfaces = [] + lldp: models.LLDPNeighborsDetailDict = {} + lldp_interfaces: List[str] = [] if interface: command = "show lldp neighbors interface {} detail".format(interface) @@ -667,6 +676,7 @@ def _get_reply_table( def _get_command_table(self, command: str, table_name: str, row_name: str) -> List: json_output = self._send_command(command) if type(json_output) is not dict and json_output: + assert isinstance(json_output, str) json_output = json.loads(json_output) return self._get_reply_table(json_output, table_name, row_name) @@ -693,9 +703,32 @@ def _parse_vlan_ports(self, vlan_s: Union[str, List]) -> List: vlans.append(napalm.base.helpers.canonical_interface_name(vls.strip())) return vlans + @abstractmethod + def _send_config(self, commands: Union[str, List]) -> List[str]: + raise NotImplementedError + + @abstractmethod + def _load_cfg_from_checkpoint(self) -> None: + raise NotImplementedError + + @abstractmethod + def _copy_run_start(self) -> None: + raise NotImplementedError + + @abstractmethod + def _send_command_list(self, commands: List[str]) -> List[str]: + raise NotImplementedError + class NXOSDriver(NXOSDriverBase): - def __init__(self, hostname: str, username: str, password: str, timeout: int = 60, optional_args: Optional[Dict] = None) -> None: + def __init__( + self, + hostname: str, + username: str, + password: str, + timeout: int = 60, + optional_args: Optional[Dict] = None, + ) -> None: super().__init__( hostname, username, password, timeout=timeout, optional_args=optional_args ) @@ -734,7 +767,9 @@ def open(self) -> None: def close(self) -> None: self.device = None - def _send_command(self, command: str, raw_text: bool = False) -> Dict[str, Union[str, Dict[str, Any]]]: + def _send_command( + self, command: str, raw_text: bool = False + ) -> Dict[str, Union[str, Dict[str, Any]]]: """ Wrapper for NX-API show method. @@ -742,7 +777,7 @@ def _send_command(self, command: str, raw_text: bool = False) -> Dict[str, Union """ return self.device.show(command, raw_text=raw_text) - def _send_command_list(self, commands: List[str]) -> List[str]: + def _send_command_list(self, commands: List[str]) -> List[Any]: return self.device.config_list(commands) def _send_config(self, commands: Union[str, List]) -> List[str]: @@ -771,7 +806,7 @@ def _compute_timestamp(stupid_cisco_output: str) -> float: stupid_cisco_output = stupid_cisco_output.replace("d", "day(s) ") stupid_cisco_output = stupid_cisco_output.replace("h", "hour(s)") - things = { + things: Dict[str, Dict[str, Union[int, float]]] = { "second(s)": {"weight": 1}, "minute(s)": {"weight": 60}, "hour(s)": {"weight": 3600}, @@ -822,6 +857,7 @@ def _load_cfg_from_checkpoint(self) -> None: # For nx-api a list is returned so extract the result associated with the # 'rollback' command. rollback_result = rollback_result[1] + assert isinstance(rollback_result, dict) msg = ( rollback_result.get("msg") if rollback_result.get("msg") @@ -835,13 +871,14 @@ def _load_cfg_from_checkpoint(self) -> None: raise ReplaceConfigException def rollback(self) -> None: + assert isinstance(self.device, NXOSDevice) if self.changed: self.device.rollback(self.rollback_cfg) self._copy_run_start() self.changed = False def get_facts(self) -> models.FactsDict: - facts = {} + facts: models.FactsDict = {} facts["vendor"] = "Cisco" show_inventory_table = self._get_command_table( @@ -858,6 +895,7 @@ def get_facts(self) -> models.FactsDict: break show_version = self._send_command("show version") + show_version = cast(Dict[str, str], show_version) facts["model"] = show_version.get("chassis_id", "") facts["hostname"] = show_version.get("host_name", "") facts["os_version"] = show_version.get( @@ -891,7 +929,7 @@ def get_facts(self) -> models.FactsDict: return facts def get_interfaces(self) -> Dict[str, models.InterfaceDict]: - interfaces = {} + interfaces: Dict[str, models.InterfaceDict] = {} iface_cmd = "show interface" interfaces_out = self._send_command(iface_cmd) interfaces_body = interfaces_out["TABLE_interface"]["ROW_interface"] @@ -977,7 +1015,10 @@ def get_bgp_neighbors(self) -> Dict[str, models.BGPStateNeighborsPerVRFDict]: vrf_list = [] for vrf_dict in vrf_list: - result_vrf_dict = {"router_id": str(vrf_dict["vrf-router-id"]), "peers": {}} + result_vrf_dict: models.BGPStateNeighborsPerVRFDict = { + "router_id": str(vrf_dict["vrf-router-id"]), + "peers": {}, + } af_list = vrf_dict.get("TABLE_af", {}).get("ROW_af", []) if isinstance(af_list, dict): @@ -1001,7 +1042,7 @@ def get_bgp_neighbors(self) -> Dict[str, models.BGPStateNeighborsPerVRFDict]: afid_dict = af_name_dict[int(af_dict["af-id"])] safi_name = afid_dict[int(saf_dict["safi"])] - result_peer_dict = { + result_peer_dict: models.BGPStateNeighborDict = { "local_as": as_number(vrf_dict["vrf-local-as"]), "remote_as": remoteas, "remote_id": neighborid, @@ -1037,12 +1078,12 @@ def cli(self, commands: List[str]) -> Dict[str, Union[str, Dict[str, Any]]]: cli_output[str(command)] = command_output return cli_output - def get_arp_table(self, vrf: str = "") -> Dict[str, models.ARPTableDict]: + def get_arp_table(self, vrf: str = "") -> List[models.ARPTableDict]: if vrf: msg = "VRF support has not been added for this getter on this platform." raise NotImplementedError(msg) - arp_table = [] + arp_table: List[models.ARPTableDict] = [] command = "show ip arp" arp_table_vrf = self._get_command_table(command, "TABLE_vrf", "ROW_vrf") arp_table_raw = self._get_table_rows(arp_table_vrf[0], "TABLE_adj", "ROW_adj") @@ -1083,8 +1124,10 @@ def get_arp_table(self, vrf: str = "") -> Dict[str, models.ARPTableDict]: ) return arp_table - def _get_ntp_entity(self, peer_type: str) -> Dict[str, Dict]: - ntp_entities = {} + def _get_ntp_entity( + self, peer_type: str + ) -> Dict[str, Union[models.NTPPeerDict, models.NTPServerDict]]: + ntp_entities: Dict[str, Union[models.NTPPeerDict, models.NTPServerDict]] = {} command = "show ntp peers" ntp_peers_table = self._get_command_table(command, "TABLE_peers", "ROW_peers") @@ -1092,7 +1135,8 @@ def _get_ntp_entity(self, peer_type: str) -> Dict[str, Dict]: if ntp_peer.get("serv_peer", "").strip() != peer_type: continue peer_addr = napalm.base.helpers.ip(ntp_peer.get("PeerIPAddress").strip()) - ntp_entities[peer_addr] = {} + # Ignore the type of the following line until NTP data is modelled + ntp_entities[peer_addr] = {} # type: ignore return ntp_entities @@ -1102,8 +1146,8 @@ def get_ntp_peers(self) -> Dict[str, models.NTPPeerDict]: def get_ntp_servers(self) -> Dict[str, models.NTPServerDict]: return self._get_ntp_entity("Server") - def get_ntp_stats(self) -> Dict[str, models.NTPStats]: - ntp_stats = [] + def get_ntp_stats(self) -> List[models.NTPStats]: + ntp_stats: List[models.NTPStats] = [] command = "show ntp peer-status" ntp_stats_table = self._get_command_table( command, "TABLE_peersstatus", "ROW_peersstatus" @@ -1134,7 +1178,7 @@ def get_ntp_stats(self) -> Dict[str, models.NTPStats]: return ntp_stats def get_interfaces_ip(self) -> Dict[str, models.InterfacesIPDict]: - interfaces_ip = {} + interfaces_ip: Dict[str, models.InterfacesIPDict] = {} ipv4_command = "show ip interface" ipv4_interf_table_vrf = self._get_command_table( ipv4_command, "TABLE_intf", "ROW_intf" @@ -1246,7 +1290,7 @@ def get_interfaces_ip(self) -> Dict[str, models.InterfacesIPDict]: return interfaces_ip def get_mac_address_table(self) -> List[models.MACAdressTable]: - mac_table = [] + mac_table: List[models.MACAdressTable] = [] command = "show mac address-table" mac_table_raw = self._get_command_table( command, "TABLE_mac_address", "ROW_mac_address" @@ -1287,7 +1331,7 @@ def get_snmp_information(self) -> models.SNMPDict: if not snmp_config: return snmp_information - snmp_information = { + snmp_information: models.SNMPDict = { "contact": str(""), "location": str(""), "community": {}, @@ -1323,9 +1367,13 @@ def get_snmp_information(self) -> models.SNMPDict: def get_users(self) -> Dict[str, models.UsersDict]: _CISCO_TO_CISCO_MAP = {"network-admin": 15, "network-operator": 5} - _DEFAULT_USER_DICT = {"password": "", "level": 0, "sshkeys": []} + _DEFAULT_USER_DICT: models.UsersDict = { + "password": "", + "level": 0, + "sshkeys": [], + } - users = {} + users: Dict[str, models.UsersDict] = {} command = "show running-config" section_username_raw_output = self.cli([command]).get(command, "") section_username_tabled_output = napalm.base.helpers.textfsm_extractor( @@ -1349,7 +1397,7 @@ def get_users(self) -> Dict[str, models.UsersDict]: level = int(role.split("-")[-1]) else: level = _CISCO_TO_CISCO_MAP.get(role, 0) - if level > users.get(username).get("level"): + if level > users[username]["level"]: # unfortunately on Cisco you can set different priv levels for the same user # Good news though: the device will consider the highest level users[username]["level"] = level @@ -1362,13 +1410,8 @@ def get_users(self) -> Dict[str, models.UsersDict]: users[username]["sshkeys"].append(str(sshkeyvalue)) return users -<<<<<<< HEAD - def get_network_instances(self, name=""): - """get_network_instances implementation for NX-OS""" -======= def get_network_instances(self, name: str = "") -> Dict[str, models.NetworkInstanceDict]: """ get_network_instances implementation for NX-OS """ ->>>>>>> b9ee5a2a... Make tests pass by ignoring the annotations part of the arg spec # command 'show vrf detail' returns all VRFs with detailed information # format: list of dictionaries with keys such as 'vrf_name' and 'rd' @@ -1418,8 +1461,8 @@ def get_network_instances(self, name: str = "") -> Dict[str, models.NetworkInsta return vrfs def get_environment(self) -> models.EnvironmentDict: - def _process_pdus(power_data): - normalized = defaultdict(dict) + def _process_pdus(power_data: Dict) -> Dict[str, models.PowerDict]: + normalized: Dict[str, models.PowerDict] = defaultdict(dict) # some nexus devices have keys postfixed with the shorthand device series name (ie n3k) # ex. on a 9k, the key is TABLE_psinfo, but on a 3k it is TABLE_psinfo_n3k ps_info_key = [ @@ -1472,7 +1515,7 @@ def _process_pdus(power_data): ) return json.loads(json.dumps(normalized)) - def _process_fans(fan_data): + def _process_fans(fan_data: Dict) -> Dict[str, models.FanDict]: normalized = {} for entry in fan_data["TABLE_faninfo"]["ROW_faninfo"]: if "PS" in entry["fanname"]: @@ -1482,11 +1525,13 @@ def _process_fans(fan_data): # Copying the behavior of eos.py where if the fanstatus key is not found # we default the status to True "status": entry.get("fanstatus", "Ok") - == "Ok" + == "Ok" } return normalized - def _process_temperature(temperature_data): + def _process_temperature( + temperature_data: Dict, + ) -> Dict[str, models.TemperatureDict]: normalized = {} # The modname and sensor type are not unique enough keys, so adding a count count = 1 @@ -1501,12 +1546,12 @@ def _process_temperature(temperature_data): "temperature": float(entry.get("curtemp", -1)), "is_alert": entry.get("alarmstatus", "Ok").rstrip() != "Ok", "is_critical": float(entry.get("curtemp")) - > float(entry.get("majthres")), + > float(entry.get("majthres")), } count += 1 return normalized - def _process_cpu(cpu_data): + def _process_cpu(cpu_data: Dict) -> Dict[int, models.CPUDict]: idle = ( cpu_data.get("idle_percent") if cpu_data.get("idle_percent") @@ -1514,7 +1559,7 @@ def _process_cpu(cpu_data): ) return {0: {"%usage": round(100 - float(idle), 2)}} - def _process_memory(memory_data): + def _process_memory(memory_data: Dict) -> models.MemoryDict: avail = memory_data["TABLE_process_tag"]["ROW_process_tag"][ "process-memory-share-total-shm-avail" ] @@ -1536,7 +1581,7 @@ def _process_memory(memory_data): } def get_vlans(self) -> Dict[str, models.VlanDict]: - vlans = {} + vlans: Dict[str, models.VlanDict] = {} command = "show vlan brief" vlan_table_raw = self._get_command_table( command, "TABLE_vlanbriefxbrief", "ROW_vlanbriefxbrief" From f4604a2cfb823297d401707d4362dcedb252175f Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 27 Mar 2021 17:49:56 +0100 Subject: [PATCH 21/74] Solve type errors --- mypy.ini | 9 +++ napalm/base/__init__.py | 4 +- napalm/base/base.py | 15 +++- napalm/base/helpers.py | 86 ++++++++++++++------- napalm/base/mock.py | 86 +++++++++++++-------- napalm/base/netmiko_helpers.py | 5 +- napalm/base/test/models.py | 26 +++++-- napalm/base/utils/jinja_filters.py | 9 ++- napalm/base/utils/string_parsers.py | 21 ++--- napalm/base/validate.py | 58 ++++++++++---- napalm/nxapi_plumbing/api_client.py | 12 ++- napalm/nxapi_plumbing/device.py | 2 +- napalm/nxapi_plumbing/errors.py | 4 +- napalm/nxapi_plumbing/utilities.py | 2 +- napalm/nxos/nxos.py | 115 ++++++++++++++++++++-------- test/base/test_helpers.py | 6 +- 16 files changed, 321 insertions(+), 139 deletions(-) diff --git a/mypy.ini b/mypy.ini index ad9b3c264..8a0948964 100644 --- a/mypy.ini +++ b/mypy.ini @@ -8,9 +8,18 @@ disallow_untyped_defs = True [mypy-napalm.nxos.*] disallow_untyped_defs = True +[mypy-napalm.eos.*] +ignore_errors = True + +[mypy-napalm.ios.*] +ignore_errors = True + [mypy-napalm.nxapi_plumbing.*] disallow_untyped_defs = True +[mypy-napalm.base.clitools.*] +ignore_errors = True + [mypy-napalm.base.test.*] ignore_errors = True diff --git a/napalm/base/__init__.py b/napalm/base/__init__.py index fe677a7d7..934cefe77 100644 --- a/napalm/base/__init__.py +++ b/napalm/base/__init__.py @@ -19,6 +19,8 @@ import importlib # NAPALM base +from typing import Type + from napalm.base.base import NetworkDriver from napalm.base.exceptions import ModuleImportError from napalm.base.mock import MockDriver @@ -29,7 +31,7 @@ ] -def get_network_driver(name, prepend=True): +def get_network_driver(name: str, prepend: bool = True) -> Type[NetworkDriver]: """ Searches for a class derived form the base NAPALM class NetworkDriver in a specific library. The library name must repect the following pattern: napalm_[DEVICE_OS]. diff --git a/napalm/base/base.py b/napalm/base/base.py index e02093f75..37f9d631b 100644 --- a/napalm/base/base.py +++ b/napalm/base/base.py @@ -31,6 +31,13 @@ class NetworkDriver(object): + hostname: str + username: str + password: str + timeout: int + force_no_enable: bool + use_canonical_interface: bool + def __init__( self, hostname: str, @@ -54,7 +61,7 @@ def __init__( """ raise NotImplementedError - def __enter__(self) -> "NetworkDriver": + def __enter__(self) -> "NetworkDriver": # type: ignore try: self.open() return self @@ -65,7 +72,7 @@ def __enter__(self) -> "NetworkDriver": else: raise - def __exit__( + def __exit__( # type: ignore self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], @@ -74,7 +81,7 @@ def __exit__( self.close() if exc_type is not None and ( exc_type.__name__ not in dir(napalm.base.exceptions) - and exc_type.__name__ not in __builtins__.keys() + and exc_type.__name__ not in __builtins__.keys() # type: ignore ): epilog = ( "NAPALM didn't catch this exception. Please, fill a bugfix on " @@ -1776,7 +1783,7 @@ def compliance_report( self, validation_file: Optional[str] = None, validation_source: Optional[str] = None, - ) -> Dict: + ) -> models.ReportResult: """ Return a compliance report. diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index f7d9954f1..59d60b584 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -8,8 +8,11 @@ from collections.abc import Iterable # third party libs +from typing import Optional, Dict, Any, List, Union, Tuple, TypeVar, Callable, Type + import jinja2 import textfsm +from lxml import etree from netaddr import EUI from netaddr import mac_unix from netaddr import IPAddress @@ -18,9 +21,13 @@ # local modules import napalm.base.exceptions from napalm.base import constants +from napalm.base.test.models import ConfigDict from napalm.base.utils.jinja_filters import CustomJinjaFilters from napalm.base.canonical_map import base_interfaces, reverse_mapping +T = TypeVar("T") +R = TypeVar("R") + # ------------------------------------------------------------------- # Functional Global # ------------------------------------------------------------------- @@ -41,14 +48,14 @@ class _MACFormat(mac_unix): # callable helpers # ------------------------------------------------------------------- def load_template( - cls, - template_name, - template_source=None, - template_path=None, - openconfig=False, - jinja_filters={}, - **template_vars, -): + cls: "napalm.base.NetworkDriver", + template_name: str, + template_source: Optional[str] = None, + template_path: Optional[str] = None, + openconfig: bool = False, + jinja_filters: Dict = {}, + **template_vars: Any, +) -> None: try: search_path = [] if isinstance(template_source, str): @@ -111,7 +118,9 @@ def load_template( return cls.load_merge_candidate(config=configuration) -def cisco_conf_parse_parents(parent, child, config): +def cisco_conf_parse_parents( + parent: str, child: str, config: Union[str, List[str]] +) -> List[str]: """ Use CiscoConfParse to find parent lines that contain a specific child line. @@ -120,13 +129,15 @@ def cisco_conf_parse_parents(parent, child, config): :param config: The device running/startup config """ if type(config) == str: - config = config.splitlines() + config = config.splitlines() # type: ignore parse = CiscoConfParse(config) cfg_obj = parse.find_parents_w_child(parent, child) return cfg_obj -def cisco_conf_parse_objects(cfg_section, config): +def cisco_conf_parse_objects( + cfg_section: str, config: Union[str, List[str]] +) -> List[str]: """ Use CiscoConfParse to find and return a section of Cisco IOS config. Similar to "show run | section " @@ -136,7 +147,7 @@ def cisco_conf_parse_objects(cfg_section, config): """ return_config = [] if type(config) is str: - config = config.splitlines() + config = config.splitlines() # type: ignore parse = CiscoConfParse(config) cfg_obj = parse.find_objects(cfg_section) for parent in cfg_obj: @@ -146,7 +157,7 @@ def cisco_conf_parse_objects(cfg_section, config): return return_config -def regex_find_txt(pattern, text, default=""): +def regex_find_txt(pattern: str, text: str, default: str = "") -> Any: """ "" RegEx search for pattern in text. Will try to match the data type of the "default" value or return the default value if no match is found. @@ -167,7 +178,7 @@ def regex_find_txt(pattern, text, default=""): if not isinstance(value, type(default)): if isinstance(value, list) and len(value) == 1: value = value[0] - value = type(default)(value) + value = type(default)(value) # type: ignore except Exception as regexFindTxtErr01: # in case of any exception, returns default logger.error( 'errorCode="regexFindTxtErr01" in napalm.base.helpers with systemMessage="%s"\ @@ -175,11 +186,13 @@ def regex_find_txt(pattern, text, default=""): default to empty string"' % (regexFindTxtErr01) ) - value = default + value = default # type: ignore return value -def textfsm_extractor(cls, template_name, raw_text): +def textfsm_extractor( + cls: "napalm.base.NetworkDriver", template_name: str, raw_text: str +) -> List[Dict]: """ Applies a TextFSM template over a raw text and return the matching table. @@ -245,7 +258,12 @@ def textfsm_extractor(cls, template_name, raw_text): ) -def find_txt(xml_tree, path, default="", namespaces=None): +def find_txt( + xml_tree: etree._Element, + path: str, + default: str = "", + namespaces: Optional[Dict] = None, +) -> str: """ Extracts the text value from an XML tree, using XPath. In case of error or text element unavailability, will return a default value. @@ -284,7 +302,7 @@ def find_txt(xml_tree, path, default="", namespaces=None): return str(value) -def convert(to, who, default=""): +def convert(to: Callable[[T], R], who: Optional[T], default: R = "") -> R: """ Converts data to a specific datatype. In case of error, will return a default value. @@ -302,7 +320,7 @@ def convert(to, who, default=""): return default -def mac(raw): +def mac(raw: str) -> str: """ Converts a raw string to a standardised MAC Address EUI Format. @@ -339,7 +357,7 @@ def mac(raw): return str(EUI(raw, dialect=_MACFormat)) -def ip(addr, version=None): +def ip(addr: str, version: Optional[int] = None) -> str: """ Converts a raw string to a valid IP address. Optional version argument will detect that \ object matches specified version. @@ -368,7 +386,7 @@ def ip(addr, version=None): return str(addr_obj) -def as_number(as_number_val): +def as_number(as_number_val: str) -> int: """Convert AS Number to standardized asplain notation as an integer.""" as_number_str = str(as_number_val) if "." in as_number_str: @@ -378,14 +396,16 @@ def as_number(as_number_val): return int(as_number_str) -def split_interface(intf_name): +def split_interface(intf_name: str) -> Tuple[str, str]: """Split an interface name based on first digit, slash, or space match.""" head = intf_name.rstrip(r"/\0123456789. ") tail = intf_name[len(head) :].lstrip() return (head, tail) -def canonical_interface_name(interface, addl_name_map=None): +def canonical_interface_name( + interface: str, addl_name_map: Optional[Dict[str, str]] = None +) -> str: """Function to return an interface's canonical name (fully expanded name). Use of explicit matches used to indicate a clear understanding on any potential @@ -410,13 +430,18 @@ def canonical_interface_name(interface, addl_name_map=None): # check in dict for mapping if name_map.get(interface_type): long_int = name_map.get(interface_type) + assert isinstance(long_int, str) return long_int + str(interface_number) # if nothing matched, return the original name else: return interface -def abbreviated_interface_name(interface, addl_name_map=None, addl_reverse_map=None): +def abbreviated_interface_name( + interface: str, + addl_name_map: Optional[Dict[str, str]] = None, + addl_reverse_map: Optional[Dict[str, str]] = None, +) -> str: """Function to return an abbreviated representation of the interface name. :param interface: The interface you are attempting to abbreviate. @@ -449,6 +474,8 @@ def abbreviated_interface_name(interface, addl_name_map=None, addl_reverse_map=N else: canonical_type = interface_type + assert isinstance(canonical_type, str) + try: abbreviated_name = rev_name_map[canonical_type] + str(interface_number) return abbreviated_name @@ -459,7 +486,7 @@ def abbreviated_interface_name(interface, addl_name_map=None, addl_reverse_map=N return interface -def transform_lldp_capab(capabilities): +def transform_lldp_capab(capabilities: Union[str, Any]) -> List[str]: if capabilities and isinstance(capabilities, str): capabilities = capabilities.strip().lower().split(",") return sorted( @@ -469,7 +496,7 @@ def transform_lldp_capab(capabilities): return [] -def generate_regex_or(filters): +def generate_regex_or(filters: Iterable) -> str: """ Build a regular expression logical-or from a list/tuple of regex patterns. @@ -490,7 +517,7 @@ def generate_regex_or(filters): return return_pattern -def sanitize_config(config, filters): +def sanitize_config(config: str, filters: Dict) -> str: """ Given a dictionary of filters, remove sensitive data from the provided config. """ @@ -499,12 +526,13 @@ def sanitize_config(config, filters): return config -def sanitize_configs(configs, filters): +def sanitize_configs(configs: ConfigDict, filters: Dict) -> ConfigDict: """ Apply sanitize_config on the dictionary of configs typically returned by the get_config method. """ for cfg_name, config in configs.items(): + assert isinstance(config, str) if config.strip(): - configs[cfg_name] = sanitize_config(config, filters) + configs[cfg_name] = sanitize_config(config, filters) # type: ignore return configs diff --git a/napalm/base/mock.py b/napalm/base/mock.py index 0ac52f8f1..64074e6ac 100644 --- a/napalm/base/mock.py +++ b/napalm/base/mock.py @@ -11,6 +11,8 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under # the License. +from collections import Callable +from typing import Optional, List, Dict, Union, Any from napalm.base.base import NetworkDriver import napalm.base.exceptions @@ -23,8 +25,10 @@ from pydoc import locate +from napalm.base.test import models -def raise_exception(result): + +def raise_exception(result): # type: ignore exc = locate(result["exception"]) if exc: raise exc(*result.get("args", []), **result.get("kwargs", {})) @@ -32,19 +36,19 @@ def raise_exception(result): raise TypeError("Couldn't resolve exception {}", result["exception"]) -def is_mocked_method(method): +def is_mocked_method(method: str) -> bool: mocked_methods = ["traceroute", "ping"] if method.startswith("get_") or method in mocked_methods: return True return False -def mocked_method(path, name, count): +def mocked_method(path: str, name: str, count: int) -> Callable: parent_method = getattr(NetworkDriver, name) parent_method_args = inspect.getfullargspec(parent_method) modifier = 0 if "self" not in parent_method_args.args else 1 - def _mocked_method(*args, **kwargs): + def _mocked_method(*args, **kwargs): # type: ignore # Check len(args) if len(args) + len(kwargs) + modifier > len(parent_method_args.args): raise TypeError( @@ -64,7 +68,7 @@ def _mocked_method(*args, **kwargs): return _mocked_method -def mocked_data(path, name, count): +def mocked_data(path: str, name: str, count: int) -> Union[Dict, List]: filename = "{}.{}".format(os.path.join(path, name), count) try: with open(filename) as f: @@ -74,26 +78,36 @@ def mocked_data(path, name, count): if "exception" in result: raise_exception(result) + assert False else: return result class MockDevice(object): - def __init__(self, parent, profile): + def __init__(self, parent: NetworkDriver, profile: str) -> None: self.parent = parent self.profile = profile - def run_commands(self, commands): + def run_commands(self, commands: List[str]) -> str: """Mock for EOS""" - return list(self.parent.cli(commands).values())[0] + return_value = list(self.parent.cli(commands).values())[0] + assert isinstance(return_value, str) + return return_value - def show(self, command): + def show(self, command: str) -> str: """Mock for nxos""" return self.run_commands([command]) class MockDriver(NetworkDriver): - def __init__(self, hostname, username, password, timeout=60, optional_args=None): + def __init__( + self, + hostname: str, + username: str, + password: str, + timeout: int = 60, + optional_args: Optional[Dict] = None, + ) -> None: """ Supported optional_args: * path(str) - path to where the mocked files are located @@ -102,41 +116,43 @@ def __init__(self, hostname, username, password, timeout=60, optional_args=None) self.hostname = hostname self.username = username self.password = password + if not optional_args: + optional_args = {} self.path = optional_args.get("path", "") self.profile = optional_args.get("profile", []) self.fail_on_open = optional_args.get("fail_on_open", False) self.opened = False - self.calls = {} + self.calls: Dict[str, int] = {} self.device = MockDevice(self, self.profile) # None no action, True load_merge, False load_replace - self.merge = None - self.filename = None - self.config = None + self.merge: Optional[bool] = None + self.filename: Optional[str] = None + self.config: Optional[str] = None self._pending_commits = False - def _count_calls(self, name): + def _count_calls(self, name: str) -> int: current_count = self.calls.get(name, 0) self.calls[name] = current_count + 1 return self.calls[name] - def _raise_if_closed(self): + def _raise_if_closed(self) -> None: if not self.opened: raise napalm.base.exceptions.ConnectionClosedException("connection closed") - def open(self): + def open(self) -> None: if self.fail_on_open: raise napalm.base.exceptions.ConnectionException("You told me to do this") self.opened = True - def close(self): + def close(self) -> None: self.opened = False - def is_alive(self): + def is_alive(self) -> models.AliveDict: return {"is_alive": self.opened} - def cli(self, commands): + def cli(self, commands: List[str]) -> Dict[str, Union[str, Dict[str, Any]]]: count = self._count_calls("cli") result = {} regexp = re.compile("[^a-zA-Z0-9]+") @@ -146,9 +162,11 @@ def cli(self, commands): filename = "{}.{}".format(os.path.join(self.path, name), i) with open(filename, "r") as f: result[c] = f.read() - return result + return result # type: ignore - def load_merge_candidate(self, filename=None, config=None): + def load_merge_candidate( + self, filename: Optional[str] = None, config: Optional[str] = None + ) -> None: count = self._count_calls("load_merge_candidate") self._raise_if_closed() self.merge = True @@ -156,7 +174,9 @@ def load_merge_candidate(self, filename=None, config=None): self.config = config mocked_data(self.path, "load_merge_candidate", count) - def load_replace_candidate(self, filename=None, config=None): + def load_replace_candidate( + self, filename: Optional[str] = None, config: Optional[str] = None + ) -> None: count = self._count_calls("load_replace_candidate") self._raise_if_closed() self.merge = False @@ -164,12 +184,16 @@ def load_replace_candidate(self, filename=None, config=None): self.config = config mocked_data(self.path, "load_replace_candidate", count) - def compare_config(self, filename=None, config=None): + def compare_config( + self, filename: Optional[str] = None, config: Optional[str] = None + ) -> str: count = self._count_calls("compare_config") self._raise_if_closed() - return mocked_data(self.path, "compare_config", count)["diff"] + mocked = mocked_data(self.path, "compare_config", count) + assert isinstance(mocked, dict) + return mocked["diff"] - def commit_config(self, message="", revert_in=None): + def commit_config(self, message: str = "", revert_in: Optional[int] = None) -> None: count = self._count_calls("commit_config") self._raise_if_closed() if revert_in is not None: @@ -182,7 +206,7 @@ def commit_config(self, message="", revert_in=None): self.config = None mocked_data(self.path, "commit_config", count) - def discard_config(self): + def discard_config(self) -> None: count = self._count_calls("discard_config") self._raise_if_closed() self.merge = None @@ -206,11 +230,13 @@ def rollback(self): self.config_session = None self._pending_commits = False - def _rpc(self, get): + def _rpc(self, get: str) -> str: """This one is only useful for junos.""" - return list(self.cli([get]).values())[0] + return_value = list(self.cli([get]).values())[0] + assert isinstance(return_value, str) + return return_value - def __getattribute__(self, name): + def __getattribute__(self, name: str) -> Callable: if is_mocked_method(name): self._raise_if_closed() count = self._count_calls(name) diff --git a/napalm/base/netmiko_helpers.py b/napalm/base/netmiko_helpers.py index 382041f29..6455ee955 100644 --- a/napalm/base/netmiko_helpers.py +++ b/napalm/base/netmiko_helpers.py @@ -10,10 +10,12 @@ # License for the specific language governing permissions and limitations under # the License. import inspect +from typing import Dict + from netmiko import BaseConnection -def netmiko_args(optional_args): +def netmiko_args(optional_args: Dict) -> Dict: """Check for Netmiko arguments that were passed in as NAPALM optional arguments. Return a dictionary of these optional args that will be passed into the Netmiko @@ -22,6 +24,7 @@ def netmiko_args(optional_args): fields = inspect.getfullargspec(BaseConnection.__init__) args = fields[0] defaults = fields[3] + assert isinstance(defaults, tuple) check_self = args.pop(0) if check_self != "self": diff --git a/napalm/base/test/models.py b/napalm/base/test/models.py index ecfc2ddc9..e3473e12b 100644 --- a/napalm/base/test/models.py +++ b/napalm/base/test/models.py @@ -1,4 +1,4 @@ -from typing import Dict, Literal, Union, List, Optional +from typing import Dict, List try: from typing import TypedDict @@ -261,10 +261,14 @@ "InterfacesIPDictEntry", {"prefix_length": int}, total=False ) -InterfacesIPDict = Dict[ - str, - Dict[Union[Literal["ipv4"], Literal["ipv6"]], InterfacesIPDictEntry], -] +InterfacesIPDict = TypedDict( + "InterfacesIPDict", + { + "ipv4": Dict[str, InterfacesIPDictEntry], + "ipv6": Dict[str, InterfacesIPDictEntry], + }, + total=False, +) MACAdressTable = TypedDict( "MACAdressTable", @@ -431,3 +435,15 @@ ) VlanDict = TypedDict("VlanDict", {"name": str, "interfaces": List}) + +DictValidationResult = TypedDict( + "DictValidationResult", + {"complies": bool, "present": Dict, "missing": List, "extra": List}, +) + +ListValidationResult = TypedDict( + "ListValidationResult", + {"complies": bool, "present": List, "missing": List, "extra": List}, +) + +ReportResult = TypedDict("ReportResult", {"complies": bool, "skipped": List}) diff --git a/napalm/base/utils/jinja_filters.py b/napalm/base/utils/jinja_filters.py index 73f25fee4..fc1a1b95a 100644 --- a/napalm/base/utils/jinja_filters.py +++ b/napalm/base/utils/jinja_filters.py @@ -1,11 +1,12 @@ """Some common jinja filters.""" +from typing import Dict, Any class CustomJinjaFilters(object): """Utility filters for jinja2.""" @classmethod - def filters(cls): + def filters(cls) -> Dict: """Return jinja2 filters that this module provide.""" return { "oc_attr_isdefault": oc_attr_isdefault, @@ -14,7 +15,7 @@ def filters(cls): } -def oc_attr_isdefault(o): +def oc_attr_isdefault(o: Any) -> bool: """Return wether an OC attribute has been defined or not.""" if not o._changed() and not o.default(): return True @@ -23,7 +24,7 @@ def oc_attr_isdefault(o): return False -def openconfig_to_cisco_af(value): +def openconfig_to_cisco_af(value: str) -> str: """Translate openconfig AF name to Cisco AFI name.""" if ":" in value: value = value.split(":")[1] @@ -39,7 +40,7 @@ def openconfig_to_cisco_af(value): return mapd[value] -def openconfig_to_eos_af(value): +def openconfig_to_eos_af(value: str) -> str: """Translate openconfig AF name to EOS AFI name.""" if ":" in value: value = value.split(":")[1] diff --git a/napalm/base/utils/string_parsers.py b/napalm/base/utils/string_parsers.py index 005018da3..2d1ca14a9 100644 --- a/napalm/base/utils/string_parsers.py +++ b/napalm/base/utils/string_parsers.py @@ -1,25 +1,28 @@ """ Common methods to normalize a string """ import re +from typing import Union, List, Iterable, Dict, Optional -def convert(text): +def convert(text: str) -> Union[str, int]: """Convert text to integer, if it is a digit.""" if text.isdigit(): return int(text) return text -def alphanum_key(key): - """split on end numbers.""" +def alphanum_key(key: str) -> List[Union[str, int]]: + """ split on end numbers.""" return [convert(c) for c in re.split("([0-9]+)", key)] -def sorted_nicely(sort_me): - """Sort the given iterable in the way that humans expect.""" +def sorted_nicely(sort_me: Iterable) -> Iterable: + """ Sort the given iterable in the way that humans expect.""" return sorted(sort_me, key=alphanum_key) -def colon_separated_string_to_dict(string, separator=":"): +def colon_separated_string_to_dict( + string: str, separator: str = ":" +) -> Dict[str, Optional[str]]: """ Converts a string in the format: @@ -38,7 +41,7 @@ def colon_separated_string_to_dict(string, separator=":"): into a dictionary """ - dictionary = dict() + dictionary: Dict[str, Optional[str]] = dict() for line in string.splitlines(): line_data = line.split(separator) if len(line_data) > 1: @@ -52,7 +55,7 @@ def colon_separated_string_to_dict(string, separator=":"): return dictionary -def hyphen_range(string): +def hyphen_range(string: str) -> List[int]: """ Expands a string of numbers separated by commas and hyphens into a list of integers. For example: 2-3,5-7,20-21,23,100-200 @@ -76,7 +79,7 @@ def hyphen_range(string): return list_numbers -def convert_uptime_string_seconds(uptime): +def convert_uptime_string_seconds(uptime: str) -> int: """Convert uptime strings to seconds. The string can be formatted various ways.""" regex_list = [ # n years, n weeks, n days, n hours, n minutes where each of the fields except minutes diff --git a/napalm/base/validate.py b/napalm/base/validate.py index a9616adac..4eb269cec 100644 --- a/napalm/base/validate.py +++ b/napalm/base/validate.py @@ -3,18 +3,25 @@ See: https://napalm.readthedocs.io/en/latest/validate.html """ +from __future__ import annotations + +from typing import Dict, List, Union, TypeVar, Optional, TYPE_CHECKING + import yaml import copy import re +if TYPE_CHECKING: + from napalm.base import NetworkDriver from napalm.base.exceptions import ValidationException +from napalm.base.test import models # We put it here to compile it only once numeric_compare_regex = re.compile(r"^(<|>|<=|>=|==|!=)(\d+(\.\d+){0,1})$") -def _get_validation_file(validation_file): +def _get_validation_file(validation_file: str) -> Dict[str, Dict]: try: with open(validation_file, "r") as stream: try: @@ -26,7 +33,7 @@ def _get_validation_file(validation_file): return validation_source -def _mode(mode_string): +def _mode(mode_string: str) -> Dict[str, bool]: mode = {"strict": False} for m in mode_string.split(): @@ -36,8 +43,15 @@ def _mode(mode_string): return mode -def _compare_getter_list(src, dst, mode): - result = {"complies": True, "present": [], "missing": [], "extra": []} +def _compare_getter_list( + src: List, dst: List, mode: Dict[str, bool] +) -> models.ListValidationResult: + result: models.ListValidationResult = { + "complies": True, + "present": [], + "missing": [], + "extra": [], + } for src_element in src: found = False @@ -71,8 +85,15 @@ def _compare_getter_list(src, dst, mode): return result -def _compare_getter_dict(src, dst, mode): - result = {"complies": True, "present": {}, "missing": [], "extra": []} +def _compare_getter_dict( + src: Dict[str, List], dst: Dict[str, List], mode: Dict[str, bool] +) -> models.DictValidationResult: + result: models.DictValidationResult = { + "complies": True, + "present": {}, + "missing": [], + "extra": [], + } dst = copy.deepcopy(dst) # Otherwise we are going to modify a "live" object for key, src_element in src.items(): @@ -111,7 +132,12 @@ def _compare_getter_dict(src, dst, mode): return result -def compare(src, dst): +CompareInput = TypeVar("CompareInput", str, Dict, List) + + +def compare( + src: CompareInput, dst: CompareInput +) -> Union[bool, models.DictValidationResult, models.ListValidationResult]: if isinstance(src, str): src = str(src) @@ -152,7 +178,7 @@ def compare(src, dst): return src == dst -def _compare_numeric(src_num, dst_num): +def _compare_numeric(src_num: str, dst_num: str) -> bool: """Compare numerical values. You can use '<%d','>%d'.""" dst_num = float(dst_num) @@ -174,7 +200,7 @@ def _compare_numeric(src_num, dst_num): return getattr(dst_num, operand[match.group(1)])(float(match.group(2))) -def _compare_range(src_num, dst_num): +def _compare_range(src_num: str, dst_num: str) -> bool: """Compare value against a range of values. You can use '%d<->%d'.""" dst_num = float(dst_num) @@ -191,7 +217,7 @@ def _compare_range(src_num, dst_num): return False -def empty_tree(input_list): +def empty_tree(input_list: List) -> bool: """Recursively iterate through values in nested lists.""" for item in input_list: if not isinstance(item, list) or not empty_tree(item): @@ -199,14 +225,20 @@ def empty_tree(input_list): return True -def compliance_report(cls, validation_file=None, validation_source=None): - report = {} +def compliance_report( + cls: NetworkDriver, + validation_file: Optional[str] = None, + validation_source: Optional[str] = None, +) -> models.ReportResult: + report: models.ReportResult = {} # type: ignore if validation_file: - validation_source = _get_validation_file(validation_file) + validation_source = _get_validation_file(validation_file) # type: ignore # Otherwise we are going to modify a "live" object validation_source = copy.deepcopy(validation_source) + assert isinstance(validation_source, list), validation_source + for validation_check in validation_source: for getter, expected_results in validation_check.items(): if getter == "get_config": diff --git a/napalm/nxapi_plumbing/api_client.py b/napalm/nxapi_plumbing/api_client.py index d9fb16359..f9f9e3977 100644 --- a/napalm/nxapi_plumbing/api_client.py +++ b/napalm/nxapi_plumbing/api_client.py @@ -138,7 +138,6 @@ def _nxapi_command( if isinstance(commands, string_types): commands = [commands] - raw_text = True if method == "cli_ascii" else False response = self._send_request(commands, method=method) @@ -152,7 +151,13 @@ def _nxapi_command_conf( method = self.cmd_method_conf return self._nxapi_command(commands=commands, method=method) - def _build_payload(self, commands: List[str], method: str, rpc_version: str = "2.0", api_version: float = 1.0) -> str: + def _build_payload( + self, + commands: List[str], + method: str, + rpc_version: str = "2.0", + api_version: str = "1.0", + ) -> str: """Construct the JSON-RPC payload for NX-API.""" payload_list = [] id_num = 1 @@ -160,7 +165,7 @@ def _build_payload(self, commands: List[str], method: str, rpc_version: str = "2 payload = { "jsonrpc": rpc_version, "method": method, - "params": {"cmd": command, "version": api_version}, + "params": {"cmd": command, "version": float(api_version)}, "id": id_num, } payload_list.append(payload) @@ -212,6 +217,7 @@ def _error_check(self, command_response: Dict) -> None: error = command_response.get("error") if error: command = command_response.get("command") + assert isinstance(command, str) if "data" in error: raise NXAPICommandError(command, error["data"]["msg"]) else: diff --git a/napalm/nxapi_plumbing/device.py b/napalm/nxapi_plumbing/device.py index 02f8ce4a8..fb9fc6088 100644 --- a/napalm/nxapi_plumbing/device.py +++ b/napalm/nxapi_plumbing/device.py @@ -54,7 +54,7 @@ def __init__( verify=verify, ) - def show(self, command: str, raw_text: bool = False) -> Union[str, List]: + def show(self, command: str, raw_text: bool = False) -> Any: """Send a non-configuration command. Args: diff --git a/napalm/nxapi_plumbing/errors.py b/napalm/nxapi_plumbing/errors.py index 144e8ae1f..f524959e5 100644 --- a/napalm/nxapi_plumbing/errors.py +++ b/napalm/nxapi_plumbing/errors.py @@ -14,11 +14,11 @@ class NXAPIError(Exception): class NXAPICommandError(NXAPIError): - def __init__(self, command, message): + def __init__(self, command: str, message: str) -> None: self.command = command self.message = message - def __repr__(self): + def __repr__(self) -> str: return 'The command "{}" gave the error "{}".'.format( self.command, self.message ) diff --git a/napalm/nxapi_plumbing/utilities.py b/napalm/nxapi_plumbing/utilities.py index 6300de068..04a2ea9df 100644 --- a/napalm/nxapi_plumbing/utilities.py +++ b/napalm/nxapi_plumbing/utilities.py @@ -1,5 +1,5 @@ from lxml import etree -def xml_to_string(xml_object): +def xml_to_string(xml_object: etree.Element) -> str: return etree.tostring(xml_object).decode() diff --git a/napalm/nxos/nxos.py b/napalm/nxos/nxos.py index d76e0f3e6..2ece388dc 100644 --- a/napalm/nxos/nxos.py +++ b/napalm/nxos/nxos.py @@ -19,12 +19,25 @@ import tempfile import time import uuid + # import stdlib from abc import abstractmethod from builtins import super from collections import defaultdict + # import third party lib -from typing import Optional, Dict, List, Union, Any, cast, Callable, TypeVar +from typing import ( + Optional, + Dict, + List, + Union, + Any, + cast, + Callable, + TypeVar, + TypedDict, + DefaultDict, +) from netaddr import IPAddress from netaddr.core import AddrFormatError @@ -32,6 +45,7 @@ from requests.exceptions import ConnectionError import napalm.base.constants as c + # import NAPALM Base import napalm.base.helpers from napalm.base import NetworkDriver @@ -50,13 +64,22 @@ NXAPICommandError, ) -F = TypeVar('F', bound=Callable[..., Any]) +F = TypeVar("F", bound=Callable[..., Any]) +ShowIPInterfaceReturn = TypedDict( + "ShowIPInterfaceReturn", + { + "intf-name": str, + "prefix": str, + "unnum-intf": str, + "masklen": str, + }, +) def ensure_netmiko_conn(func: F) -> F: """Decorator that ensures Netmiko connection exists.""" - def wrap_function(self, filename=None, config=None): # type: ignore + def wrap_function(self, filename=None, config=None): # type: ignore try: netmiko_object = self._netmiko_device if netmiko_object is None: @@ -214,8 +237,8 @@ def _get_diff(self) -> str: try: diff_out = ( diff_out.split("Generating Rollback Patch")[1] - .replace("Rollback Patch is Empty", "") - .strip() + .replace("Rollback Patch is Empty", "") + .strip() ) for line in diff_out.splitlines(): if line: @@ -493,7 +516,7 @@ def traceroute( ip_address = "*" traceroute_result["success"][hop_index]["probes"][ probe_index + 1 - ] = { + ] = { "host_name": str(host_name), "ip_address": str(ip_address), "rtt": rtt, @@ -560,18 +583,24 @@ def get_config( ] filter_pattern = generate_regex_or(filter_strings) - config: models.ConfigDict = {"startup": "", "running": "", "candidate": ""} # default values + config: models.ConfigDict = { + "startup": "", + "running": "", + "candidate": "", + } # default values # NX-OS only supports "all" on "show run" run_full = " all" if full else "" if retrieve.lower() in ("running", "all"): command = f"show running-config{run_full}" output = self._send_command(command, raw_text=True) + assert isinstance(output, str) output = re.sub(filter_pattern, "", output, flags=re.M) config["running"] = output.strip() if retrieve.lower() in ("startup", "all"): command = "show startup-config" output = self._send_command(command, raw_text=True) + assert isinstance(output, str) output = re.sub(filter_pattern, "", output, flags=re.M) config["startup"] = output.strip() @@ -594,7 +623,10 @@ def get_lldp_neighbors(self) -> Dict[str, List[models.LLDPNeighborDict]]: # When lacking a system name (in show lldp neighbors) if hostname == "N/A": hostname = lldp_entry["remote_chassis_id"] - lldp_dict: models.LLDPNeighborDict = {"port": lldp_entry["remote_port"], "hostname": hostname} + lldp_dict: models.LLDPNeighborDict = { + "port": lldp_entry["remote_port"], + "hostname": hostname, + } lldp[intf_name].append(lldp_dict) return lldp @@ -640,7 +672,7 @@ def get_lldp_neighbors_detail( # Turn the interfaces into their long version local_intf = napalm.base.helpers.canonical_interface_name(local_intf) lldp.setdefault(local_intf, []) - lldp[local_intf].append(lldp_entry) + lldp[local_intf].append(lldp_entry) # type: ignore return lldp @@ -663,7 +695,7 @@ def _get_table_rows( if isinstance(_table, list): _table_rows = [_table_row.get(row_name) for _table_row in _table] elif isinstance(_table, dict): - _table_rows = _table.get(row_name) + _table_rows = _table.get(row_name) # type: ignore if not isinstance(_table_rows, list): _table_rows = [_table_rows] return _table_rows @@ -767,20 +799,27 @@ def open(self) -> None: def close(self) -> None: self.device = None - def _send_command( - self, command: str, raw_text: bool = False - ) -> Dict[str, Union[str, Dict[str, Any]]]: + def _send_command(self, command: str, raw_text: bool = False) -> Any: """ Wrapper for NX-API show method. Allows more code sharing between NX-API and SSH. """ + assert ( + self.device is not None + ), "Call open() or use as a context manager before calling _send_command" return self.device.show(command, raw_text=raw_text) - def _send_command_list(self, commands: List[str]) -> List[Any]: + def _send_command_list(self, commands: List[str]) -> List[Any]: + assert ( + self.device is not None + ), "Call open() or use as a context manager before running _send_command_list" return self.device.config_list(commands) def _send_config(self, commands: Union[str, List]) -> List[str]: + assert ( + self.device is not None + ), "Call open() or use as a context manager before running _send_config" if isinstance(commands, str): # Has to be a list generator and not generator expression (not JSON serializable) commands = [command for command in commands.splitlines() if command] @@ -823,9 +862,7 @@ def _compute_timestamp(stupid_cisco_output: str) -> float: int, part.replace(key, ""), 0 ) - delta = sum( - [det.get("count", 0) * det.get("weight") for det in things.values()] - ) + delta = sum([det.get("count", 0) * det["weight"] for det in things.values()]) return time.time() - delta def is_alive(self) -> models.AliveDict: @@ -835,6 +872,9 @@ def is_alive(self) -> models.AliveDict: return {"is_alive": False} def _copy_run_start(self) -> None: + assert ( + self.device is not None + ), "Call open() or use as a context manager before calling _copy_run_start" results = self.device.save(filename="startup-config") if not results: msg = "Unable to save running-config to startup-config!" @@ -878,7 +918,7 @@ def rollback(self) -> None: self.changed = False def get_facts(self) -> models.FactsDict: - facts: models.FactsDict = {} + facts: models.FactsDict = {} # type: ignore facts["vendor"] = "Cisco" show_inventory_table = self._get_command_table( @@ -887,7 +927,7 @@ def get_facts(self) -> models.FactsDict: if isinstance(show_inventory_table, dict): show_inventory_table = [show_inventory_table] - facts["serial_number"] = None + facts["serial_number"] = None # type: ignore for row in show_inventory_table: if row["name"] == '"Chassis"' or row["name"] == "Chassis": @@ -935,7 +975,9 @@ def get_interfaces(self) -> Dict[str, models.InterfaceDict]: interfaces_body = interfaces_out["TABLE_interface"]["ROW_interface"] for interface_details in interfaces_body: + assert isinstance(interface_details, dict) interface_name = interface_details.get("interface") + assert isinstance(interface_name, str) if interface_details.get("eth_mtu"): interface_mtu = int(interface_details["eth_mtu"]) @@ -980,7 +1022,7 @@ def get_interfaces(self) -> Dict[str, models.InterfaceDict]: "speed": interface_speed, "mtu": interface_mtu, "mac_address": napalm.base.helpers.convert( - napalm.base.helpers.mac, mac_address + napalm.base.helpers.mac, mac_address, "" ), } return interfaces @@ -1184,8 +1226,12 @@ def get_interfaces_ip(self) -> Dict[str, models.InterfacesIPDict]: ipv4_command, "TABLE_intf", "ROW_intf" ) + # Cast the table to the proper type + cast(ShowIPInterfaceReturn, ipv4_interf_table_vrf) + for interface in ipv4_interf_table_vrf: interface_name = str(interface.get("intf-name", "")) + assert isinstance(interface_name, str) addr_str = interface.get("prefix") unnumbered = str(interface.get("unnum-intf", "")) if addr_str: @@ -1195,7 +1241,7 @@ def get_interfaces_ip(self) -> Dict[str, models.InterfacesIPDict]: interfaces_ip[interface_name] = {} if "ipv4" not in interfaces_ip[interface_name].keys(): interfaces_ip[interface_name]["ipv4"] = {} - if address not in interfaces_ip[interface_name].get("ipv4"): + if address not in interfaces_ip[interface_name]["ipv4"]: interfaces_ip[interface_name]["ipv4"][address] = {} interfaces_ip[interface_name]["ipv4"][address].update( {"prefix_length": prefix} @@ -1210,7 +1256,7 @@ def get_interfaces_ip(self) -> Dict[str, models.InterfacesIPDict]: interfaces_ip[interface_name] = {} if "ipv4" not in interfaces_ip[interface_name].keys(): interfaces_ip[interface_name]["ipv4"] = {} - if address not in interfaces_ip[interface_name].get("ipv4"): + if address not in interfaces_ip[interface_name]["ipv4"]: interfaces_ip[interface_name]["ipv4"][address] = {} interfaces_ip[interface_name]["ipv4"][address].update( {"prefix_length": prefix} @@ -1228,9 +1274,7 @@ def get_interfaces_ip(self) -> Dict[str, models.InterfacesIPDict]: secondary_address_prefix = int(secondary_address.get("masklen1", "")) if "ipv4" not in interfaces_ip[interface_name].keys(): interfaces_ip[interface_name]["ipv4"] = {} - if secondary_address_ip not in interfaces_ip[interface_name].get( - "ipv4" - ): + if secondary_address_ip not in interfaces_ip[interface_name]["ipv4"]: interfaces_ip[interface_name]["ipv4"][secondary_address_ip] = {} interfaces_ip[interface_name]["ipv4"][secondary_address_ip].update( {"prefix_length": secondary_address_prefix} @@ -1267,7 +1311,7 @@ def get_interfaces_ip(self) -> Dict[str, models.InterfacesIPDict]: for ipv6_address in interface.get("addr", ""): address = napalm.base.helpers.ip(ipv6_address.split("/")[0]) prefix = int(ipv6_address.split("/")[-1]) - if address not in interfaces_ip[interface_name].get("ipv6"): + if address not in interfaces_ip[interface_name]["ipv6"]: interfaces_ip[interface_name]["ipv6"][address] = {} interfaces_ip[interface_name]["ipv6"][address].update( {"prefix_length": prefix} @@ -1282,7 +1326,7 @@ def get_interfaces_ip(self) -> Dict[str, models.InterfacesIPDict]: else: prefix = 128 - if address not in interfaces_ip[interface_name].get("ipv6"): + if address not in interfaces_ip[interface_name]["ipv6"]: interfaces_ip[interface_name]["ipv6"][address] = {} interfaces_ip[interface_name]["ipv6"][address].update( {"prefix_length": prefix} @@ -1321,9 +1365,10 @@ def get_mac_address_table(self) -> List[models.MACAdressTable]: return mac_table def get_snmp_information(self) -> models.SNMPDict: - snmp_information = {} + snmp_information: models.SNMPDict = {} # type: ignore snmp_command = "show running-config" snmp_raw_output = self.cli([snmp_command]).get(snmp_command, "") + assert isinstance(snmp_raw_output, str) snmp_config = napalm.base.helpers.textfsm_extractor( self, "snmp_config", snmp_raw_output ) @@ -1376,6 +1421,7 @@ def get_users(self) -> Dict[str, models.UsersDict]: users: Dict[str, models.UsersDict] = {} command = "show running-config" section_username_raw_output = self.cli([command]).get(command, "") + assert isinstance(section_username_raw_output, str) section_username_tabled_output = napalm.base.helpers.textfsm_extractor( self, "users", section_username_raw_output ) @@ -1462,7 +1508,9 @@ def get_network_instances(self, name: str = "") -> Dict[str, models.NetworkInsta def get_environment(self) -> models.EnvironmentDict: def _process_pdus(power_data: Dict) -> Dict[str, models.PowerDict]: - normalized: Dict[str, models.PowerDict] = defaultdict(dict) + # defaultdict(dict) doesn't yet seem to work with Mypy + # https://github.com/python/mypy/issues/7217 + normalized: DefaultDict[str, models.PowerDict] = defaultdict(dict) # type: ignore # some nexus devices have keys postfixed with the shorthand device series name (ie n3k) # ex. on a 9k, the key is TABLE_psinfo, but on a 3k it is TABLE_psinfo_n3k ps_info_key = [ @@ -1516,7 +1564,7 @@ def _process_pdus(power_data: Dict) -> Dict[str, models.PowerDict]: return json.loads(json.dumps(normalized)) def _process_fans(fan_data: Dict) -> Dict[str, models.FanDict]: - normalized = {} + normalized: Dict[str, models.FanDict] = {} for entry in fan_data["TABLE_faninfo"]["ROW_faninfo"]: if "PS" in entry["fanname"]: # Skip fans in power supplies @@ -1525,14 +1573,14 @@ def _process_fans(fan_data: Dict) -> Dict[str, models.FanDict]: # Copying the behavior of eos.py where if the fanstatus key is not found # we default the status to True "status": entry.get("fanstatus", "Ok") - == "Ok" + == "Ok" } return normalized def _process_temperature( temperature_data: Dict, ) -> Dict[str, models.TemperatureDict]: - normalized = {} + normalized: Dict[str, models.TemperatureDict] = {} # The modname and sensor type are not unique enough keys, so adding a count count = 1 past_tempmod = "1" @@ -1546,7 +1594,7 @@ def _process_temperature( "temperature": float(entry.get("curtemp", -1)), "is_alert": entry.get("alarmstatus", "Ok").rstrip() != "Ok", "is_critical": float(entry.get("curtemp")) - > float(entry.get("majthres")), + > float(entry.get("majthres")), } count += 1 return normalized @@ -1557,6 +1605,7 @@ def _process_cpu(cpu_data: Dict) -> Dict[int, models.CPUDict]: if cpu_data.get("idle_percent") else cpu_data["TABLE_cpu_util"]["ROW_cpu_util"]["idle_percent"] ) + assert isinstance(idle, str) return {0: {"%usage": round(100 - float(idle), 2)}} def _process_memory(memory_data: Dict) -> models.MemoryDict: diff --git a/test/base/test_helpers.py b/test/base/test_helpers.py index 5c9ffe796..5b6d0b150 100644 --- a/test/base/test_helpers.py +++ b/test/base/test_helpers.py @@ -240,11 +240,11 @@ def test_convert(self): napalm.base.helpers.convert(int, "non-int-value", default=-100) == -100 ) # default value returned - self.assertIsInstance(napalm.base.helpers.convert(float, "1e-17"), float) + self.assertIsInstance(napalm.base.helpers.convert(float, "1e-17", 1.0), float) # converts indeed to float - self.assertFalse(napalm.base.helpers.convert(str, None) == "None") + self.assertFalse(napalm.base.helpers.convert(str, None, "") == "None") # should not convert None-type to 'None' string - self.assertTrue(napalm.base.helpers.convert(str, None) == "") + self.assertTrue(napalm.base.helpers.convert(str, None, "") == "") # should return empty unicode def test_find_txt(self): From aca0ebbf78e93b343b19000627ec1604a7bdf647 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 27 Mar 2021 17:51:33 +0100 Subject: [PATCH 22/74] Add type checker to github actions --- .github/workflows/commit.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/commit.yaml b/.github/workflows/commit.yaml index 81f3e6a27..e276d438e 100644 --- a/.github/workflows/commit.yaml +++ b/.github/workflows/commit.yaml @@ -1,4 +1,4 @@ ---- +--- name: build on: [push, pull_request] jobs: @@ -33,10 +33,14 @@ jobs: run: | pylama . + - name: Run type checker + run: | + mypy -p napalm || true + - name: Run Tests run: | py.test --cov=napalm --cov-report term-missing -vs --pylama - + build_docs: needs: std_tests runs-on: ubuntu-latest From 12f9da7989de727fdd8f00266bb7a82972d1d673 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 27 Mar 2021 17:56:54 +0100 Subject: [PATCH 23/74] Specify mypy config file in github action --- .github/workflows/commit.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/commit.yaml b/.github/workflows/commit.yaml index e276d438e..3bfd305c8 100644 --- a/.github/workflows/commit.yaml +++ b/.github/workflows/commit.yaml @@ -35,7 +35,7 @@ jobs: - name: Run type checker run: | - mypy -p napalm || true + mypy -p napalm --config-file mypy.ini || true - name: Run Tests run: | From d8eaed72d162f908c89519f8c2bc3eafeedecf7b Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 27 Mar 2021 18:01:41 +0100 Subject: [PATCH 24/74] Fix unused imports --- napalm/base/base.py | 2 -- napalm/base/helpers.py | 13 ++++++------- napalm/base/validate.py | 4 ++-- napalm/nxapi_plumbing/api_client.py | 2 +- napalm/nxapi_plumbing/device.py | 2 +- napalm/nxos/nxos.py | 2 +- 6 files changed, 11 insertions(+), 14 deletions(-) diff --git a/napalm/base/base.py b/napalm/base/base.py index 37f9d631b..07e5efde9 100644 --- a/napalm/base/base.py +++ b/napalm/base/base.py @@ -12,8 +12,6 @@ # License for the specific language governing permissions and limitations under # the License. -from __future__ import annotations - import sys from types import TracebackType from typing import Optional, Dict, Type, Any, Literal, List, Union diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index 59d60b584..233d49dd1 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -1,29 +1,28 @@ """Helper functions for the NAPALM base.""" +import itertools +import logging # std libs import os import re import sys -import itertools -import logging from collections.abc import Iterable - # third party libs -from typing import Optional, Dict, Any, List, Union, Tuple, TypeVar, Callable, Type +from typing import Optional, Dict, Any, List, Union, Tuple, TypeVar, Callable import jinja2 import textfsm +from ciscoconfparse import CiscoConfParse from lxml import etree from netaddr import EUI -from netaddr import mac_unix from netaddr import IPAddress -from ciscoconfparse import CiscoConfParse +from netaddr import mac_unix # local modules import napalm.base.exceptions from napalm.base import constants +from napalm.base.canonical_map import base_interfaces, reverse_mapping from napalm.base.test.models import ConfigDict from napalm.base.utils.jinja_filters import CustomJinjaFilters -from napalm.base.canonical_map import base_interfaces, reverse_mapping T = TypeVar("T") R = TypeVar("R") diff --git a/napalm/base/validate.py b/napalm/base/validate.py index 4eb269cec..76b1bdaff 100644 --- a/napalm/base/validate.py +++ b/napalm/base/validate.py @@ -5,11 +5,11 @@ """ from __future__ import annotations +import copy +import re from typing import Dict, List, Union, TypeVar, Optional, TYPE_CHECKING import yaml -import copy -import re if TYPE_CHECKING: from napalm.base import NetworkDriver diff --git a/napalm/nxapi_plumbing/api_client.py b/napalm/nxapi_plumbing/api_client.py index f9f9e3977..0efc3fee4 100644 --- a/napalm/nxapi_plumbing/api_client.py +++ b/napalm/nxapi_plumbing/api_client.py @@ -6,7 +6,7 @@ from __future__ import print_function, unicode_literals from builtins import super -from typing import Optional, List, Dict, Union, Any +from typing import Optional, List, Dict, Any import requests from requests.auth import HTTPBasicAuth diff --git a/napalm/nxapi_plumbing/device.py b/napalm/nxapi_plumbing/device.py index fb9fc6088..3f2ac5f5b 100644 --- a/napalm/nxapi_plumbing/device.py +++ b/napalm/nxapi_plumbing/device.py @@ -6,7 +6,7 @@ from __future__ import print_function, unicode_literals -from typing import List, Optional, Dict, Any, Union +from typing import List, Optional, Any, Union from napalm.nxapi_plumbing.errors import NXAPIError, NXAPICommandError from napalm.nxapi_plumbing.api_client import RPCClient, XMLClient, RPCBase diff --git a/napalm/nxos/nxos.py b/napalm/nxos/nxos.py index 2ece388dc..67c1dc586 100644 --- a/napalm/nxos/nxos.py +++ b/napalm/nxos/nxos.py @@ -41,7 +41,7 @@ from netaddr import IPAddress from netaddr.core import AddrFormatError -from netmiko import file_transfer, ConnectHandler +from netmiko import file_transfer from requests.exceptions import ConnectionError import napalm.base.constants as c From 8b8986bc92992231b48f6ad2f7bcda05c57b6064 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 27 Mar 2021 18:11:24 +0100 Subject: [PATCH 25/74] Black formatting --- napalm/base/helpers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index 233d49dd1..90f570edb 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -1,11 +1,13 @@ """Helper functions for the NAPALM base.""" import itertools import logging + # std libs import os import re import sys from collections.abc import Iterable + # third party libs from typing import Optional, Dict, Any, List, Union, Tuple, TypeVar, Callable From 80f9a9893b3c44f03bbd1fdeb2bde3b3882cf7f3 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 27 Mar 2021 18:14:54 +0100 Subject: [PATCH 26/74] Fix further pipeline problems --- .github/workflows/commit.yaml | 2 +- napalm/base/base.py | 7 ++++++- napalm/base/validate.py | 2 -- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/commit.yaml b/.github/workflows/commit.yaml index 3bfd305c8..03741ece4 100644 --- a/.github/workflows/commit.yaml +++ b/.github/workflows/commit.yaml @@ -35,7 +35,7 @@ jobs: - name: Run type checker run: | - mypy -p napalm --config-file mypy.ini || true + mypy -p napalm --config-file mypy.ini - name: Run Tests run: | diff --git a/napalm/base/base.py b/napalm/base/base.py index 07e5efde9..a00b4ca7e 100644 --- a/napalm/base/base.py +++ b/napalm/base/base.py @@ -14,7 +14,12 @@ import sys from types import TracebackType -from typing import Optional, Dict, Type, Any, Literal, List, Union +from typing import Optional, Dict, Type, Any, List, Union + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal from netmiko import ConnectHandler, NetMikoTimeoutException diff --git a/napalm/base/validate.py b/napalm/base/validate.py index 76b1bdaff..5222e3aa1 100644 --- a/napalm/base/validate.py +++ b/napalm/base/validate.py @@ -3,8 +3,6 @@ See: https://napalm.readthedocs.io/en/latest/validate.html """ -from __future__ import annotations - import copy import re from typing import Dict, List, Union, TypeVar, Optional, TYPE_CHECKING From 18907d33a1124043bcfc217b01827cb08db5d1bc Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 27 Mar 2021 18:17:47 +0100 Subject: [PATCH 27/74] Fix further pipeline errors --- napalm/base/base.py | 2 +- napalm/base/helpers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/napalm/base/base.py b/napalm/base/base.py index a00b4ca7e..c88414e78 100644 --- a/napalm/base/base.py +++ b/napalm/base/base.py @@ -19,7 +19,7 @@ try: from typing import Literal except ImportError: - from typing_extensions import Literal + from typing_extensions import Literal # type: ignore from netmiko import ConnectHandler, NetMikoTimeoutException diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index 90f570edb..311ff1081 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -303,7 +303,7 @@ def find_txt( return str(value) -def convert(to: Callable[[T], R], who: Optional[T], default: R = "") -> R: +def convert(to: Callable[[T], R], who: Optional[T], default: R = "") -> R: # type: ignore """ Converts data to a specific datatype. In case of error, will return a default value. From 4ee35ba002a1f226f45ca32d18b9e2868e6717c1 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 27 Mar 2021 18:21:31 +0100 Subject: [PATCH 28/74] Put type in quotes --- napalm/base/validate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/napalm/base/validate.py b/napalm/base/validate.py index 5222e3aa1..1e2b529a7 100644 --- a/napalm/base/validate.py +++ b/napalm/base/validate.py @@ -224,7 +224,7 @@ def empty_tree(input_list: List) -> bool: def compliance_report( - cls: NetworkDriver, + cls: "NetworkDriver", validation_file: Optional[str] = None, validation_source: Optional[str] = None, ) -> models.ReportResult: From 97e0cae6b53ef07d999b3e9d77749a25f5880102 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 27 Mar 2021 18:24:53 +0100 Subject: [PATCH 29/74] Convert all typing_extensions try/except imports to pure typing_extensions imports --- napalm/base/base.py | 5 +---- napalm/base/test/models.py | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/napalm/base/base.py b/napalm/base/base.py index c88414e78..dcb08b146 100644 --- a/napalm/base/base.py +++ b/napalm/base/base.py @@ -16,10 +16,7 @@ from types import TracebackType from typing import Optional, Dict, Type, Any, List, Union -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal # type: ignore +from typing_extensions import Literal from netmiko import ConnectHandler, NetMikoTimeoutException diff --git a/napalm/base/test/models.py b/napalm/base/test/models.py index e3473e12b..a724ff050 100644 --- a/napalm/base/test/models.py +++ b/napalm/base/test/models.py @@ -1,9 +1,6 @@ from typing import Dict, List -try: - from typing import TypedDict -except ImportError: - from typing_extensions import TypedDict +from typing_extensions import TypedDict ConfigurationDict = TypedDict( "ConfigurationDict", {"running": str, "candidate": str, "startup": str} From e373e033e28d981ed366e3c1586cccebdb244574 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 27 Mar 2021 18:28:07 +0100 Subject: [PATCH 30/74] Import TypedDict from typing_extensions in nxos.py --- napalm/nxos/nxos.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/napalm/nxos/nxos.py b/napalm/nxos/nxos.py index 67c1dc586..6b8d5fec1 100644 --- a/napalm/nxos/nxos.py +++ b/napalm/nxos/nxos.py @@ -35,10 +35,11 @@ cast, Callable, TypeVar, - TypedDict, DefaultDict, ) +from typing_extensions import TypedDict + from netaddr import IPAddress from netaddr.core import AddrFormatError from netmiko import file_transfer From 72c3e921fa0a2892b194870211c80c2e3e15243d Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Fri, 30 Apr 2021 23:44:47 +0200 Subject: [PATCH 31/74] Ignore typing errors from napalm.iosxr_netconf --- mypy.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mypy.ini b/mypy.ini index 8a0948964..9819a29a8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -14,6 +14,9 @@ ignore_errors = True [mypy-napalm.ios.*] ignore_errors = True +[mypy-napalm.iosxr_netconf.*] +ignore_errors = True + [mypy-napalm.nxapi_plumbing.*] disallow_untyped_defs = True From 5ced7a3018d23942e8a2aaa11576e3bec214c9f1 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 1 May 2021 13:42:16 +0200 Subject: [PATCH 32/74] Fix Callable import in mock.py --- napalm/base/mock.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/napalm/base/mock.py b/napalm/base/mock.py index 64074e6ac..91405cf53 100644 --- a/napalm/base/mock.py +++ b/napalm/base/mock.py @@ -11,8 +11,7 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under # the License. -from collections import Callable -from typing import Optional, List, Dict, Union, Any +from typing import Optional, List, Dict, Union, Any, Callable from napalm.base.base import NetworkDriver import napalm.base.exceptions From d52a2cb1df5dd750507d0886d5809022f3c4d697 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Mon, 22 Mar 2021 15:12:46 +0100 Subject: [PATCH 33/74] Add models and annotate NetworkDriver class with types --- napalm/base/base.py | 9 +++-- napalm/base/test/base.py | 4 ++- napalm/base/test/models.py | 70 +++++++++++++++++++++++++++++++++----- 3 files changed, 71 insertions(+), 12 deletions(-) diff --git a/napalm/base/base.py b/napalm/base/base.py index dcb08b146..3e5c69647 100644 --- a/napalm/base/base.py +++ b/napalm/base/base.py @@ -12,6 +12,8 @@ # License for the specific language governing permissions and limitations under # the License. +from __future__ import annotations + import sys from types import TracebackType from typing import Optional, Dict, Type, Any, List, Union @@ -61,7 +63,7 @@ def __init__( """ raise NotImplementedError - def __enter__(self) -> "NetworkDriver": # type: ignore + def __enter__(self) -> "NetworkDriver": try: self.open() return self @@ -73,6 +75,7 @@ def __enter__(self) -> "NetworkDriver": # type: ignore raise def __exit__( # type: ignore + def __exit__( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], @@ -1156,7 +1159,7 @@ def get_route_to( """ raise NotImplementedError - def get_snmp_information(self) -> models.SNMPDict: + def get_snmp_information(self) -> Dict[str, models.SNMPDict]: """ Returns a dict of dicts containing SNMP configuration. @@ -1316,7 +1319,7 @@ def ping( size: int = c.PING_SIZE, count: int = c.PING_COUNT, vrf: str = c.PING_VRF, - source_interface: str=c.PING_SOURCE_INTERFACE, + source_interface: str = c.PING_SOURCE_INTERFACE ) -> models.PingResultDict: """ Executes ping on the device and returns a dictionary with the result diff --git a/napalm/base/test/base.py b/napalm/base/test/base.py index f005101ca..69a372aac 100644 --- a/napalm/base/test/base.py +++ b/napalm/base/test/base.py @@ -442,7 +442,9 @@ def test_get_mac_address_table(self): result = len(get_mac_address_table) > 0 for mac_table_entry in get_mac_address_table: - result = result and self._test_model(models.MACAdressTable, mac_table_entry) + result = result and self._test_model( + models.MACAdressTable, mac_table_entry + ) self.assertTrue(result) diff --git a/napalm/base/test/models.py b/napalm/base/test/models.py index a724ff050..894b84a03 100644 --- a/napalm/base/test/models.py +++ b/napalm/base/test/models.py @@ -1,6 +1,9 @@ -from typing import Dict, List +from typing import Dict, Literal, Union, List, Optional -from typing_extensions import TypedDict +try: + from typing import TypedDict +except ImportError: + from typing_extensions import TypedDict ConfigurationDict = TypedDict( "ConfigurationDict", {"running": str, "candidate": str, "startup": str} @@ -94,6 +97,23 @@ }, ) +MemoryDict = TypedDict("MemoryDict", {"used_ram": int, "available_ram": int}) + +FanDict = TypedDict("FanDict", {"status": bool}) + +CPUDict = TypedDict("CPUDict", {"%usage": float}) + +EnvironmentDict = TypedDict( + "EnvironmentDict", + { + "fans": Dict[str, FanDict], + "temperature": Dict[str, TemperatureDict], + "power": Dict[str, PowerDict], + "cpu": Dict[str, CPUDict], + "memory": Dict[str, MemoryDict], + }, +) + PeerDict = TypedDict( "PeerDict", { @@ -336,9 +356,7 @@ }, ) -PingResultDictEntry = TypedDict( - "PingResultDictEntry", {"ip_address": str, "rtt": float} -) +PingResultDictEntry = TypedDict("PingResultDictEntry", {"ip_address": str, "rtt": float}) PingDict = TypedDict( "PingDict", @@ -354,9 +372,7 @@ ) PingResultDict = TypedDict( - "PingResultDict", - {"success": PingDict, "error": str}, - total=False, + "PingResultDict", {"success": Optional[PingDict], "error": Optional[str]} ) TracerouteDict = TypedDict( @@ -400,6 +416,44 @@ ConfigDict = TypedDict("ConfigDict", {"running": str, "startup": str, "candidate": str}) +======= +TracerouteResultDictEntry = TypedDict("TracerouteResultDictEntry", {"probes": Dict[int, TracerouteDict]}) + +TracerouteResultDict = TypedDict( + "TracerouteResultDict", + {"success": Optional[Dict[int, TracerouteResultDictEntry]], "error": Optional[str]}, +) + +UsersDict = TypedDict("UsersDict", {"level": int, "password": str, "sshkeys": List}) + +OpticsStateDict = TypedDict( + "OpticsStateDict", {"instant": float, "avg": float, "min": float, "max": float} +) + +OpticsStatePerChannelDict = TypedDict("OpticsStatePerChannelDict", { + "input_power": OpticsStateDict, + "output_power": OpticsStateDict, + "laser_bias_current": OpticsStateDict +}) + +OpticsPerChannelDict = TypedDict("OpticsPerChannelDict", { + "index": int, + "state": OpticsStatePerChannelDict +}) + +OpticsPhysicalChannelsDict = TypedDict("OpticsPhysicalChannelsDict", { + "channels": OpticsPerChannelDict +}) + +OpticsDict = TypedDict( + "OpticsDict", + { + "physical_channels": OpticsPhysicalChannelsDict + } +) + +ConfigDict = TypedDict("ConfigDict", {"running": str, "startup": str, "candidate": str}) + NetworkInstanceDict = TypedDict( "NetworkInstanceDict", {"name": str, "type": str, "state": dict, "interfaces": dict} ) From 066da2d12cd462d118e363c000fac957a2b7b126 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Mon, 22 Mar 2021 17:24:41 +0100 Subject: [PATCH 34/74] Make tests pass by ignoring the annotations part of the arg spec --- napalm/base/test/base.py | 4 +--- napalm/base/test/models.py | 42 +++++++++++++++++++------------------- napalm/nxos/nxos.py | 29 +++++++++++--------------- 3 files changed, 34 insertions(+), 41 deletions(-) diff --git a/napalm/base/test/base.py b/napalm/base/test/base.py index 69a372aac..f005101ca 100644 --- a/napalm/base/test/base.py +++ b/napalm/base/test/base.py @@ -442,9 +442,7 @@ def test_get_mac_address_table(self): result = len(get_mac_address_table) > 0 for mac_table_entry in get_mac_address_table: - result = result and self._test_model( - models.MACAdressTable, mac_table_entry - ) + result = result and self._test_model(models.MACAdressTable, mac_table_entry) self.assertTrue(result) diff --git a/napalm/base/test/models.py b/napalm/base/test/models.py index 894b84a03..796cf420e 100644 --- a/napalm/base/test/models.py +++ b/napalm/base/test/models.py @@ -356,7 +356,9 @@ }, ) -PingResultDictEntry = TypedDict("PingResultDictEntry", {"ip_address": str, "rtt": float}) +PingResultDictEntry = TypedDict( + "PingResultDictEntry", {"ip_address": str, "rtt": float} +) PingDict = TypedDict( "PingDict", @@ -372,7 +374,7 @@ ) PingResultDict = TypedDict( - "PingResultDict", {"success": Optional[PingDict], "error": Optional[str]} + "PingResultDict", {"success": Optional[PingDict], "error": Optional[str]}, total=False ) TracerouteDict = TypedDict( @@ -380,6 +382,7 @@ ) TracerouteResultDictEntry = TypedDict( +<<<<<<< HEAD "TracerouteResultDictEntry", {"probes": Dict[int, TracerouteDict]}, total=False ) @@ -416,12 +419,12 @@ ConfigDict = TypedDict("ConfigDict", {"running": str, "startup": str, "candidate": str}) -======= TracerouteResultDictEntry = TypedDict("TracerouteResultDictEntry", {"probes": Dict[int, TracerouteDict]}) TracerouteResultDict = TypedDict( "TracerouteResultDict", {"success": Optional[Dict[int, TracerouteResultDictEntry]], "error": Optional[str]}, + total=False ) UsersDict = TypedDict("UsersDict", {"level": int, "password": str, "sshkeys": List}) @@ -430,28 +433,25 @@ "OpticsStateDict", {"instant": float, "avg": float, "min": float, "max": float} ) -OpticsStatePerChannelDict = TypedDict("OpticsStatePerChannelDict", { - "input_power": OpticsStateDict, - "output_power": OpticsStateDict, - "laser_bias_current": OpticsStateDict -}) - -OpticsPerChannelDict = TypedDict("OpticsPerChannelDict", { - "index": int, - "state": OpticsStatePerChannelDict -}) +OpticsStatePerChannelDict = TypedDict( + "OpticsStatePerChannelDict", + { + "input_power": OpticsStateDict, + "output_power": OpticsStateDict, + "laser_bias_current": OpticsStateDict, + }, +) -OpticsPhysicalChannelsDict = TypedDict("OpticsPhysicalChannelsDict", { - "channels": OpticsPerChannelDict -}) +OpticsPerChannelDict = TypedDict( + "OpticsPerChannelDict", {"index": int, "state": OpticsStatePerChannelDict} +) -OpticsDict = TypedDict( - "OpticsDict", - { - "physical_channels": OpticsPhysicalChannelsDict - } +OpticsPhysicalChannelsDict = TypedDict( + "OpticsPhysicalChannelsDict", {"channels": OpticsPerChannelDict} ) +OpticsDict = TypedDict("OpticsDict", {"physical_channels": OpticsPhysicalChannelsDict}) + ConfigDict = TypedDict("ConfigDict", {"running": str, "startup": str, "candidate": str}) NetworkInstanceDict = TypedDict( diff --git a/napalm/nxos/nxos.py b/napalm/nxos/nxos.py index 6b8d5fec1..099ecd311 100644 --- a/napalm/nxos/nxos.py +++ b/napalm/nxos/nxos.py @@ -26,26 +26,21 @@ from collections import defaultdict # import third party lib -from typing import ( - Optional, - Dict, - List, - Union, - Any, - cast, - Callable, - TypeVar, - DefaultDict, -) - -from typing_extensions import TypedDict +from typing import Optional, Dict, List, Union, Any +from requests.exceptions import ConnectionError from netaddr import IPAddress from netaddr.core import AddrFormatError from netmiko import file_transfer -from requests.exceptions import ConnectionError -import napalm.base.constants as c +from napalm.base.test import models +from napalm.nxapi_plumbing import Device as NXOSDevice +from napalm.nxapi_plumbing import ( + NXAPIAuthError, + NXAPIConnectionError, + NXAPICommandError, +) +import json # import NAPALM Base import napalm.base.helpers @@ -321,7 +316,7 @@ def ping( size: int = c.PING_SIZE, count: int = c.PING_COUNT, vrf: str = c.PING_VRF, - source_interface: str =c.PING_SOURCE_INTERFACE, + source_interface: str = c.PING_SOURCE_INTERFACE, ) -> models.PingResultDict: """ Execute ping on the device and returns a dictionary with the result. @@ -569,7 +564,7 @@ def _create_tmp_file(config: str) -> str: fobj.write(config) return filename - def _disable_confirmation(self) -> None: + def _disable_confirmation(self) -> str: self._send_command_list(["terminal dont-ask"]) def get_config( From 67aef9646d14340c1bb92cb25bd14e3ccc9f3101 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Mon, 22 Mar 2021 22:00:52 +0100 Subject: [PATCH 35/74] Add lots of type hints, asserts and some abstract methods. Enable type checking for nxapi_plumbing --- mypy.ini | 3 +++ napalm/base/base.py | 2 +- napalm/base/test/models.py | 26 ++++-------------- napalm/nxapi_plumbing/api_client.py | 3 ++- napalm/nxapi_plumbing/device.py | 1 + napalm/nxos/nxos.py | 42 +++++++++++++---------------- 6 files changed, 31 insertions(+), 46 deletions(-) diff --git a/mypy.ini b/mypy.ini index 9819a29a8..cc98b8d84 100644 --- a/mypy.ini +++ b/mypy.ini @@ -23,6 +23,9 @@ disallow_untyped_defs = True [mypy-napalm.base.clitools.*] ignore_errors = True +[mypy-napalm.nxapi_plumbing.*] +disallow_untyped_defs = True + [mypy-napalm.base.test.*] ignore_errors = True diff --git a/napalm/base/base.py b/napalm/base/base.py index 3e5c69647..e8ced5053 100644 --- a/napalm/base/base.py +++ b/napalm/base/base.py @@ -1159,7 +1159,7 @@ def get_route_to( """ raise NotImplementedError - def get_snmp_information(self) -> Dict[str, models.SNMPDict]: + def get_snmp_information(self) -> models.SNMPDict: """ Returns a dict of dicts containing SNMP configuration. diff --git a/napalm/base/test/models.py b/napalm/base/test/models.py index 796cf420e..2e060eea4 100644 --- a/napalm/base/test/models.py +++ b/napalm/base/test/models.py @@ -86,23 +86,6 @@ CPUDict = TypedDict("CPUDict", {"%usage": float}) -EnvironmentDict = TypedDict( - "EnvironmentDict", - { - "fans": Dict[str, FanDict], - "temperature": Dict[str, TemperatureDict], - "power": Dict[str, PowerDict], - "cpu": Dict[int, CPUDict], - "memory": MemoryDict, - }, -) - -MemoryDict = TypedDict("MemoryDict", {"used_ram": int, "available_ram": int}) - -FanDict = TypedDict("FanDict", {"status": bool}) - -CPUDict = TypedDict("CPUDict", {"%usage": float}) - EnvironmentDict = TypedDict( "EnvironmentDict", { @@ -374,7 +357,9 @@ ) PingResultDict = TypedDict( - "PingResultDict", {"success": Optional[PingDict], "error": Optional[str]}, total=False + "PingResultDict", + {"success": PingDict, "error": str}, + total=False, ) TracerouteDict = TypedDict( @@ -382,7 +367,6 @@ ) TracerouteResultDictEntry = TypedDict( -<<<<<<< HEAD "TracerouteResultDictEntry", {"probes": Dict[int, TracerouteDict]}, total=False ) @@ -423,8 +407,8 @@ TracerouteResultDict = TypedDict( "TracerouteResultDict", - {"success": Optional[Dict[int, TracerouteResultDictEntry]], "error": Optional[str]}, - total=False + {"success": Dict[int, TracerouteResultDictEntry], "error": str}, + total=False, ) UsersDict = TypedDict("UsersDict", {"level": int, "password": str, "sshkeys": List}) diff --git a/napalm/nxapi_plumbing/api_client.py b/napalm/nxapi_plumbing/api_client.py index 0efc3fee4..fc876010b 100644 --- a/napalm/nxapi_plumbing/api_client.py +++ b/napalm/nxapi_plumbing/api_client.py @@ -6,7 +6,7 @@ from __future__ import print_function, unicode_literals from builtins import super -from typing import Optional, List, Dict, Any +from typing import Optional, List, Dict, Union, Any import requests from requests.auth import HTTPBasicAuth @@ -138,6 +138,7 @@ def _nxapi_command( if isinstance(commands, string_types): commands = [commands] + raw_text = True if method == "cli_ascii" else False response = self._send_request(commands, method=method) diff --git a/napalm/nxapi_plumbing/device.py b/napalm/nxapi_plumbing/device.py index 3f2ac5f5b..b89434338 100644 --- a/napalm/nxapi_plumbing/device.py +++ b/napalm/nxapi_plumbing/device.py @@ -7,6 +7,7 @@ from __future__ import print_function, unicode_literals from typing import List, Optional, Any, Union +from typing import List, Optional, Dict, Any, Union from napalm.nxapi_plumbing.errors import NXAPIError, NXAPICommandError from napalm.nxapi_plumbing.api_client import RPCClient, XMLClient, RPCBase diff --git a/napalm/nxos/nxos.py b/napalm/nxos/nxos.py index 099ecd311..3b56c6476 100644 --- a/napalm/nxos/nxos.py +++ b/napalm/nxos/nxos.py @@ -19,29 +19,19 @@ import tempfile import time import uuid - # import stdlib from abc import abstractmethod from builtins import super from collections import defaultdict - # import third party lib -from typing import Optional, Dict, List, Union, Any +from typing import Optional, Dict, List, Union, Any, cast, Callable, TypeVar -from requests.exceptions import ConnectionError from netaddr import IPAddress from netaddr.core import AddrFormatError -from netmiko import file_transfer - -from napalm.base.test import models -from napalm.nxapi_plumbing import Device as NXOSDevice -from napalm.nxapi_plumbing import ( - NXAPIAuthError, - NXAPIConnectionError, - NXAPICommandError, -) -import json +from netmiko import file_transfer, ConnectHandler +from requests.exceptions import ConnectionError +import napalm.base.constants as c # import NAPALM Base import napalm.base.helpers from napalm.base import NetworkDriver @@ -71,11 +61,13 @@ }, ) +F = TypeVar('F', bound=Callable[..., Any]) + def ensure_netmiko_conn(func: F) -> F: """Decorator that ensures Netmiko connection exists.""" - def wrap_function(self, filename=None, config=None): # type: ignore + def wrap_function(self, filename=None, config=None): # type: ignore try: netmiko_object = self._netmiko_device if netmiko_object is None: @@ -233,8 +225,8 @@ def _get_diff(self) -> str: try: diff_out = ( diff_out.split("Generating Rollback Patch")[1] - .replace("Rollback Patch is Empty", "") - .strip() + .replace("Rollback Patch is Empty", "") + .strip() ) for line in diff_out.splitlines(): if line: @@ -512,7 +504,7 @@ def traceroute( ip_address = "*" traceroute_result["success"][hop_index]["probes"][ probe_index + 1 - ] = { + ] = { "host_name": str(host_name), "ip_address": str(ip_address), "rtt": rtt, @@ -564,7 +556,7 @@ def _create_tmp_file(config: str) -> str: fobj.write(config) return filename - def _disable_confirmation(self) -> str: + def _disable_confirmation(self) -> None: self._send_command_list(["terminal dont-ask"]) def get_config( @@ -795,7 +787,9 @@ def open(self) -> None: def close(self) -> None: self.device = None - def _send_command(self, command: str, raw_text: bool = False) -> Any: + def _send_command( + self, command: str, raw_text: bool = False + ) -> Dict[str, Union[str, Dict[str, Any]]]: """ Wrapper for NX-API show method. @@ -1452,7 +1446,9 @@ def get_users(self) -> Dict[str, models.UsersDict]: users[username]["sshkeys"].append(str(sshkeyvalue)) return users - def get_network_instances(self, name: str = "") -> Dict[str, models.NetworkInstanceDict]: + def get_network_instances( + self, name: str = "" + ) -> Dict[str, models.NetworkInstanceDict]: """ get_network_instances implementation for NX-OS """ # command 'show vrf detail' returns all VRFs with detailed information @@ -1569,7 +1565,7 @@ def _process_fans(fan_data: Dict) -> Dict[str, models.FanDict]: # Copying the behavior of eos.py where if the fanstatus key is not found # we default the status to True "status": entry.get("fanstatus", "Ok") - == "Ok" + == "Ok" } return normalized @@ -1590,7 +1586,7 @@ def _process_temperature( "temperature": float(entry.get("curtemp", -1)), "is_alert": entry.get("alarmstatus", "Ok").rstrip() != "Ok", "is_critical": float(entry.get("curtemp")) - > float(entry.get("majthres")), + > float(entry.get("majthres")), } count += 1 return normalized From 39b63482a809877f60963495e049db6e5d4c4b22 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 27 Mar 2021 17:49:56 +0100 Subject: [PATCH 36/74] Solve type errors --- mypy.ini | 3 --- napalm/base/base.py | 3 +-- napalm/base/helpers.py | 8 +++++-- napalm/base/mock.py | 3 ++- napalm/base/test/models.py | 2 +- napalm/base/utils/string_parsers.py | 2 +- napalm/base/validate.py | 4 ++-- napalm/nxapi_plumbing/api_client.py | 1 - napalm/nxos/nxos.py | 33 +++++++++++++++++++---------- 9 files changed, 35 insertions(+), 24 deletions(-) diff --git a/mypy.ini b/mypy.ini index cc98b8d84..9819a29a8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -23,9 +23,6 @@ disallow_untyped_defs = True [mypy-napalm.base.clitools.*] ignore_errors = True -[mypy-napalm.nxapi_plumbing.*] -disallow_untyped_defs = True - [mypy-napalm.base.test.*] ignore_errors = True diff --git a/napalm/base/base.py b/napalm/base/base.py index e8ced5053..4e010cef7 100644 --- a/napalm/base/base.py +++ b/napalm/base/base.py @@ -63,7 +63,7 @@ def __init__( """ raise NotImplementedError - def __enter__(self) -> "NetworkDriver": + def __enter__(self) -> "NetworkDriver": # type: ignore try: self.open() return self @@ -74,7 +74,6 @@ def __enter__(self) -> "NetworkDriver": else: raise - def __exit__( # type: ignore def __exit__( self, exc_type: Optional[Type[BaseException]], diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index 311ff1081..3ee50a1f4 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -11,9 +11,11 @@ # third party libs from typing import Optional, Dict, Any, List, Union, Tuple, TypeVar, Callable +from ciscoconfparse import CiscoConfParse +from typing import Optional, Dict, Any, List, Union, Tuple, TypeVar, Callable, Type + import jinja2 import textfsm -from ciscoconfparse import CiscoConfParse from lxml import etree from netaddr import EUI from netaddr import IPAddress @@ -22,6 +24,8 @@ # local modules import napalm.base.exceptions from napalm.base import constants +from napalm.base.test.models import ConfigDict +from napalm.base.utils.jinja_filters import CustomJinjaFilters from napalm.base.canonical_map import base_interfaces, reverse_mapping from napalm.base.test.models import ConfigDict from napalm.base.utils.jinja_filters import CustomJinjaFilters @@ -303,7 +307,7 @@ def find_txt( return str(value) -def convert(to: Callable[[T], R], who: Optional[T], default: R = "") -> R: # type: ignore +def convert(to: Callable[[T], R], who: Optional[T], default: R = "") -> R: """ Converts data to a specific datatype. In case of error, will return a default value. diff --git a/napalm/base/mock.py b/napalm/base/mock.py index 91405cf53..64074e6ac 100644 --- a/napalm/base/mock.py +++ b/napalm/base/mock.py @@ -11,7 +11,8 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under # the License. -from typing import Optional, List, Dict, Union, Any, Callable +from collections import Callable +from typing import Optional, List, Dict, Union, Any from napalm.base.base import NetworkDriver import napalm.base.exceptions diff --git a/napalm/base/test/models.py b/napalm/base/test/models.py index 2e060eea4..7d94fccc8 100644 --- a/napalm/base/test/models.py +++ b/napalm/base/test/models.py @@ -1,4 +1,4 @@ -from typing import Dict, Literal, Union, List, Optional +from typing import Dict, List try: from typing import TypedDict diff --git a/napalm/base/utils/string_parsers.py b/napalm/base/utils/string_parsers.py index 2d1ca14a9..62e14a4fb 100644 --- a/napalm/base/utils/string_parsers.py +++ b/napalm/base/utils/string_parsers.py @@ -16,7 +16,7 @@ def alphanum_key(key: str) -> List[Union[str, int]]: def sorted_nicely(sort_me: Iterable) -> Iterable: - """ Sort the given iterable in the way that humans expect.""" + """Sort the given iterable in the way that humans expect.""" return sorted(sort_me, key=alphanum_key) diff --git a/napalm/base/validate.py b/napalm/base/validate.py index 1e2b529a7..122bc0198 100644 --- a/napalm/base/validate.py +++ b/napalm/base/validate.py @@ -3,11 +3,11 @@ See: https://napalm.readthedocs.io/en/latest/validate.html """ -import copy -import re from typing import Dict, List, Union, TypeVar, Optional, TYPE_CHECKING import yaml +import copy +import re if TYPE_CHECKING: from napalm.base import NetworkDriver diff --git a/napalm/nxapi_plumbing/api_client.py b/napalm/nxapi_plumbing/api_client.py index fc876010b..f9f9e3977 100644 --- a/napalm/nxapi_plumbing/api_client.py +++ b/napalm/nxapi_plumbing/api_client.py @@ -138,7 +138,6 @@ def _nxapi_command( if isinstance(commands, string_types): commands = [commands] - raw_text = True if method == "cli_ascii" else False response = self._send_request(commands, method=method) diff --git a/napalm/nxos/nxos.py b/napalm/nxos/nxos.py index 3b56c6476..f75696ce0 100644 --- a/napalm/nxos/nxos.py +++ b/napalm/nxos/nxos.py @@ -19,12 +19,25 @@ import tempfile import time import uuid + # import stdlib from abc import abstractmethod from builtins import super from collections import defaultdict + # import third party lib -from typing import Optional, Dict, List, Union, Any, cast, Callable, TypeVar +from typing import ( + Optional, + Dict, + List, + Union, + Any, + cast, + Callable, + TypeVar, + TypedDict, + DefaultDict, +) from netaddr import IPAddress from netaddr.core import AddrFormatError @@ -32,6 +45,7 @@ from requests.exceptions import ConnectionError import napalm.base.constants as c + # import NAPALM Base import napalm.base.helpers from napalm.base import NetworkDriver @@ -61,13 +75,12 @@ }, ) -F = TypeVar('F', bound=Callable[..., Any]) def ensure_netmiko_conn(func: F) -> F: """Decorator that ensures Netmiko connection exists.""" - def wrap_function(self, filename=None, config=None): # type: ignore + def wrap_function(self, filename=None, config=None): # type: ignore try: netmiko_object = self._netmiko_device if netmiko_object is None: @@ -225,8 +238,8 @@ def _get_diff(self) -> str: try: diff_out = ( diff_out.split("Generating Rollback Patch")[1] - .replace("Rollback Patch is Empty", "") - .strip() + .replace("Rollback Patch is Empty", "") + .strip() ) for line in diff_out.splitlines(): if line: @@ -504,7 +517,7 @@ def traceroute( ip_address = "*" traceroute_result["success"][hop_index]["probes"][ probe_index + 1 - ] = { + ] = { "host_name": str(host_name), "ip_address": str(ip_address), "rtt": rtt, @@ -787,9 +800,7 @@ def open(self) -> None: def close(self) -> None: self.device = None - def _send_command( - self, command: str, raw_text: bool = False - ) -> Dict[str, Union[str, Dict[str, Any]]]: + def _send_command(self, command: str, raw_text: bool = False) -> Any: """ Wrapper for NX-API show method. @@ -1565,7 +1576,7 @@ def _process_fans(fan_data: Dict) -> Dict[str, models.FanDict]: # Copying the behavior of eos.py where if the fanstatus key is not found # we default the status to True "status": entry.get("fanstatus", "Ok") - == "Ok" + == "Ok" } return normalized @@ -1586,7 +1597,7 @@ def _process_temperature( "temperature": float(entry.get("curtemp", -1)), "is_alert": entry.get("alarmstatus", "Ok").rstrip() != "Ok", "is_critical": float(entry.get("curtemp")) - > float(entry.get("majthres")), + > float(entry.get("majthres")), } count += 1 return normalized From c528848ee53c26d0ccaa0742114b671ff2c19676 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 27 Mar 2021 17:51:33 +0100 Subject: [PATCH 37/74] Add type checker to github actions From d6ed6e26ffd710fb2f08d0909d40eb85753fbbfa Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 27 Mar 2021 18:01:41 +0100 Subject: [PATCH 38/74] Fix unused imports --- napalm/base/base.py | 2 -- napalm/base/helpers.py | 7 +------ napalm/base/validate.py | 5 +++-- napalm/nxapi_plumbing/api_client.py | 2 +- napalm/nxapi_plumbing/device.py | 1 - napalm/nxos/nxos.py | 2 +- 6 files changed, 6 insertions(+), 13 deletions(-) diff --git a/napalm/base/base.py b/napalm/base/base.py index 4e010cef7..7f929e3cc 100644 --- a/napalm/base/base.py +++ b/napalm/base/base.py @@ -12,8 +12,6 @@ # License for the specific language governing permissions and limitations under # the License. -from __future__ import annotations - import sys from types import TracebackType from typing import Optional, Dict, Type, Any, List, Union diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index 3ee50a1f4..9d14b1f1a 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -1,21 +1,18 @@ """Helper functions for the NAPALM base.""" import itertools import logging - # std libs import os import re import sys from collections.abc import Iterable - # third party libs -from typing import Optional, Dict, Any, List, Union, Tuple, TypeVar, Callable from ciscoconfparse import CiscoConfParse from typing import Optional, Dict, Any, List, Union, Tuple, TypeVar, Callable, Type - import jinja2 import textfsm +from ciscoconfparse import CiscoConfParse from lxml import etree from netaddr import EUI from netaddr import IPAddress @@ -24,8 +21,6 @@ # local modules import napalm.base.exceptions from napalm.base import constants -from napalm.base.test.models import ConfigDict -from napalm.base.utils.jinja_filters import CustomJinjaFilters from napalm.base.canonical_map import base_interfaces, reverse_mapping from napalm.base.test.models import ConfigDict from napalm.base.utils.jinja_filters import CustomJinjaFilters diff --git a/napalm/base/validate.py b/napalm/base/validate.py index 122bc0198..7cd2ad2fc 100644 --- a/napalm/base/validate.py +++ b/napalm/base/validate.py @@ -3,11 +3,12 @@ See: https://napalm.readthedocs.io/en/latest/validate.html """ -from typing import Dict, List, Union, TypeVar, Optional, TYPE_CHECKING -import yaml import copy import re +from typing import Dict, List, Union, TypeVar, Optional, TYPE_CHECKING + +import yaml if TYPE_CHECKING: from napalm.base import NetworkDriver diff --git a/napalm/nxapi_plumbing/api_client.py b/napalm/nxapi_plumbing/api_client.py index f9f9e3977..0efc3fee4 100644 --- a/napalm/nxapi_plumbing/api_client.py +++ b/napalm/nxapi_plumbing/api_client.py @@ -6,7 +6,7 @@ from __future__ import print_function, unicode_literals from builtins import super -from typing import Optional, List, Dict, Union, Any +from typing import Optional, List, Dict, Any import requests from requests.auth import HTTPBasicAuth diff --git a/napalm/nxapi_plumbing/device.py b/napalm/nxapi_plumbing/device.py index b89434338..3f2ac5f5b 100644 --- a/napalm/nxapi_plumbing/device.py +++ b/napalm/nxapi_plumbing/device.py @@ -7,7 +7,6 @@ from __future__ import print_function, unicode_literals from typing import List, Optional, Any, Union -from typing import List, Optional, Dict, Any, Union from napalm.nxapi_plumbing.errors import NXAPIError, NXAPICommandError from napalm.nxapi_plumbing.api_client import RPCClient, XMLClient, RPCBase diff --git a/napalm/nxos/nxos.py b/napalm/nxos/nxos.py index f75696ce0..81c49f9cc 100644 --- a/napalm/nxos/nxos.py +++ b/napalm/nxos/nxos.py @@ -41,7 +41,7 @@ from netaddr import IPAddress from netaddr.core import AddrFormatError -from netmiko import file_transfer, ConnectHandler +from netmiko import file_transfer from requests.exceptions import ConnectionError import napalm.base.constants as c From ce4962c066a9ba6fe8b5b1d1aab80660470344b0 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 27 Mar 2021 18:11:24 +0100 Subject: [PATCH 39/74] Black formatting --- napalm/base/helpers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index 9d14b1f1a..3ea56db94 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -1,11 +1,13 @@ """Helper functions for the NAPALM base.""" import itertools import logging + # std libs import os import re import sys from collections.abc import Iterable + # third party libs from ciscoconfparse import CiscoConfParse From 3f4c6adb078da35db69929b0f6ed6e23f694da52 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 27 Mar 2021 18:14:54 +0100 Subject: [PATCH 40/74] Fix further pipeline problems --- napalm/base/base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/napalm/base/base.py b/napalm/base/base.py index 7f929e3cc..79bf434ed 100644 --- a/napalm/base/base.py +++ b/napalm/base/base.py @@ -16,7 +16,10 @@ from types import TracebackType from typing import Optional, Dict, Type, Any, List, Union -from typing_extensions import Literal +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal from netmiko import ConnectHandler, NetMikoTimeoutException From 91dd630b48786256ba9aeb2c991f43db119fa28d Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 27 Mar 2021 18:17:47 +0100 Subject: [PATCH 41/74] Fix further pipeline errors --- napalm/base/base.py | 2 +- napalm/base/helpers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/napalm/base/base.py b/napalm/base/base.py index 79bf434ed..a572cc09f 100644 --- a/napalm/base/base.py +++ b/napalm/base/base.py @@ -19,7 +19,7 @@ try: from typing import Literal except ImportError: - from typing_extensions import Literal + from typing_extensions import Literal # type: ignore from netmiko import ConnectHandler, NetMikoTimeoutException diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index 3ea56db94..07b58ae1c 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -304,7 +304,7 @@ def find_txt( return str(value) -def convert(to: Callable[[T], R], who: Optional[T], default: R = "") -> R: +def convert(to: Callable[[T], R], who: Optional[T], default: R = "") -> R: # type: ignore """ Converts data to a specific datatype. In case of error, will return a default value. From 229820fb80cbefc7832ab4f4c9504b07b0236b75 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 27 Mar 2021 18:24:53 +0100 Subject: [PATCH 42/74] Convert all typing_extensions try/except imports to pure typing_extensions imports --- napalm/base/base.py | 5 +---- napalm/base/test/models.py | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/napalm/base/base.py b/napalm/base/base.py index a572cc09f..7f929e3cc 100644 --- a/napalm/base/base.py +++ b/napalm/base/base.py @@ -16,10 +16,7 @@ from types import TracebackType from typing import Optional, Dict, Type, Any, List, Union -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal # type: ignore +from typing_extensions import Literal from netmiko import ConnectHandler, NetMikoTimeoutException diff --git a/napalm/base/test/models.py b/napalm/base/test/models.py index 7d94fccc8..fdf719553 100644 --- a/napalm/base/test/models.py +++ b/napalm/base/test/models.py @@ -1,9 +1,6 @@ from typing import Dict, List -try: - from typing import TypedDict -except ImportError: - from typing_extensions import TypedDict +from typing_extensions import TypedDict ConfigurationDict = TypedDict( "ConfigurationDict", {"running": str, "candidate": str, "startup": str} From 3d099403c239be925bb0cbc14dc6c71cda80707c Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 27 Mar 2021 18:28:07 +0100 Subject: [PATCH 43/74] Import TypedDict from typing_extensions in nxos.py --- napalm/nxos/nxos.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/napalm/nxos/nxos.py b/napalm/nxos/nxos.py index 81c49f9cc..a69c8ff93 100644 --- a/napalm/nxos/nxos.py +++ b/napalm/nxos/nxos.py @@ -35,10 +35,11 @@ cast, Callable, TypeVar, - TypedDict, DefaultDict, ) +from typing_extensions import TypedDict + from netaddr import IPAddress from netaddr.core import AddrFormatError from netmiko import file_transfer From 53d799536b4911e5c7975fb187b8787c39e7513a Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 1 May 2021 13:42:16 +0200 Subject: [PATCH 44/74] Fix Callable import in mock.py --- napalm/base/mock.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/napalm/base/mock.py b/napalm/base/mock.py index 64074e6ac..91405cf53 100644 --- a/napalm/base/mock.py +++ b/napalm/base/mock.py @@ -11,8 +11,7 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under # the License. -from collections import Callable -from typing import Optional, List, Dict, Union, Any +from typing import Optional, List, Dict, Union, Any, Callable from napalm.base.base import NetworkDriver import napalm.base.exceptions From 10c0c38adf83a4c85b658af4cddf4358b407f213 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Fri, 4 Jun 2021 19:10:05 +0200 Subject: [PATCH 45/74] Black --- napalm/base/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/napalm/base/base.py b/napalm/base/base.py index 7f929e3cc..c99efee77 100644 --- a/napalm/base/base.py +++ b/napalm/base/base.py @@ -1316,7 +1316,7 @@ def ping( size: int = c.PING_SIZE, count: int = c.PING_COUNT, vrf: str = c.PING_VRF, - source_interface: str = c.PING_SOURCE_INTERFACE + source_interface: str = c.PING_SOURCE_INTERFACE, ) -> models.PingResultDict: """ Executes ping on the device and returns a dictionary with the result From f7c36a0913ba2bb49fda287f3e9c1d4351299f67 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Fri, 4 Jun 2021 19:17:39 +0200 Subject: [PATCH 46/74] Black --- napalm/nxos/nxos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/napalm/nxos/nxos.py b/napalm/nxos/nxos.py index a69c8ff93..120d2b935 100644 --- a/napalm/nxos/nxos.py +++ b/napalm/nxos/nxos.py @@ -1461,7 +1461,7 @@ def get_users(self) -> Dict[str, models.UsersDict]: def get_network_instances( self, name: str = "" ) -> Dict[str, models.NetworkInstanceDict]: - """ get_network_instances implementation for NX-OS """ + """get_network_instances implementation for NX-OS""" # command 'show vrf detail' returns all VRFs with detailed information # format: list of dictionaries with keys such as 'vrf_name' and 'rd' From 37e409a9ad9a3e01ee0317fc1e71c3d643f62838 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sun, 25 Jul 2021 16:00:52 +0200 Subject: [PATCH 47/74] Black --- napalm/base/test/models.py | 4 +++- napalm/base/utils/string_parsers.py | 2 +- napalm/nxos/nxos.py | 1 - 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/napalm/base/test/models.py b/napalm/base/test/models.py index fdf719553..2cc0abf72 100644 --- a/napalm/base/test/models.py +++ b/napalm/base/test/models.py @@ -400,7 +400,9 @@ ConfigDict = TypedDict("ConfigDict", {"running": str, "startup": str, "candidate": str}) -TracerouteResultDictEntry = TypedDict("TracerouteResultDictEntry", {"probes": Dict[int, TracerouteDict]}) +TracerouteResultDictEntry = TypedDict( + "TracerouteResultDictEntry", {"probes": Dict[int, TracerouteDict]} +) TracerouteResultDict = TypedDict( "TracerouteResultDict", diff --git a/napalm/base/utils/string_parsers.py b/napalm/base/utils/string_parsers.py index 62e14a4fb..c7dd8f376 100644 --- a/napalm/base/utils/string_parsers.py +++ b/napalm/base/utils/string_parsers.py @@ -11,7 +11,7 @@ def convert(text: str) -> Union[str, int]: def alphanum_key(key: str) -> List[Union[str, int]]: - """ split on end numbers.""" + """split on end numbers.""" return [convert(c) for c in re.split("([0-9]+)", key)] diff --git a/napalm/nxos/nxos.py b/napalm/nxos/nxos.py index 120d2b935..1b332fabc 100644 --- a/napalm/nxos/nxos.py +++ b/napalm/nxos/nxos.py @@ -77,7 +77,6 @@ ) - def ensure_netmiko_conn(func: F) -> F: """Decorator that ensures Netmiko connection exists.""" From 81c7a44321bd5c5a93ceee2c5a28c15da7822151 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sun, 25 Jul 2021 16:07:57 +0200 Subject: [PATCH 48/74] Fix linter issues --- napalm/base/helpers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index 07b58ae1c..a5705a91f 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -10,8 +10,7 @@ # third party libs -from ciscoconfparse import CiscoConfParse -from typing import Optional, Dict, Any, List, Union, Tuple, TypeVar, Callable, Type +from typing import Optional, Dict, Any, List, Union, Tuple, TypeVar, Callable import jinja2 import textfsm from ciscoconfparse import CiscoConfParse From 5c38927c9c5a8332933d5325989fbeaef07bb4a3 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sun, 25 Jul 2021 16:27:32 +0200 Subject: [PATCH 49/74] Fix MyPy issues --- napalm/base/base.py | 1 + napalm/base/test/models.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/napalm/base/base.py b/napalm/base/base.py index c99efee77..695dc2ece 100644 --- a/napalm/base/base.py +++ b/napalm/base/base.py @@ -90,6 +90,7 @@ def __exit__( ) print(epilog) return False + return None def __del__(self) -> None: """ diff --git a/napalm/base/test/models.py b/napalm/base/test/models.py index 2cc0abf72..d169fa30b 100644 --- a/napalm/base/test/models.py +++ b/napalm/base/test/models.py @@ -1,4 +1,4 @@ -from typing import Dict, List +from typing import Dict, List, Union from typing_extensions import TypedDict @@ -89,8 +89,8 @@ "fans": Dict[str, FanDict], "temperature": Dict[str, TemperatureDict], "power": Dict[str, PowerDict], - "cpu": Dict[str, CPUDict], - "memory": Dict[str, MemoryDict], + "cpu": Dict[int, CPUDict], + "memory": MemoryDict, }, ) From f74e05f48a42b702a3794ba6049b44431efca3fa Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sun, 25 Jul 2021 16:30:39 +0200 Subject: [PATCH 50/74] Remove unused import --- napalm/base/test/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/napalm/base/test/models.py b/napalm/base/test/models.py index d169fa30b..338d952c7 100644 --- a/napalm/base/test/models.py +++ b/napalm/base/test/models.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Union +from typing import Dict, List from typing_extensions import TypedDict From 47b93aefdd3c5bcba12627bb00f620542e38ce45 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 27 Mar 2021 17:49:56 +0100 Subject: [PATCH 51/74] Solve type errors --- napalm/base/base.py | 2 +- napalm/base/helpers.py | 7 ++++--- napalm/base/mock.py | 9 +++++---- napalm/base/validate.py | 4 ++-- napalm/nxos/nxos.py | 6 ++++-- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/napalm/base/base.py b/napalm/base/base.py index 695dc2ece..9cb9ae714 100644 --- a/napalm/base/base.py +++ b/napalm/base/base.py @@ -72,7 +72,7 @@ def __enter__(self) -> "NetworkDriver": # type: ignore else: raise - def __exit__( + def __exit__( # type: ignore self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index a5705a91f..d9fa08f21 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -6,11 +6,10 @@ import os import re import sys +from typing import Optional, Dict, Any, List, Union, Tuple, TypeVar, Callable, Type from collections.abc import Iterable # third party libs - -from typing import Optional, Dict, Any, List, Union, Tuple, TypeVar, Callable import jinja2 import textfsm from ciscoconfparse import CiscoConfParse @@ -22,6 +21,8 @@ # local modules import napalm.base.exceptions from napalm.base import constants +from napalm.base.test.models import ConfigDict +from napalm.base.utils.jinja_filters import CustomJinjaFilters from napalm.base.canonical_map import base_interfaces, reverse_mapping from napalm.base.test.models import ConfigDict from napalm.base.utils.jinja_filters import CustomJinjaFilters @@ -303,7 +304,7 @@ def find_txt( return str(value) -def convert(to: Callable[[T], R], who: Optional[T], default: R = "") -> R: # type: ignore +def convert(to: Callable[[T], R], who: Optional[T], default: R = "") -> R: """ Converts data to a specific datatype. In case of error, will return a default value. diff --git a/napalm/base/mock.py b/napalm/base/mock.py index 91405cf53..50a670e48 100644 --- a/napalm/base/mock.py +++ b/napalm/base/mock.py @@ -11,7 +11,8 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under # the License. -from typing import Optional, List, Dict, Union, Any, Callable +from collections import Callable +from typing import Optional, List, Dict, Union, Any from napalm.base.base import NetworkDriver import napalm.base.exceptions @@ -213,7 +214,7 @@ def discard_config(self) -> None: self.config = None mocked_data(self.path, "discard_config", count) - def confirm_commit(self): + def confirm_commit(self) -> None: count = self._count_calls("confirm_commit") self._raise_if_closed() self.merge = None @@ -222,10 +223,10 @@ def confirm_commit(self): self._pending_commits = False mocked_data(self.path, "confirm_commit", count) - def has_pending_commit(self): + def has_pending_commit(self) -> bool: return self._pending_commits - def rollback(self): + def rollback(self) -> None: self.config_session = None self._pending_commits = False diff --git a/napalm/base/validate.py b/napalm/base/validate.py index 7cd2ad2fc..ec5a7273c 100644 --- a/napalm/base/validate.py +++ b/napalm/base/validate.py @@ -3,13 +3,13 @@ See: https://napalm.readthedocs.io/en/latest/validate.html """ +from __future__ import annotations +import yaml import copy import re from typing import Dict, List, Union, TypeVar, Optional, TYPE_CHECKING -import yaml - if TYPE_CHECKING: from napalm.base import NetworkDriver from napalm.base.exceptions import ValidationException diff --git a/napalm/nxos/nxos.py b/napalm/nxos/nxos.py index 1b332fabc..24abadcc4 100644 --- a/napalm/nxos/nxos.py +++ b/napalm/nxos/nxos.py @@ -35,10 +35,12 @@ cast, Callable, TypeVar, - DefaultDict, ) -from typing_extensions import TypedDict +from typing_extensions import ( + TypedDict, + DefaultDict, +) from netaddr import IPAddress from netaddr.core import AddrFormatError From 431ebf42b321f3ebca4063a99523b14a4f89c2f2 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 27 Mar 2021 17:51:33 +0100 Subject: [PATCH 52/74] Add type checker to github actions From 448589951c178d51113ed3820e2293d9e1883155 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sun, 25 Jul 2021 16:27:32 +0200 Subject: [PATCH 53/74] Fix MyPy issues --- napalm/base/test/models.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/napalm/base/test/models.py b/napalm/base/test/models.py index 338d952c7..5ca4e0ba9 100644 --- a/napalm/base/test/models.py +++ b/napalm/base/test/models.py @@ -1,4 +1,4 @@ -from typing import Dict, List +from typing import Dict, List, Union from typing_extensions import TypedDict @@ -404,14 +404,6 @@ "TracerouteResultDictEntry", {"probes": Dict[int, TracerouteDict]} ) -TracerouteResultDict = TypedDict( - "TracerouteResultDict", - {"success": Dict[int, TracerouteResultDictEntry], "error": str}, - total=False, -) - -UsersDict = TypedDict("UsersDict", {"level": int, "password": str, "sshkeys": List}) - OpticsStateDict = TypedDict( "OpticsStateDict", {"instant": float, "avg": float, "min": float, "max": float} ) @@ -433,10 +425,6 @@ "OpticsPhysicalChannelsDict", {"channels": OpticsPerChannelDict} ) -OpticsDict = TypedDict("OpticsDict", {"physical_channels": OpticsPhysicalChannelsDict}) - -ConfigDict = TypedDict("ConfigDict", {"running": str, "startup": str, "candidate": str}) - NetworkInstanceDict = TypedDict( "NetworkInstanceDict", {"name": str, "type": str, "state": dict, "interfaces": dict} ) From 9260bfae0fe54b03961db010bfc7f052dd819d9d Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Fri, 11 Feb 2022 09:52:16 +0100 Subject: [PATCH 54/74] Fix pylama issues --- napalm/base/helpers.py | 4 +--- napalm/base/test/models.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index d9fa08f21..27e20e1b4 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -6,7 +6,7 @@ import os import re import sys -from typing import Optional, Dict, Any, List, Union, Tuple, TypeVar, Callable, Type +from typing import Optional, Dict, Any, List, Union, Tuple, TypeVar, Callable from collections.abc import Iterable # third party libs @@ -24,8 +24,6 @@ from napalm.base.test.models import ConfigDict from napalm.base.utils.jinja_filters import CustomJinjaFilters from napalm.base.canonical_map import base_interfaces, reverse_mapping -from napalm.base.test.models import ConfigDict -from napalm.base.utils.jinja_filters import CustomJinjaFilters T = TypeVar("T") R = TypeVar("R") diff --git a/napalm/base/test/models.py b/napalm/base/test/models.py index 5ca4e0ba9..4714f74bb 100644 --- a/napalm/base/test/models.py +++ b/napalm/base/test/models.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Union +from typing import Dict, List from typing_extensions import TypedDict From 3bc3ed877ed8aada5118346ccc5c37e710fec133 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Fri, 11 Feb 2022 09:57:28 +0100 Subject: [PATCH 55/74] Remove future import. --- napalm/base/validate.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/napalm/base/validate.py b/napalm/base/validate.py index ec5a7273c..b7051c78c 100644 --- a/napalm/base/validate.py +++ b/napalm/base/validate.py @@ -3,8 +3,6 @@ See: https://napalm.readthedocs.io/en/latest/validate.html """ -from __future__ import annotations - import yaml import copy import re From a9780962583eeb8a4acdad2017c6a82571a67fcc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Feb 2022 16:34:46 +0000 Subject: [PATCH 56/74] Bump pytest-pythonpath from 0.7.3 to 0.7.4 Bumps [pytest-pythonpath](https://github.com/bigsassy/pytest-pythonpath) from 0.7.3 to 0.7.4. - [Release notes](https://github.com/bigsassy/pytest-pythonpath/releases) - [Commits](https://github.com/bigsassy/pytest-pythonpath/commits) --- updated-dependencies: - dependency-name: pytest-pythonpath dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index f7b3077f1..672e35301 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,7 +5,7 @@ flake8-import-order==0.18.1 pytest==5.4.3 pytest-cov==3.0.0 pytest-json==0.4.0 -pytest-pythonpath==0.7.3 +pytest-pythonpath==0.7.4 pylama==7.7.1 mock==4.0.3 tox==3.24.4 From 92119dcf4240a2727dbca5ae8b93c619cd047222 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 12 Feb 2022 20:07:15 +0100 Subject: [PATCH 57/74] Fix outstanding Mypy errors - Fix wrong import path for napalm.base.exceptions.CommitError - Re-write convert method to properly utilize typing --- napalm/base/helpers.py | 26 +++++++++++++++++++++++--- napalm/base/mock.py | 4 +++- napalm/nxos/nxos.py | 6 +++--- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index 27e20e1b4..36ed178af 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -302,16 +302,36 @@ def find_txt( return str(value) -def convert(to: Callable[[T], R], who: Optional[T], default: R = "") -> R: +def convert(to: Callable[[T], R], who: Optional[T], default: Optional[R] = None) -> R: """ Converts data to a specific datatype. In case of error, will return a default value. :param to: datatype to be casted to. :param who: value to cast. - :param default: value to return in case of error. - :return: a str value. + :param default: default value to return in case of an error with the conversion function. + :return: the result of the cast or a default value. """ + if default is None: + # Mypy is currently unable to resolve the Optional[R] correctly, therefore the following + # assignments to 'default' need a 'type: ignore' statement. + # Ref: https://github.com/python/mypy/issues/8708 + if to in [str, ip, mac]: + default = "" # type: ignore + elif to in [float, int]: + default = 0 # type: ignore + elif to == bool: + default = False # type: ignore + elif to == list: + default = [] # type: ignore + else: + raise ValueError( + f"Can't convert with callable {to} - no default is defined for this type." + ) + + # This is safe because the None-case if handled above + assert default is not None + if who is None: return default try: diff --git a/napalm/base/mock.py b/napalm/base/mock.py index 50a670e48..872946fcc 100644 --- a/napalm/base/mock.py +++ b/napalm/base/mock.py @@ -198,7 +198,9 @@ def commit_config(self, message: str = "", revert_in: Optional[int] = None) -> N self._raise_if_closed() if revert_in is not None: if self.has_pending_commit(): - raise napalm.CommitError("Pending commit confirm already in process!") + raise napalm.base.exceptions.CommitError( + "Pending commit confirm already in process!" + ) else: self._pending_commits = True self.merge = None diff --git a/napalm/nxos/nxos.py b/napalm/nxos/nxos.py index 24abadcc4..7355714b1 100644 --- a/napalm/nxos/nxos.py +++ b/napalm/nxos/nxos.py @@ -862,7 +862,7 @@ def _compute_timestamp(stupid_cisco_output: str) -> float: for key in things_keys: if key in part: things[key]["count"] = napalm.base.helpers.convert( - int, part.replace(key, ""), 0 + int, part.replace(key, "") ) delta = sum([det.get("count", 0) * det["weight"] for det in things.values()]) @@ -1025,7 +1025,7 @@ def get_interfaces(self) -> Dict[str, models.InterfaceDict]: "speed": interface_speed, "mtu": interface_mtu, "mac_address": napalm.base.helpers.convert( - napalm.base.helpers.mac, mac_address, "" + napalm.base.helpers.mac, mac_address ), } return interfaces @@ -1161,7 +1161,7 @@ def get_arp_table(self, vrf: str = "") -> List[models.ARPTableDict]: { "interface": interface, "mac": napalm.base.helpers.convert( - napalm.base.helpers.mac, raw_mac, raw_mac + napalm.base.helpers.mac, raw_mac ), "ip": napalm.base.helpers.ip(raw_ip), "age": age_sec, From 6731176dbbdce49a1086b8647d82d4c8abeb6062 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 12 Feb 2022 20:44:47 +0100 Subject: [PATCH 58/74] Fix new type errors from develop rebase. --- napalm/base/helpers.py | 29 +++++++++++++++++++++-------- napalm/nxapi_plumbing/api_client.py | 4 ++-- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index 36ed178af..a8fa0fd78 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -77,11 +77,20 @@ def load_template( ) else: # Search modules for template paths - search_path = [ - os.path.dirname(os.path.abspath(sys.modules[c.__module__].__file__)) - for c in cls.__class__.mro() - if c is not object - ] + for c in cls.__class__.mro(): + if c is object: + continue + module = sys.modules[c.__module__].__file__ + if module: + path = os.path.abspath(module) + else: + continue + if path: + path_to_append = os.path.dirname(path) + else: + continue + if path_to_append: + search_path.append(path_to_append) if openconfig: search_path = ["{}/oc_templates".format(s) for s in search_path] @@ -209,9 +218,13 @@ def textfsm_extractor( for c in cls.__class__.mro(): if c is object: continue - current_dir = os.path.dirname( - os.path.abspath(sys.modules[c.__module__].__file__) - ) + module = sys.modules[c.__module__].__file__ + if module: + current_dir = os.path.dirname( + os.path.abspath(module) + ) + else: + continue template_dir_path = "{current_dir}/utils/textfsm_templates".format( current_dir=current_dir ) diff --git a/napalm/nxapi_plumbing/api_client.py b/napalm/nxapi_plumbing/api_client.py index cf636e573..a813ec716 100644 --- a/napalm/nxapi_plumbing/api_client.py +++ b/napalm/nxapi_plumbing/api_client.py @@ -12,7 +12,7 @@ from requests import Response from requests.auth import HTTPBasicAuth from requests.exceptions import ConnectionError -from requests.packages.urllib3.exceptions import InsecureRequestWarning +from requests.packages.urllib3.exceptions import InsecureRequestWarning # type: ignore import json from lxml import etree @@ -90,7 +90,7 @@ def _send_request(self, commands: List[str], method: str) -> Response: try: if not self.verify: - requests.packages.urllib3.disable_warnings(InsecureRequestWarning) + requests.packages.urllib3.disable_warnings(InsecureRequestWarning) # type: ignore response = requests.post( self.url, From ee7e0bb230dcc25b55c022054b28ed53553890ad Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 12 Feb 2022 20:45:10 +0100 Subject: [PATCH 59/74] Re-add new type checking requirements to dev-requirements. --- requirements-dev.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6f238e0c3..f12725676 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,3 +10,7 @@ pylama==7.7.1 mock==4.0.3 tox==3.24.5 mypy==0.931 +types-requests==2.27.9 +types-six==1.16.10 +types-setuptools==57.4.9 +types-PyYAML==6.0.4 \ No newline at end of file From 8c324ef502139162a79c48705f9d3df5bd29e327 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 12 Feb 2022 20:46:29 +0100 Subject: [PATCH 60/74] Apply black. --- napalm/base/helpers.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index a8fa0fd78..0b4a13f72 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -220,9 +220,7 @@ def textfsm_extractor( continue module = sys.modules[c.__module__].__file__ if module: - current_dir = os.path.dirname( - os.path.abspath(module) - ) + current_dir = os.path.dirname(os.path.abspath(module)) else: continue template_dir_path = "{current_dir}/utils/textfsm_templates".format( From 70f7b4e5f320f3eb9354f4349f1567939903a70a Mon Sep 17 00:00:00 2001 From: Xenia Mountrouidou Date: Sat, 12 Feb 2022 16:02:56 -0500 Subject: [PATCH 61/74] adds mac and ip checks for get_lldp_neighbors, get_lldp_neighbors_detail, get_interfaces_ip, get_ntp_peers, traceroute, ping --- napalm/ios/ios.py | 28 ++- .../normal/expected_result.json | 70 +++---- .../missing_hostname/expected_result.json | 24 +-- .../alternate/expected_result.json | 44 ++--- .../alternate2/expected_result.json | 48 ++--- .../alternate4/expected_result.json | 112 +++++------ .../device_id_20chars/expected_result.json | 164 ++++++++-------- .../expected_result.json | 164 ++++++++-------- .../missing_capability/expected_result.json | 44 ++--- .../normal/expected_result.json | 22 ++- .../older_ios/expected_result.json | 48 ++--- .../normal/expected_result.json | 176 +++++++++++++++++- 12 files changed, 572 insertions(+), 372 deletions(-) diff --git a/napalm/ios/ios.py b/napalm/ios/ios.py index 6bbdc8299..ddfad3ed8 100644 --- a/napalm/ios/ios.py +++ b/napalm/ios/ios.py @@ -876,11 +876,13 @@ def get_lldp_neighbors(self): lldp[intf_name] = [] for lldp_entry in entries: hostname = lldp_entry["remote_system_name"] + port = lldp_entry["remote_port"] # Match IOS behaviour of taking remote chassis ID # When lacking a system name (in show lldp neighbors) if not hostname: - hostname = lldp_entry["remote_chassis_id"] - lldp_dict = {"port": lldp_entry["remote_port"], "hostname": hostname} + hostname = napalm.base.helpers.mac(lldp_entry["remote_chassis_id"]) + port = napalm.base.helpers.mac(port) + lldp_dict = {"port": port, "hostname": hostname} lldp[intf_name].append(lldp_dict) return lldp @@ -901,6 +903,16 @@ def get_lldp_neighbors_detail(self, interface=""): if len(lldp_entries) == 0: return {} + # format chassis_id for consistency, if it is a mac address + for entry in lldp_entries: + if re.search( + r"^(\d|\w){4}.(\d|\w){4}.(\d|\w){4}$", entry["remote_chassis_id"] + ): + entry["remote_chassis_id"] = napalm.base.helpers.mac( + entry["remote_chassis_id"] + ) + print(entry["remote_chassis_id"]) + # Older IOS versions don't have 'Local Intf' defined in LLDP detail. # We need to get them from the non-detailed command # which is in the same sequence as the detailed output @@ -1203,6 +1215,7 @@ def get_interfaces_ip(self): m = re.match(INTERNET_ADDRESS, line) if m: ip, prefix = m.groups() + ip = napalm.base.helpers.ip(ip) ipv4.update({ip: {"prefix_length": int(prefix)}}) interfaces[interface_name] = {"ipv4": ipv4} @@ -1220,10 +1233,12 @@ def get_interfaces_ip(self): m = re.match(LINK_LOCAL_ADDRESS, line) if m: ip = m.group(1) + ip = napalm.base.helpers.ip(ip, 6) ipv6.update({ip: {"prefix_length": 10}}) m = re.match(GLOBAL_ADDRESS, line) if m: ip, prefix = m.groups() + ip = napalm.base.helpers.ip(ip, 6) ipv6.update({ip: {"prefix_length": int(prefix)}}) # Interface without ipv6 doesn't appears in show ipv6 interface @@ -2383,7 +2398,7 @@ def get_ntp_peers(self): ntp_stats = self.get_ntp_stats() return { - ntp_peer.get("remote"): {} + napalm.base.helpers.ip(ntp_peer.get("remote")): {} for ntp_peer in ntp_stats if ntp_peer.get("remote") } @@ -3251,7 +3266,10 @@ def ping( results_array = [] for i in range(probes_received): results_array.append( - {"ip_address": str(destination), "rtt": 0.0} + { + "ip_address": napalm.base.helpers.ip(str(destination)), + "rtt": 0.0, + } ) ping_dict["success"].update({"results": results_array}) @@ -3374,7 +3392,7 @@ def traceroute( current_probe += 1 # If current_element contains msec record the entry for probe elif "msec" in current_element: - ip_address = str(ip_address) + ip_address = napalm.base.helpers.ip(str(ip_address)) host_name = str(host_name) rtt = float(current_element.replace("msec", "")) results[current_hop]["probes"][current_probe][ diff --git a/test/ios/mocked_data/test_get_interfaces_ip/normal/expected_result.json b/test/ios/mocked_data/test_get_interfaces_ip/normal/expected_result.json index b1cf7ccae..4d6dbba3c 100644 --- a/test/ios/mocked_data/test_get_interfaces_ip/normal/expected_result.json +++ b/test/ios/mocked_data/test_get_interfaces_ip/normal/expected_result.json @@ -1,34 +1,40 @@ { - "Vlan20": { - "ipv4": { - "172.29.50.3": { - "prefix_length": 27 - } - }, - "ipv6": { - "FE80::C009:24FF:FEAC:0": { - "prefix_length": 10 - }, - "2002::1": { - "prefix_length": 64 - } - } - }, - "Vlan41": { - "ipv4": { - "172.29.52.34": { - "prefix_length": 27 - }, - "192.168.81.34": { - "prefix_length": 24 - } - } - }, - "Loopback0": { - "ipv6": { - "FE80::C009:24FF:FEAC:0": {"prefix_length": 10}, - "2001::1": {"prefix_length": 128}, - "2001::2": {"prefix_length": 128} - } - } + "Vlan20": { + "ipv4": { + "172.29.50.3": { + "prefix_length": 27 + } + }, + "ipv6": { + "fe80::c009:24ff:feac:0": { + "prefix_length": 10 + }, + "2002::1": { + "prefix_length": 64 + } + } + }, + "Vlan41": { + "ipv4": { + "172.29.52.34": { + "prefix_length": 27 + }, + "192.168.81.34": { + "prefix_length": 24 + } + } + }, + "Loopback0": { + "ipv6": { + "fe80::c009:24ff:feac:0": { + "prefix_length": 10 + }, + "2001::1": { + "prefix_length": 128 + }, + "2001::2": { + "prefix_length": 128 + } + } + } } diff --git a/test/ios/mocked_data/test_get_lldp_neighbors/missing_hostname/expected_result.json b/test/ios/mocked_data/test_get_lldp_neighbors/missing_hostname/expected_result.json index b82010b7c..b229b8671 100644 --- a/test/ios/mocked_data/test_get_lldp_neighbors/missing_hostname/expected_result.json +++ b/test/ios/mocked_data/test_get_lldp_neighbors/missing_hostname/expected_result.json @@ -1,14 +1,14 @@ { - "FastEthernet0/24": [ - { - "port": "Fa0/19", - "hostname": "switch1" - } - ], - "FastEthernet0/17": [ - { - "port": "480f.cf28.8a1b", - "hostname": "480f.cf28.8a1b" - } - ] + "FastEthernet0/24": [ + { + "port": "Fa0/19", + "hostname": "switch1" + } + ], + "FastEthernet0/17": [ + { + "port": "48:0F:CF:28:8A:1B", + "hostname": "48:0F:CF:28:8A:1B" + } + ] } diff --git a/test/ios/mocked_data/test_get_lldp_neighbors_detail/alternate/expected_result.json b/test/ios/mocked_data/test_get_lldp_neighbors_detail/alternate/expected_result.json index 7606d18be..cc13d84d8 100644 --- a/test/ios/mocked_data/test_get_lldp_neighbors_detail/alternate/expected_result.json +++ b/test/ios/mocked_data/test_get_lldp_neighbors_detail/alternate/expected_result.json @@ -1,24 +1,24 @@ { - "GigabitEthernet1": [ - { - "parent_interface": "", - "remote_chassis_id": "2cc2.603e.363b", - "remote_port": "Management1", - "remote_port_description": "", - "remote_system_capab": ["bridge", "router"], - "remote_system_description": "Arista Networks EOS version 4.15.2F running on an Arista Networks vEOS", - "remote_system_enable_capab": ["bridge", "router"], - "remote_system_name": "eos-spine1.ntc.com" - }, - { - "parent_interface": "", - "remote_chassis_id": "0005.8671.58c0", - "remote_port": "fxp0", - "remote_port_description": "fxp0", - "remote_system_capab": ["bridge", "router"], - "remote_system_description": "Juniper Networks, Inc. vmx internet router, kernel JUNOS 15.1F4.15, Build date: 2015-12-23 19:22:55 UTC Copyright (c) 1996-2015 Juniper Networks, Inc.", - "remote_system_enable_capab": ["bridge", "router"], - "remote_system_name": "vmx1" - } - ] + "GigabitEthernet1": [ + { + "parent_interface": "", + "remote_chassis_id": "2C:C2:60:3E:36:3B", + "remote_port": "Management1", + "remote_port_description": "", + "remote_system_capab": ["bridge", "router"], + "remote_system_description": "Arista Networks EOS version 4.15.2F running on an Arista Networks vEOS", + "remote_system_enable_capab": ["bridge", "router"], + "remote_system_name": "eos-spine1.ntc.com" + }, + { + "parent_interface": "", + "remote_chassis_id": "00:05:86:71:58:C0", + "remote_port": "fxp0", + "remote_port_description": "fxp0", + "remote_system_capab": ["bridge", "router"], + "remote_system_description": "Juniper Networks, Inc. vmx internet router, kernel JUNOS 15.1F4.15, Build date: 2015-12-23 19:22:55 UTC Copyright (c) 1996-2015 Juniper Networks, Inc.", + "remote_system_enable_capab": ["bridge", "router"], + "remote_system_name": "vmx1" + } + ] } diff --git a/test/ios/mocked_data/test_get_lldp_neighbors_detail/alternate2/expected_result.json b/test/ios/mocked_data/test_get_lldp_neighbors_detail/alternate2/expected_result.json index 26bb0f986..310c73d87 100644 --- a/test/ios/mocked_data/test_get_lldp_neighbors_detail/alternate2/expected_result.json +++ b/test/ios/mocked_data/test_get_lldp_neighbors_detail/alternate2/expected_result.json @@ -1,26 +1,26 @@ { - "FastEthernet0/24": [ - { - "parent_interface": "", - "remote_chassis_id": "a40c.c3c1.3980", - "remote_port": "Fa0/19", - "remote_port_description": "FastEthernet0/19", - "remote_system_capab": ["bridge"], - "remote_system_description": "Cisco IOS Software, C2960 Software (C2960-LANBASEK9-M), Version 15.0(1)SE2, RELEASE SOFTWARE (fc3)", - "remote_system_enable_capab": ["bridge"], - "remote_system_name": "switch1" - } - ], - "FastEthernet0/17": [ - { - "parent_interface": "", - "remote_chassis_id": "480f.cf28.8a1b", - "remote_port": "480f.cf28.8a1b", - "remote_port_description": "", - "remote_system_capab": [], - "remote_system_description": "", - "remote_system_enable_capab": [], - "remote_system_name": "" - } - ] + "FastEthernet0/24": [ + { + "parent_interface": "", + "remote_chassis_id": "A4:0C:C3:C1:39:80", + "remote_port": "Fa0/19", + "remote_port_description": "FastEthernet0/19", + "remote_system_capab": ["bridge"], + "remote_system_description": "Cisco IOS Software, C2960 Software (C2960-LANBASEK9-M), Version 15.0(1)SE2, RELEASE SOFTWARE (fc3)", + "remote_system_enable_capab": ["bridge"], + "remote_system_name": "switch1" + } + ], + "FastEthernet0/17": [ + { + "parent_interface": "", + "remote_chassis_id": "48:0F:CF:28:8A:1B", + "remote_port": "480f.cf28.8a1b", + "remote_port_description": "", + "remote_system_capab": [], + "remote_system_description": "", + "remote_system_enable_capab": [], + "remote_system_name": "" + } + ] } diff --git a/test/ios/mocked_data/test_get_lldp_neighbors_detail/alternate4/expected_result.json b/test/ios/mocked_data/test_get_lldp_neighbors_detail/alternate4/expected_result.json index 6093b9ef0..b2a265195 100644 --- a/test/ios/mocked_data/test_get_lldp_neighbors_detail/alternate4/expected_result.json +++ b/test/ios/mocked_data/test_get_lldp_neighbors_detail/alternate4/expected_result.json @@ -1,58 +1,58 @@ { - "GigabitEthernet1": [ - { - "parent_interface": "", - "remote_port": "Gi1", - "remote_port_description": "GigabitEthernet1", - "remote_chassis_id": "001e.bd3a.f900", - "remote_system_name": "csr9.bogus.com", - "remote_system_description": "Cisco IOS Software [Denali], CSR1000V Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.3.1, RELEASE SOFTWARE (fc3)", - "remote_system_capab": ["bridge", "router"], - "remote_system_enable_capab": ["router"] - }, - { - "parent_interface": "", - "remote_port": "Gi1", - "remote_port_description": "GigabitEthernet1", - "remote_chassis_id": "001e.bd60.ee00", - "remote_system_name": "csr8.bogus.com", - "remote_system_description": "Cisco IOS Software [Denali], CSR1000V Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.3.1, RELEASE SOFTWARE (fc3)", - "remote_system_capab": ["bridge", "router"], - "remote_system_enable_capab": ["router"] - }, - { - "parent_interface": "", - "remote_port": "Gi1", - "remote_port_description": "GigabitEthernet1", - "remote_chassis_id": "001e.e507.ed00", - "remote_system_name": "csr12.bogus.com", - "remote_system_description": "Cisco IOS Software [Denali], CSR1000V Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.3.1, RELEASE SOFTWARE (fc3)", - "remote_system_capab": ["bridge", "router"], - "remote_system_enable_capab": ["router"] - } - ], - "GigabitEthernet2": [ - { - "parent_interface": "", - "remote_port": "Gi2", - "remote_port_description": "GigabitEthernet2", - "remote_chassis_id": "001e.bd3a.f900", - "remote_system_name": "csr9.bogus.com", - "remote_system_description": "Cisco IOS Software [Denali], CSR1000V Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.3.1, RELEASE SOFTWARE (fc3)", - "remote_system_capab": ["bridge", "router"], - "remote_system_enable_capab": ["router"] - } - ], - "GigabitEthernet4": [ - { - "parent_interface": "", - "remote_port": "Gi4", - "remote_port_description": "GigabitEthernet4", - "remote_chassis_id": "001e.bd60.ee00", - "remote_system_name": "csr8.bogus.com", - "remote_system_description": "Cisco IOS Software [Denali], CSR1000V Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.3.1, RELEASE SOFTWARE (fc3)", - "remote_system_capab": ["bridge", "router"], - "remote_system_enable_capab": ["router"] - } - ] + "GigabitEthernet1": [ + { + "parent_interface": "", + "remote_port": "Gi1", + "remote_port_description": "GigabitEthernet1", + "remote_chassis_id": "00:1E:BD:3A:F9:00", + "remote_system_name": "csr9.bogus.com", + "remote_system_description": "Cisco IOS Software [Denali], CSR1000V Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.3.1, RELEASE SOFTWARE (fc3)", + "remote_system_capab": ["bridge", "router"], + "remote_system_enable_capab": ["router"] + }, + { + "parent_interface": "", + "remote_port": "Gi1", + "remote_port_description": "GigabitEthernet1", + "remote_chassis_id": "00:1E:BD:60:EE:00", + "remote_system_name": "csr8.bogus.com", + "remote_system_description": "Cisco IOS Software [Denali], CSR1000V Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.3.1, RELEASE SOFTWARE (fc3)", + "remote_system_capab": ["bridge", "router"], + "remote_system_enable_capab": ["router"] + }, + { + "parent_interface": "", + "remote_port": "Gi1", + "remote_port_description": "GigabitEthernet1", + "remote_chassis_id": "00:1E:E5:07:ED:00", + "remote_system_name": "csr12.bogus.com", + "remote_system_description": "Cisco IOS Software [Denali], CSR1000V Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.3.1, RELEASE SOFTWARE (fc3)", + "remote_system_capab": ["bridge", "router"], + "remote_system_enable_capab": ["router"] + } + ], + "GigabitEthernet2": [ + { + "parent_interface": "", + "remote_port": "Gi2", + "remote_port_description": "GigabitEthernet2", + "remote_chassis_id": "00:1E:BD:3A:F9:00", + "remote_system_name": "csr9.bogus.com", + "remote_system_description": "Cisco IOS Software [Denali], CSR1000V Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.3.1, RELEASE SOFTWARE (fc3)", + "remote_system_capab": ["bridge", "router"], + "remote_system_enable_capab": ["router"] + } + ], + "GigabitEthernet4": [ + { + "parent_interface": "", + "remote_port": "Gi4", + "remote_port_description": "GigabitEthernet4", + "remote_chassis_id": "00:1E:BD:60:EE:00", + "remote_system_name": "csr8.bogus.com", + "remote_system_description": "Cisco IOS Software [Denali], CSR1000V Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.3.1, RELEASE SOFTWARE (fc3)", + "remote_system_capab": ["bridge", "router"], + "remote_system_enable_capab": ["router"] + } + ] } diff --git a/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars/expected_result.json b/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars/expected_result.json index e3be9181a..1e4d13a9a 100644 --- a/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars/expected_result.json +++ b/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars/expected_result.json @@ -1,84 +1,84 @@ { - "GigabitEthernet1": [ - { - "parent_interface": "", - "remote_chassis_id": "0102.e2ff.fdb0", - "remote_port": "Te2/1/14", - "remote_port_description": "ABCD-U1AC04-SW1", - "remote_system_capab": ["bridge", "router"], - "remote_system_description": "Cisco IOS Software, IOS-XE Software, Catalyst 4500 L3 Switch Software (cat4500e-UNIVERSALK9-M), Version 03.06.05.E RELEASE SOFTWARE (fc2)", - "remote_system_enable_capab": ["bridge"], - "remote_system_name": "ABCD-U-DSW1.wan.acme.com" - }, - { - "parent_interface": "", - "remote_chassis_id": "0102.e2ff.fdb0", - "remote_port": "Te1/1/14", - "remote_port_description": "ABCD-U1AC04-SW1", - "remote_system_capab": ["bridge", "router"], - "remote_system_description": "Cisco IOS Software, IOS-XE Software, Catalyst 4500 L3 Switch Software (cat4500e-UNIVERSALK9-M), Version 03.06.05.E RELEASE SOFTWARE (fc2)", - "remote_system_enable_capab": ["bridge"], - "remote_system_name": "ABCD-U-DSW1.wan.acme.com" - }, - { - "parent_interface": "", - "remote_chassis_id": "hmbbkku-la", - "remote_port": "port-001", - "remote_port_description": "", - "remote_system_capab": ["station"], - "remote_system_description": "Hewlett-Packard HP ProBook 650 G1,A3112DD10303,5CG4543H13 + engineering", - "remote_system_enable_capab": ["station"], - "remote_system_name": "HMBBKKU-LA" - }, - { - "parent_interface": "", - "remote_chassis_id": "jniceel-la", - "remote_port": "port-001", - "remote_port_description": "", - "remote_system_capab": ["station"], - "remote_system_description": "HP HP ProBook 650 G2,,5CG64565JFL + engineering", - "remote_system_enable_capab": ["station"], - "remote_system_name": "JNICEEL-LA" - }, - { - "parent_interface": "", - "remote_chassis_id": "swaaaer-lta", - "remote_port": "port-001", - "remote_port_description": "", - "remote_system_capab": ["station"], - "remote_system_description": "Hewlett-Packard HP ProBook 6570b,A10112D102,5CB40117ZP + engineering", - "remote_system_enable_capab": ["station"], - "remote_system_name": "SWAAAER-LTA" - }, - { - "parent_interface": "", - "remote_chassis_id": "rberrrann-lta", - "remote_port": "port-001", - "remote_port_description": "", - "remote_system_capab": ["station"], - "remote_system_description": "Hewlett-Packard HP ProBook 650 G1,A3112DD11203,CNU444BMZ0 + engineering", - "remote_system_enable_capab": ["station"], - "remote_system_name": "RBERRRANN-LTA" - }, - { - "parent_interface": "", - "remote_chassis_id": "cr-443-2", - "remote_port": "port-001", - "remote_port_description": "Siemens, SIMATIC NET, CP 343-1, 6GK7 443-1EX50-0XE0 , HW: 7, FW: V3.0.23, Ethernet Port, X1 P1", - "remote_system_capab": ["station"], - "remote_system_description": "Siemens, SIMATIC NET, CP 443-1, 6GK7 443-1EX50-0XE0, HW: Version 7, FW: Version V3.0.23, VPH6201262", - "remote_system_enable_capab": ["station"], - "remote_system_name": "" - }, - { - "parent_interface": "", - "remote_port": "30e4.db01.2345", - "remote_port_description": "Port 1", - "remote_chassis_id": "192.168.2.44", - "remote_system_name": "Cisco IP Phone SPA504G", - "remote_system_description": "", - "remote_system_capab": ["bridge", "telephone"], - "remote_system_enable_capab": ["bridge", "telephone"] - } - ] + "GigabitEthernet1": [ + { + "parent_interface": "", + "remote_chassis_id": "01:02:E2:FF:FD:B0", + "remote_port": "Te2/1/14", + "remote_port_description": "ABCD-U1AC04-SW1", + "remote_system_capab": ["bridge", "router"], + "remote_system_description": "Cisco IOS Software, IOS-XE Software, Catalyst 4500 L3 Switch Software (cat4500e-UNIVERSALK9-M), Version 03.06.05.E RELEASE SOFTWARE (fc2)", + "remote_system_enable_capab": ["bridge"], + "remote_system_name": "ABCD-U-DSW1.wan.acme.com" + }, + { + "parent_interface": "", + "remote_chassis_id": "01:02:E2:FF:FD:B0", + "remote_port": "Te1/1/14", + "remote_port_description": "ABCD-U1AC04-SW1", + "remote_system_capab": ["bridge", "router"], + "remote_system_description": "Cisco IOS Software, IOS-XE Software, Catalyst 4500 L3 Switch Software (cat4500e-UNIVERSALK9-M), Version 03.06.05.E RELEASE SOFTWARE (fc2)", + "remote_system_enable_capab": ["bridge"], + "remote_system_name": "ABCD-U-DSW1.wan.acme.com" + }, + { + "parent_interface": "", + "remote_chassis_id": "hmbbkku-la", + "remote_port": "port-001", + "remote_port_description": "", + "remote_system_capab": ["station"], + "remote_system_description": "Hewlett-Packard HP ProBook 650 G1,A3112DD10303,5CG4543H13 + engineering", + "remote_system_enable_capab": ["station"], + "remote_system_name": "HMBBKKU-LA" + }, + { + "parent_interface": "", + "remote_chassis_id": "jniceel-la", + "remote_port": "port-001", + "remote_port_description": "", + "remote_system_capab": ["station"], + "remote_system_description": "HP HP ProBook 650 G2,,5CG64565JFL + engineering", + "remote_system_enable_capab": ["station"], + "remote_system_name": "JNICEEL-LA" + }, + { + "parent_interface": "", + "remote_chassis_id": "swaaaer-lta", + "remote_port": "port-001", + "remote_port_description": "", + "remote_system_capab": ["station"], + "remote_system_description": "Hewlett-Packard HP ProBook 6570b,A10112D102,5CB40117ZP + engineering", + "remote_system_enable_capab": ["station"], + "remote_system_name": "SWAAAER-LTA" + }, + { + "parent_interface": "", + "remote_chassis_id": "rberrrann-lta", + "remote_port": "port-001", + "remote_port_description": "", + "remote_system_capab": ["station"], + "remote_system_description": "Hewlett-Packard HP ProBook 650 G1,A3112DD11203,CNU444BMZ0 + engineering", + "remote_system_enable_capab": ["station"], + "remote_system_name": "RBERRRANN-LTA" + }, + { + "parent_interface": "", + "remote_chassis_id": "cr-443-2", + "remote_port": "port-001", + "remote_port_description": "Siemens, SIMATIC NET, CP 343-1, 6GK7 443-1EX50-0XE0 , HW: 7, FW: V3.0.23, Ethernet Port, X1 P1", + "remote_system_capab": ["station"], + "remote_system_description": "Siemens, SIMATIC NET, CP 443-1, 6GK7 443-1EX50-0XE0, HW: Version 7, FW: Version V3.0.23, VPH6201262", + "remote_system_enable_capab": ["station"], + "remote_system_name": "" + }, + { + "parent_interface": "", + "remote_port": "30e4.db01.2345", + "remote_port_description": "Port 1", + "remote_chassis_id": "192.168.2.44", + "remote_system_name": "Cisco IP Phone SPA504G", + "remote_system_description": "", + "remote_system_capab": ["bridge", "telephone"], + "remote_system_enable_capab": ["bridge", "telephone"] + } + ] } diff --git a/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars_space/expected_result.json b/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars_space/expected_result.json index a6ecb0dd8..14bea13ab 100644 --- a/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars_space/expected_result.json +++ b/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars_space/expected_result.json @@ -1,84 +1,84 @@ { - "GigabitEthernet1": [ - { - "parent_interface": "", - "remote_chassis_id": "0102.e2ff.fdb0", - "remote_port": "Te2/1/14", - "remote_port_description": "ABCD-U1AC04-SW1", - "remote_system_capab": ["bridge", "router"], - "remote_system_description": "Cisco IOS Software, IOS-XE Software, Catalyst 4500 L3 Switch Software (cat4500e-UNIVERSALK9-M), Version 03.06.05.E RELEASE SOFTWARE (fc2)", - "remote_system_enable_capab": ["bridge"], - "remote_system_name": "ABCD-U-DSW1.wan.acme.com" - }, - { - "parent_interface": "", - "remote_chassis_id": "0102.e2ff.fdb0", - "remote_port": "Te1/1/14", - "remote_port_description": "ABCD-U1AC04-SW1", - "remote_system_capab": ["bridge", "router"], - "remote_system_description": "Cisco IOS Software, IOS-XE Software, Catalyst 4500 L3 Switch Software (cat4500e-UNIVERSALK9-M), Version 03.06.05.E RELEASE SOFTWARE (fc2)", - "remote_system_enable_capab": ["bridge"], - "remote_system_name": "ABCD-U-DSW1.wan.acme.com" - }, - { - "parent_interface": "", - "remote_chassis_id": "hmbbkku-la", - "remote_port": "port-001", - "remote_port_description": "", - "remote_system_capab": ["station"], - "remote_system_description": "Hewlett-Packard HP ProBook 650 G1,A3112DD10303,5CG4543H13 + engineering", - "remote_system_enable_capab": ["station"], - "remote_system_name": "HMBBKKU-LA" - }, - { - "parent_interface": "", - "remote_chassis_id": "jniceel-la", - "remote_port": "port-001", - "remote_port_description": "", - "remote_system_capab": ["station"], - "remote_system_description": "HP HP ProBook 650 G2,,5CG64565JFL + engineering", - "remote_system_enable_capab": ["station"], - "remote_system_name": "JNICEEL-LA" - }, - { - "parent_interface": "", - "remote_chassis_id": "swaaaer-lta", - "remote_port": "port-001", - "remote_port_description": "", - "remote_system_capab": ["station"], - "remote_system_description": "Hewlett-Packard HP ProBook 6570b,A10112D102,5CB40117ZP + engineering", - "remote_system_enable_capab": ["station"], - "remote_system_name": "SWAAAER-LTA" - }, - { - "parent_interface": "", - "remote_chassis_id": "rberrrann-lta", - "remote_port": "port-001", - "remote_port_description": "", - "remote_system_capab": ["station"], - "remote_system_description": "Hewlett-Packard HP ProBook 650 G1,A3112DD11203,CNU444BMZ0 + engineering", - "remote_system_enable_capab": ["station"], - "remote_system_name": "RBERRRANN-LTA" - }, - { - "parent_interface": "", - "remote_chassis_id": "cr-443-2", - "remote_port": "port-001", - "remote_port_description": "Siemens, SIMATIC NET, CP 343-1, 6GK7 443-1EX50-0XE0 , HW: 7, FW: V3.0.23, Ethernet Port, X1 P1", - "remote_system_capab": ["station"], - "remote_system_description": "Siemens, SIMATIC NET, CP 443-1, 6GK7 443-1EX50-0XE0, HW: Version 7, FW: Version V3.0.23, VPH6201262", - "remote_system_enable_capab": ["station"], - "remote_system_name": "" - }, - { - "parent_interface": "", - "remote_port": "30e4.db01.2345", - "remote_port_description": "Port 1", - "remote_chassis_id": "192.168.2.44", - "remote_system_name": "Cisco IP Phone SPA5 4G", - "remote_system_description": "", - "remote_system_capab": ["bridge", "telephone"], - "remote_system_enable_capab": ["bridge", "telephone"] - } - ] + "GigabitEthernet1": [ + { + "parent_interface": "", + "remote_chassis_id": "01:02:E2:FF:FD:B0", + "remote_port": "Te2/1/14", + "remote_port_description": "ABCD-U1AC04-SW1", + "remote_system_capab": ["bridge", "router"], + "remote_system_description": "Cisco IOS Software, IOS-XE Software, Catalyst 4500 L3 Switch Software (cat4500e-UNIVERSALK9-M), Version 03.06.05.E RELEASE SOFTWARE (fc2)", + "remote_system_enable_capab": ["bridge"], + "remote_system_name": "ABCD-U-DSW1.wan.acme.com" + }, + { + "parent_interface": "", + "remote_chassis_id": "01:02:E2:FF:FD:B0", + "remote_port": "Te1/1/14", + "remote_port_description": "ABCD-U1AC04-SW1", + "remote_system_capab": ["bridge", "router"], + "remote_system_description": "Cisco IOS Software, IOS-XE Software, Catalyst 4500 L3 Switch Software (cat4500e-UNIVERSALK9-M), Version 03.06.05.E RELEASE SOFTWARE (fc2)", + "remote_system_enable_capab": ["bridge"], + "remote_system_name": "ABCD-U-DSW1.wan.acme.com" + }, + { + "parent_interface": "", + "remote_chassis_id": "hmbbkku-la", + "remote_port": "port-001", + "remote_port_description": "", + "remote_system_capab": ["station"], + "remote_system_description": "Hewlett-Packard HP ProBook 650 G1,A3112DD10303,5CG4543H13 + engineering", + "remote_system_enable_capab": ["station"], + "remote_system_name": "HMBBKKU-LA" + }, + { + "parent_interface": "", + "remote_chassis_id": "jniceel-la", + "remote_port": "port-001", + "remote_port_description": "", + "remote_system_capab": ["station"], + "remote_system_description": "HP HP ProBook 650 G2,,5CG64565JFL + engineering", + "remote_system_enable_capab": ["station"], + "remote_system_name": "JNICEEL-LA" + }, + { + "parent_interface": "", + "remote_chassis_id": "swaaaer-lta", + "remote_port": "port-001", + "remote_port_description": "", + "remote_system_capab": ["station"], + "remote_system_description": "Hewlett-Packard HP ProBook 6570b,A10112D102,5CB40117ZP + engineering", + "remote_system_enable_capab": ["station"], + "remote_system_name": "SWAAAER-LTA" + }, + { + "parent_interface": "", + "remote_chassis_id": "rberrrann-lta", + "remote_port": "port-001", + "remote_port_description": "", + "remote_system_capab": ["station"], + "remote_system_description": "Hewlett-Packard HP ProBook 650 G1,A3112DD11203,CNU444BMZ0 + engineering", + "remote_system_enable_capab": ["station"], + "remote_system_name": "RBERRRANN-LTA" + }, + { + "parent_interface": "", + "remote_chassis_id": "cr-443-2", + "remote_port": "port-001", + "remote_port_description": "Siemens, SIMATIC NET, CP 343-1, 6GK7 443-1EX50-0XE0 , HW: 7, FW: V3.0.23, Ethernet Port, X1 P1", + "remote_system_capab": ["station"], + "remote_system_description": "Siemens, SIMATIC NET, CP 443-1, 6GK7 443-1EX50-0XE0, HW: Version 7, FW: Version V3.0.23, VPH6201262", + "remote_system_enable_capab": ["station"], + "remote_system_name": "" + }, + { + "parent_interface": "", + "remote_port": "30e4.db01.2345", + "remote_port_description": "Port 1", + "remote_chassis_id": "192.168.2.44", + "remote_system_name": "Cisco IP Phone SPA5 4G", + "remote_system_description": "", + "remote_system_capab": ["bridge", "telephone"], + "remote_system_enable_capab": ["bridge", "telephone"] + } + ] } diff --git a/test/ios/mocked_data/test_get_lldp_neighbors_detail/missing_capability/expected_result.json b/test/ios/mocked_data/test_get_lldp_neighbors_detail/missing_capability/expected_result.json index 35a02e8b3..0d3b5a4bd 100644 --- a/test/ios/mocked_data/test_get_lldp_neighbors_detail/missing_capability/expected_result.json +++ b/test/ios/mocked_data/test_get_lldp_neighbors_detail/missing_capability/expected_result.json @@ -1,24 +1,24 @@ { - "GigabitEthernet1": [ - { - "parent_interface": "", - "remote_chassis_id": "00ae.3b11.1d00", - "remote_port": "Gi2/0", - "remote_port_description": "GigabitEthernet2/0", - "remote_system_capab": ["bridge", "router"], - "remote_system_description": "Cisco IOS Software, vios_l2 Software (vios_l2-ADVENTERPRISEK9-M), Version 15.2(4.0.55)E, TEST ENGINEERING ESTG_WEEKLY BUILD, synced to END_OF_FLO_ISP", - "remote_system_enable_capab": ["router"], - "remote_system_name": "SW2.cisco.com" - }, - { - "parent_interface": "", - "remote_chassis_id": "0050.56c0.0001", - "remote_port": "0050.56c0.0001", - "remote_port_description": "", - "remote_system_capab": [], - "remote_system_description": "", - "remote_system_enable_capab": [], - "remote_system_name": "" - } - ] + "GigabitEthernet1": [ + { + "parent_interface": "", + "remote_chassis_id": "00:AE:3B:11:1D:00", + "remote_port": "Gi2/0", + "remote_port_description": "GigabitEthernet2/0", + "remote_system_capab": ["bridge", "router"], + "remote_system_description": "Cisco IOS Software, vios_l2 Software (vios_l2-ADVENTERPRISEK9-M), Version 15.2(4.0.55)E, TEST ENGINEERING ESTG_WEEKLY BUILD, synced to END_OF_FLO_ISP", + "remote_system_enable_capab": ["router"], + "remote_system_name": "SW2.cisco.com" + }, + { + "parent_interface": "", + "remote_chassis_id": "00:50:56:C0:00:01", + "remote_port": "0050.56c0.0001", + "remote_port_description": "", + "remote_system_capab": [], + "remote_system_description": "", + "remote_system_enable_capab": [], + "remote_system_name": "" + } + ] } diff --git a/test/ios/mocked_data/test_get_lldp_neighbors_detail/normal/expected_result.json b/test/ios/mocked_data/test_get_lldp_neighbors_detail/normal/expected_result.json index 9bf0aa488..8b2d8721c 100644 --- a/test/ios/mocked_data/test_get_lldp_neighbors_detail/normal/expected_result.json +++ b/test/ios/mocked_data/test_get_lldp_neighbors_detail/normal/expected_result.json @@ -1,12 +1,14 @@ { - "FastEthernet4": [{ - "parent_interface": "", - "remote_system_capab": ["bridge"], - "remote_system_description": "ProCurve J9019A Switch 2510-24, revision Q.10.01, ROM Q.10.02 (/sw/code/build/harp(harp))", - "remote_port": "15", - "remote_chassis_id": "0018.fe1e.b020", - "remote_system_name": "twb-sf-hpsw1", - "remote_system_enable_capab": ["bridge"], - "remote_port_description": "15" - }] + "FastEthernet4": [ + { + "parent_interface": "", + "remote_system_capab": ["bridge"], + "remote_system_description": "ProCurve J9019A Switch 2510-24, revision Q.10.01, ROM Q.10.02 (/sw/code/build/harp(harp))", + "remote_port": "15", + "remote_chassis_id": "00:18:FE:1E:B0:20", + "remote_system_name": "twb-sf-hpsw1", + "remote_system_enable_capab": ["bridge"], + "remote_port_description": "15" + } + ] } diff --git a/test/ios/mocked_data/test_get_lldp_neighbors_detail/older_ios/expected_result.json b/test/ios/mocked_data/test_get_lldp_neighbors_detail/older_ios/expected_result.json index a50664efb..5e8a46ac8 100644 --- a/test/ios/mocked_data/test_get_lldp_neighbors_detail/older_ios/expected_result.json +++ b/test/ios/mocked_data/test_get_lldp_neighbors_detail/older_ios/expected_result.json @@ -1,26 +1,26 @@ { - "GigabitEthernet0/24": [ - { - "parent_interface": "", - "remote_chassis_id": "f4a7.39cd.2e40", - "remote_port": "ge-0/0/23", - "remote_port_description": "ptp to switch", - "remote_system_capab": ["bridge", "router"], - "remote_system_description": "Juniper Networks, Inc. ex3300-24p , version 12.3R12.4 Build date: 2016-01-20 05:03:06 UTC", - "remote_system_enable_capab": ["bridge", "router"], - "remote_system_name": "switch1" - } - ], - "GigabitEthernet0/1": [ - { - "parent_interface": "", - "remote_chassis_id": "a8b1.d41d.8680", - "remote_port": "Gi0/2", - "remote_port_description": "GigabitEthernet0/2", - "remote_system_capab": ["bridge"], - "remote_system_description": "Cisco IOS Software, C2960 Software (C2960-LANBASEK9-M), Version 15.0(1)SE2, RELEASE SOFTWARE (fc3)", - "remote_system_enable_capab": ["bridge"], - "remote_system_name": "switch2" - } - ] + "GigabitEthernet0/24": [ + { + "parent_interface": "", + "remote_chassis_id": "F4:A7:39:CD:2E:40", + "remote_port": "ge-0/0/23", + "remote_port_description": "ptp to switch", + "remote_system_capab": ["bridge", "router"], + "remote_system_description": "Juniper Networks, Inc. ex3300-24p , version 12.3R12.4 Build date: 2016-01-20 05:03:06 UTC", + "remote_system_enable_capab": ["bridge", "router"], + "remote_system_name": "switch1" + } + ], + "GigabitEthernet0/1": [ + { + "parent_interface": "", + "remote_chassis_id": "A8:B1:D4:1D:86:80", + "remote_port": "Gi0/2", + "remote_port_description": "GigabitEthernet0/2", + "remote_system_capab": ["bridge"], + "remote_system_description": "Cisco IOS Software, C2960 Software (C2960-LANBASEK9-M), Version 15.0(1)SE2, RELEASE SOFTWARE (fc3)", + "remote_system_enable_capab": ["bridge"], + "remote_system_name": "switch2" + } + ] } diff --git a/test/ios/mocked_data/test_traceroute/normal/expected_result.json b/test/ios/mocked_data/test_traceroute/normal/expected_result.json index 6a0cdbe7d..197367914 100644 --- a/test/ios/mocked_data/test_traceroute/normal/expected_result.json +++ b/test/ios/mocked_data/test_traceroute/normal/expected_result.json @@ -1 +1,175 @@ -{"success": {"1": {"probes": {"1": {"ip_address": "10.0.4.2", "rtt": 14.0, "host_name": "10.0.4.2"}, "2": {"ip_address": "10.0.4.2", "rtt": 11.0, "host_name": "10.0.4.2"}, "3": {"ip_address": "10.0.4.2", "rtt": 11.0, "host_name": "10.0.4.2"}}}, "2": {"probes": {"1": {"ip_address": "213.250.19.90", "rtt": 175.0, "host_name": "BSN-access.dynamic.siol.net"}, "2": {"ip_address": "213.250.19.90", "rtt": 157.0, "host_name": "BSN-access.dynamic.siol.net"}, "3": {"ip_address": "213.250.19.90", "rtt": 157.0, "host_name": "BSN-access.dynamic.siol.net"}}}, "3": {"probes": {"1": {"ip_address": "95.176.241.222", "rtt": 178.0, "host_name": "95.176.241.222"}, "2": {"ip_address": "95.176.241.222", "rtt": 266.0, "host_name": "95.176.241.222"}, "3": {"ip_address": "95.176.241.222", "rtt": 208.0, "host_name": "95.176.241.222"}}}, "4": {"probes": {"1": {"ip_address": "213.250.1.130", "rtt": 320.0, "host_name": "BSN-250-1-130.static.siol.net"}, "2": {"ip_address": "213.250.1.130", "rtt": 300.0, "host_name": "BSN-250-1-130.static.siol.net"}, "3": {"ip_address": "213.250.1.130", "rtt": 138.0, "host_name": "BSN-250-1-130.static.siol.net"}}}, "5": {"probes": {"1": {"ip_address": "209.85.248.115", "rtt": 122.0, "host_name": "BSN-209.85.248.115.static.siol.net"}, "2": {"ip_address": "209.85.248.217", "rtt": 157.0, "host_name": "209.85.248.217"}, "3": {"ip_address": "72.14.237.184", "rtt": 195.0, "host_name": "72.14.237.184"}}}, "6": {"probes": {"1": {"ip_address": "209.85.248.1", "rtt": 122.0, "host_name": "BSN-0.static.siol.net"}, "2": {"ip_address": "209.85.248.217", "rtt": 157.0, "host_name": "209.85.248.217"}, "3": {"ip_address": "209.85.248.217", "rtt": 195.0, "host_name": "209.85.248.217"}}}, "7": {"probes": {"1": {"ip_address": "", "rtt": 0.0, "host_name": ""}, "2": {"ip_address": "209.85.1.1", "rtt": 157.0, "host_name": "209.85.1.1"}, "3": {"ip_address": "209.85.1.1", "rtt": 195.0, "host_name": "209.85.1.1"}}}, "8": {"probes": {"1": {"ip_address": "", "rtt": 0.0, "host_name": ""}, "2": {"ip_address": "", "rtt": 0.0, "host_name": ""}, "3": {"ip_address": "", "rtt": 0.0, "host_name": ""}}}, "9": {"probes": {"1": {"ip_address": "8.8.8.8", "rtt": 213.0, "host_name": "google-public-dns-a.google.com"}, "2": {"ip_address": "8.8.8.8", "rtt": 210.0, "host_name": "google-public-dns-a.google.com"}, "3": {"ip_address": "8.8.8.8", "rtt": 197.0, "host_name": "google-public-dns-a.google.com"}}}}} +{ + "success": { + "1": { + "probes": { + "1": { + "ip_address": "10.0.4.2", + "rtt": 14, + "host_name": "10.0.4.2" + }, + "2": { + "ip_address": "10.0.4.2", + "rtt": 11, + "host_name": "10.0.4.2" + }, + "3": { + "ip_address": "10.0.4.2", + "rtt": 11, + "host_name": "10.0.4.2" + } + } + }, + "2": { + "probes": { + "1": { + "ip_address": "213.250.19.90", + "rtt": 175, + "host_name": "BSN-access.dynamic.siol.net" + }, + "2": { + "ip_address": "213.250.19.90", + "rtt": 157, + "host_name": "BSN-access.dynamic.siol.net" + }, + "3": { + "ip_address": "213.250.19.90", + "rtt": 157, + "host_name": "BSN-access.dynamic.siol.net" + } + } + }, + "3": { + "probes": { + "1": { + "ip_address": "95.176.241.222", + "rtt": 178, + "host_name": "95.176.241.222" + }, + "2": { + "ip_address": "95.176.241.222", + "rtt": 266, + "host_name": "95.176.241.222" + }, + "3": { + "ip_address": "95.176.241.222", + "rtt": 208, + "host_name": "95.176.241.222" + } + } + }, + "4": { + "probes": { + "1": { + "ip_address": "213.250.1.130", + "rtt": 320, + "host_name": "BSN-250-1-130.static.siol.net" + }, + "2": { + "ip_address": "213.250.1.130", + "rtt": 300, + "host_name": "BSN-250-1-130.static.siol.net" + }, + "3": { + "ip_address": "213.250.1.130", + "rtt": 138, + "host_name": "BSN-250-1-130.static.siol.net" + } + } + }, + "5": { + "probes": { + "1": { + "ip_address": "209.85.248.115", + "rtt": 122, + "host_name": "BSN-209.85.248.115.static.siol.net" + }, + "2": { + "ip_address": "209.85.248.217", + "rtt": 157, + "host_name": "209.85.248.217" + }, + "3": { + "ip_address": "72.14.237.184", + "rtt": 195, + "host_name": "72.14.237.184" + } + } + }, + "6": { + "probes": { + "1": { + "ip_address": "209.85.248.1", + "rtt": 122, + "host_name": "BSN-0.static.siol.net" + }, + "2": { + "ip_address": "209.85.248.217", + "rtt": 157, + "host_name": "209.85.248.217" + }, + "3": { + "ip_address": "209.85.248.217", + "rtt": 195, + "host_name": "209.85.248.217" + } + } + }, + "7": { + "probes": { + "1": { + "ip_address": "", + "rtt": 0, + "host_name": "" + }, + "2": { + "ip_address": "209.85.1.1", + "rtt": 157, + "host_name": "209.85.1.1" + }, + "3": { + "ip_address": "209.85.1.1", + "rtt": 195, + "host_name": "209.85.1.1" + } + } + }, + "8": { + "probes": { + "1": { + "ip_address": "", + "rtt": 0, + "host_name": "" + }, + "2": { + "ip_address": "", + "rtt": 0, + "host_name": "" + }, + "3": { + "ip_address": "", + "rtt": 0, + "host_name": "" + } + } + }, + "9": { + "probes": { + "1": { + "ip_address": "8.8.8.8", + "rtt": 213, + "host_name": "google-public-dns-a.google.com" + }, + "2": { + "ip_address": "8.8.8.8", + "rtt": 210, + "host_name": "google-public-dns-a.google.com" + }, + "3": { + "ip_address": "8.8.8.8", + "rtt": 197, + "host_name": "google-public-dns-a.google.com" + } + } + } + } +} From e4061c629ab71983cc388eaf3625359bf2737e5b Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Sat, 12 Feb 2022 22:13:57 +0100 Subject: [PATCH 62/74] Elaborate on assert statement supporting Mypy in convert helper function. --- napalm/base/helpers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index 0b4a13f72..bf605c333 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -340,7 +340,8 @@ def convert(to: Callable[[T], R], who: Optional[T], default: Optional[R] = None) f"Can't convert with callable {to} - no default is defined for this type." ) - # This is safe because the None-case if handled above + # This is safe because the None-case if handled above. This needs to be here because Mypy is + # unable to infer that 'default' is in fact not None based of the chained if-statements above. assert default is not None if who is None: From bae734adc0da0f6bbbe9c11bb889ec198101ae5c Mon Sep 17 00:00:00 2001 From: "Dr. X" Date: Sat, 12 Feb 2022 16:55:36 -0500 Subject: [PATCH 63/74] Update napalm/ios/ios.py Co-authored-by: Mircea Ulinic --- napalm/ios/ios.py | 1 - 1 file changed, 1 deletion(-) diff --git a/napalm/ios/ios.py b/napalm/ios/ios.py index ddfad3ed8..e0566efff 100644 --- a/napalm/ios/ios.py +++ b/napalm/ios/ios.py @@ -911,7 +911,6 @@ def get_lldp_neighbors_detail(self, interface=""): entry["remote_chassis_id"] = napalm.base.helpers.mac( entry["remote_chassis_id"] ) - print(entry["remote_chassis_id"]) # Older IOS versions don't have 'Local Intf' defined in LLDP detail. # We need to get them from the non-detailed command From e85c592e5cfcd0b1a827ae6bb59d58890f79772c Mon Sep 17 00:00:00 2001 From: Josh VanDeraa Date: Sat, 12 Feb 2022 16:30:12 -0600 Subject: [PATCH 64/74] Convert parse parents func to use netutils --- napalm/base/helpers.py | 34 ++++++++++++++++++++++++++-------- napalm/ios/ios.py | 4 ++-- requirements.txt | 2 +- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index bf605c333..105841fe5 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -12,11 +12,11 @@ # third party libs import jinja2 import textfsm -from ciscoconfparse import CiscoConfParse from lxml import etree from netaddr import EUI from netaddr import IPAddress from netaddr import mac_unix +from netutils.config.parser import IOSConfigParser # local modules import napalm.base.exceptions @@ -127,21 +127,39 @@ def load_template( return cls.load_merge_candidate(config=configuration) -def cisco_conf_parse_parents( +def netutils_parse_parents( parent: str, child: str, config: Union[str, List[str]] ) -> List[str]: """ - Use CiscoConfParse to find parent lines that contain a specific child line. + Use Netutils to find parent lines that contain a specific child line. :param parent: The parent line to search for :param child: The child line required under the given parent :param config: The device running/startup config """ - if type(config) == str: - config = config.splitlines() # type: ignore - parse = CiscoConfParse(config) - cfg_obj = parse.find_parents_w_child(parent, child) - return cfg_obj + # Config tree is the entire configuration in a tree format, + # followed by getting the individual lines that has the formats: + # ConfigLine(config_line=' ip address 192.0.2.10 255.255.255.0', parents=('interface GigabitEthernet1',)) + # ConfigLine(config_line='Current configuration : 1624 bytes', parents=()) + config_tree = IOSConfigParser(str(config)) + configuration_lines = config_tree.build_config_relationship() + + # Return config is the list that will be returned + return_config = [] + + # Loop over each of the configuration lines + for line in configuration_lines: + # Loop over any line that has a parent line. If there are no parents for a line item then + # the parents is an empty tuple. + for parent_line in line.parents: + if ( + child in line.config_line + and re.match(parent, parent_line) is not None + and parent_line not in return_config + ): + return_config.append(parent_line) + + return return_config def cisco_conf_parse_objects( diff --git a/napalm/ios/ios.py b/napalm/ios/ios.py index 6bbdc8299..5c4f413cd 100644 --- a/napalm/ios/ios.py +++ b/napalm/ios/ios.py @@ -1351,7 +1351,7 @@ def build_prefix_limit(af_table, limit, prefix_percent, prefix_timeout): if neighbor: if bgp_neighbor != neighbor: continue - afi_list = napalm.base.helpers.cisco_conf_parse_parents( + afi_list = napalm.base.helpers.netutils_parse_parents( r"\s+address-family.*", bgp_neighbor, bgp_config_text ) try: @@ -1458,7 +1458,7 @@ def build_prefix_limit(af_table, limit, prefix_percent, prefix_timeout): group_name, bgp_config_text ) multipath = False - afi_list = napalm.base.helpers.cisco_conf_parse_parents( + afi_list = napalm.base.helpers.netutils_parse_parents( r"\s+address-family.*", group_name, neighbor_config ) for afi in afi_list: diff --git a/requirements.txt b/requirements.txt index 539088f0d..e937ad0e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ pyYAML pyeapi>=0.8.2 netmiko>=3.3.0,<4.0.0 junos-eznc>=2.2.1 -ciscoconfparse scp lxml>=4.3.0 ncclient +netutils>=1.0.0 From 86c86119e4f7dbe17a5b1cdac11b8cfd3d0386b1 Mon Sep 17 00:00:00 2001 From: Josh VanDeraa Date: Sat, 12 Feb 2022 16:34:04 -0600 Subject: [PATCH 65/74] Convert parse objects to use netutils --- napalm/base/helpers.py | 30 +++++++++++++++++++++--------- napalm/ios/ios.py | 8 ++++---- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index 105841fe5..bca1378e7 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -162,7 +162,7 @@ def netutils_parse_parents( return return_config -def cisco_conf_parse_objects( +def netutils_parse_objects( cfg_section: str, config: Union[str, List[str]] ) -> List[str]: """ @@ -172,15 +172,27 @@ def cisco_conf_parse_objects( :param cfg_section: The section of the config to return eg. "router bgp" :param config: The running/startup config of the device to parse """ + # Config tree is the entire configuration in a tree format, + # followed by getting the individual lines that has the formats: + # ConfigLine(config_line=' ip address 192.0.2.10 255.255.255.0', parents=('interface GigabitEthernet1',)) + # ConfigLine(config_line='Current configuration : 1624 bytes', parents=()) + config_tree = IOSConfigParser(str(config)) + lines = config_tree.build_config_relationship() + + # Return config is the list that will be returned return_config = [] - if type(config) is str: - config = config.splitlines() # type: ignore - parse = CiscoConfParse(config) - cfg_obj = parse.find_objects(cfg_section) - for parent in cfg_obj: - return_config.append(parent.text) - for child in parent.all_children: - return_config.append(child.text) + for line in lines: + # The parent configuration is expected on the function that this is replacing, + # add the parent line to the base of the return_config + if cfg_section in line.config_line: + return_config.append(line.config_line) + # Check if the tuple is greater than 0 + if len(line.parents) > 0: + # Check the eldest parent, if that is part of the config section, then append + # the current line being checked to it. + if cfg_section in line.parents[0]: + return_config.append(line.config_line) + return return_config diff --git a/napalm/ios/ios.py b/napalm/ios/ios.py index 5c4f413cd..8d950efc8 100644 --- a/napalm/ios/ios.py +++ b/napalm/ios/ios.py @@ -1323,7 +1323,7 @@ def build_prefix_limit(af_table, limit, prefix_percent, prefix_timeout): # Get BGP config using ciscoconfparse because some old devices dont support "| sec bgp" cfg = self.get_config(retrieve="running") cfg = cfg["running"].splitlines() - bgp_config_text = napalm.base.helpers.cisco_conf_parse_objects( + bgp_config_text = napalm.base.helpers.netutils_parse_objects( "router bgp", cfg ) bgp_asn = napalm.base.helpers.regex_find_txt( @@ -1363,7 +1363,7 @@ def build_prefix_limit(af_table, limit, prefix_percent, prefix_timeout): if "vrf" in str(afi_list): continue else: - neighbor_config = napalm.base.helpers.cisco_conf_parse_objects( + neighbor_config = napalm.base.helpers.netutils_parse_objects( bgp_neighbor, bgp_config_text ) # For group_name- use peer-group name, else VRF name, else "_" for no group @@ -1454,7 +1454,7 @@ def build_prefix_limit(af_table, limit, prefix_percent, prefix_timeout): "neighbors": bgp_group_neighbors.get("_", {}), } continue - neighbor_config = napalm.base.helpers.cisco_conf_parse_objects( + neighbor_config = napalm.base.helpers.netutils_parse_objects( group_name, bgp_config_text ) multipath = False @@ -1462,7 +1462,7 @@ def build_prefix_limit(af_table, limit, prefix_percent, prefix_timeout): r"\s+address-family.*", group_name, neighbor_config ) for afi in afi_list: - afi_config = napalm.base.helpers.cisco_conf_parse_objects( + afi_config = napalm.base.helpers.netutils_parse_objects( afi, bgp_config_text ) multipath = bool( From 9acde2a91dfc9a55c851ee69f299ec32f49442a8 Mon Sep 17 00:00:00 2001 From: Josh VanDeraa Date: Sat, 12 Feb 2022 16:35:46 -0600 Subject: [PATCH 66/74] Remove confparse from mypy, update comments --- mypy.ini | 2 -- napalm/base/helpers.py | 2 +- napalm/ios/ios.py | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/mypy.ini b/mypy.ini index 9819a29a8..e3c4accc6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -41,8 +41,6 @@ ignore_missing_imports = True [mypy-lxml.*] ignore_missing_imports = True -[mypy-ciscoconfparse] -ignore_missing_imports = True [mypy-textfsm] ignore_missing_imports = True diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index bca1378e7..a51d40d96 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -166,7 +166,7 @@ def netutils_parse_objects( cfg_section: str, config: Union[str, List[str]] ) -> List[str]: """ - Use CiscoConfParse to find and return a section of Cisco IOS config. + Use Netutils to find and return a section of Cisco IOS config. Similar to "show run | section " :param cfg_section: The section of the config to return eg. "router bgp" diff --git a/napalm/ios/ios.py b/napalm/ios/ios.py index 8d950efc8..7bb761533 100644 --- a/napalm/ios/ios.py +++ b/napalm/ios/ios.py @@ -1320,7 +1320,7 @@ def build_prefix_limit(af_table, limit, prefix_percent, prefix_timeout): } return prefix_limit - # Get BGP config using ciscoconfparse because some old devices dont support "| sec bgp" + # Get BGP config using netutils because some old devices dont support "| sec bgp" cfg = self.get_config(retrieve="running") cfg = cfg["running"].splitlines() bgp_config_text = napalm.base.helpers.netutils_parse_objects( From 70fc79e76e5f96cc202279615b7951131d5ddd7a Mon Sep 17 00:00:00 2001 From: Josh VanDeraa Date: Sat, 12 Feb 2022 17:11:40 -0600 Subject: [PATCH 67/74] Code format update --- napalm/ios/ios.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/napalm/ios/ios.py b/napalm/ios/ios.py index 7bb761533..76a8a63d4 100644 --- a/napalm/ios/ios.py +++ b/napalm/ios/ios.py @@ -1323,9 +1323,7 @@ def build_prefix_limit(af_table, limit, prefix_percent, prefix_timeout): # Get BGP config using netutils because some old devices dont support "| sec bgp" cfg = self.get_config(retrieve="running") cfg = cfg["running"].splitlines() - bgp_config_text = napalm.base.helpers.netutils_parse_objects( - "router bgp", cfg - ) + bgp_config_text = napalm.base.helpers.netutils_parse_objects("router bgp", cfg) bgp_asn = napalm.base.helpers.regex_find_txt( r"router bgp (\d+)", bgp_config_text, default=0 ) From 864da803abe47515b7b66bc7dce322eeff8ed410 Mon Sep 17 00:00:00 2001 From: Xenia Mountrouidou Date: Sun, 13 Feb 2022 08:53:35 -0500 Subject: [PATCH 68/74] change chassis id to convert addressing Mircea's suggestion --- napalm/ios/ios.py | 11 ++++------- .../device_id_20chars/expected_result.json | 12 ++++++------ .../device_id_20chars_space/expected_result.json | 12 ++++++------ 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/napalm/ios/ios.py b/napalm/ios/ios.py index e0566efff..fd201f0d1 100644 --- a/napalm/ios/ios.py +++ b/napalm/ios/ios.py @@ -903,14 +903,11 @@ def get_lldp_neighbors_detail(self, interface=""): if len(lldp_entries) == 0: return {} - # format chassis_id for consistency, if it is a mac address + # format chassis_id for consistency for entry in lldp_entries: - if re.search( - r"^(\d|\w){4}.(\d|\w){4}.(\d|\w){4}$", entry["remote_chassis_id"] - ): - entry["remote_chassis_id"] = napalm.base.helpers.mac( - entry["remote_chassis_id"] - ) + entry["remote_chassis_id"] = napalm.base.helpers.convert( + napalm.base.helpers.mac, entry["remote_chassis_id"] + ) # Older IOS versions don't have 'Local Intf' defined in LLDP detail. # We need to get them from the non-detailed command diff --git a/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars/expected_result.json b/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars/expected_result.json index 1e4d13a9a..afce5103a 100644 --- a/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars/expected_result.json +++ b/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars/expected_result.json @@ -22,7 +22,7 @@ }, { "parent_interface": "", - "remote_chassis_id": "hmbbkku-la", + "remote_chassis_id": "", "remote_port": "port-001", "remote_port_description": "", "remote_system_capab": ["station"], @@ -32,7 +32,7 @@ }, { "parent_interface": "", - "remote_chassis_id": "jniceel-la", + "remote_chassis_id": "", "remote_port": "port-001", "remote_port_description": "", "remote_system_capab": ["station"], @@ -42,7 +42,7 @@ }, { "parent_interface": "", - "remote_chassis_id": "swaaaer-lta", + "remote_chassis_id": "", "remote_port": "port-001", "remote_port_description": "", "remote_system_capab": ["station"], @@ -52,7 +52,7 @@ }, { "parent_interface": "", - "remote_chassis_id": "rberrrann-lta", + "remote_chassis_id": "", "remote_port": "port-001", "remote_port_description": "", "remote_system_capab": ["station"], @@ -62,7 +62,7 @@ }, { "parent_interface": "", - "remote_chassis_id": "cr-443-2", + "remote_chassis_id": "", "remote_port": "port-001", "remote_port_description": "Siemens, SIMATIC NET, CP 343-1, 6GK7 443-1EX50-0XE0 , HW: 7, FW: V3.0.23, Ethernet Port, X1 P1", "remote_system_capab": ["station"], @@ -74,7 +74,7 @@ "parent_interface": "", "remote_port": "30e4.db01.2345", "remote_port_description": "Port 1", - "remote_chassis_id": "192.168.2.44", + "remote_chassis_id": "", "remote_system_name": "Cisco IP Phone SPA504G", "remote_system_description": "", "remote_system_capab": ["bridge", "telephone"], diff --git a/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars_space/expected_result.json b/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars_space/expected_result.json index 14bea13ab..724bd3e98 100644 --- a/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars_space/expected_result.json +++ b/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars_space/expected_result.json @@ -22,7 +22,7 @@ }, { "parent_interface": "", - "remote_chassis_id": "hmbbkku-la", + "remote_chassis_id": "", "remote_port": "port-001", "remote_port_description": "", "remote_system_capab": ["station"], @@ -32,7 +32,7 @@ }, { "parent_interface": "", - "remote_chassis_id": "jniceel-la", + "remote_chassis_id": "", "remote_port": "port-001", "remote_port_description": "", "remote_system_capab": ["station"], @@ -42,7 +42,7 @@ }, { "parent_interface": "", - "remote_chassis_id": "swaaaer-lta", + "remote_chassis_id": "", "remote_port": "port-001", "remote_port_description": "", "remote_system_capab": ["station"], @@ -52,7 +52,7 @@ }, { "parent_interface": "", - "remote_chassis_id": "rberrrann-lta", + "remote_chassis_id": "", "remote_port": "port-001", "remote_port_description": "", "remote_system_capab": ["station"], @@ -62,7 +62,7 @@ }, { "parent_interface": "", - "remote_chassis_id": "cr-443-2", + "remote_chassis_id": "", "remote_port": "port-001", "remote_port_description": "Siemens, SIMATIC NET, CP 343-1, 6GK7 443-1EX50-0XE0 , HW: 7, FW: V3.0.23, Ethernet Port, X1 P1", "remote_system_capab": ["station"], @@ -74,7 +74,7 @@ "parent_interface": "", "remote_port": "30e4.db01.2345", "remote_port_description": "Port 1", - "remote_chassis_id": "192.168.2.44", + "remote_chassis_id": "", "remote_system_name": "Cisco IP Phone SPA5 4G", "remote_system_description": "", "remote_system_capab": ["bridge", "telephone"], From 6738b5833a919a9b63e2d3ac362225f45f210660 Mon Sep 17 00:00:00 2001 From: Josh VanDeraa Date: Sun, 13 Feb 2022 10:35:15 -0600 Subject: [PATCH 69/74] Updates to support lists passed in, moves afi to general search --- mypy.ini | 2 ++ napalm/base/helpers.py | 18 ++++++++++++++++-- napalm/ios/ios.py | 19 ++++++++++--------- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/mypy.ini b/mypy.ini index e3c4accc6..6260851d7 100644 --- a/mypy.ini +++ b/mypy.ini @@ -41,6 +41,8 @@ ignore_missing_imports = True [mypy-lxml.*] ignore_missing_imports = True +[mypy-netutils.*] +ignore_missing_imports = True [mypy-textfsm] ignore_missing_imports = True diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index a51d40d96..1e8225ad3 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -137,9 +137,17 @@ def netutils_parse_parents( :param child: The child line required under the given parent :param config: The device running/startup config """ + # Check if the config is a list, if it is a list, then join it to make a string. + logger.error(config) + if isinstance(config, list): + config = "\n".join(config) + config = config + "\n" + logger.error(config) + # Config tree is the entire configuration in a tree format, # followed by getting the individual lines that has the formats: - # ConfigLine(config_line=' ip address 192.0.2.10 255.255.255.0', parents=('interface GigabitEthernet1',)) + # ConfigLine(config_line=' ip address 192.0.2.10 255.255.255.0', + # parents=('interface GigabitEthernet1',)) # ConfigLine(config_line='Current configuration : 1624 bytes', parents=()) config_tree = IOSConfigParser(str(config)) configuration_lines = config_tree.build_config_relationship() @@ -172,9 +180,15 @@ def netutils_parse_objects( :param cfg_section: The section of the config to return eg. "router bgp" :param config: The running/startup config of the device to parse """ + # Check if the config is a list, if it is a list, then join it to make a string. + if isinstance(config, list): + config = "\n".join(config) + config = config + "\n" + # Config tree is the entire configuration in a tree format, # followed by getting the individual lines that has the formats: - # ConfigLine(config_line=' ip address 192.0.2.10 255.255.255.0', parents=('interface GigabitEthernet1',)) + # ConfigLine(config_line=' ip address 192.0.2.10 255.255.255.0', + # parents=('interface GigabitEthernet1',)) # ConfigLine(config_line='Current configuration : 1624 bytes', parents=()) config_tree = IOSConfigParser(str(config)) lines = config_tree.build_config_relationship() diff --git a/napalm/ios/ios.py b/napalm/ios/ios.py index 76a8a63d4..197ad7465 100644 --- a/napalm/ios/ios.py +++ b/napalm/ios/ios.py @@ -1322,17 +1322,18 @@ def build_prefix_limit(af_table, limit, prefix_percent, prefix_timeout): # Get BGP config using netutils because some old devices dont support "| sec bgp" cfg = self.get_config(retrieve="running") - cfg = cfg["running"].splitlines() - bgp_config_text = napalm.base.helpers.netutils_parse_objects("router bgp", cfg) + bgp_config_list = napalm.base.helpers.netutils_parse_objects( + "router bgp", cfg["running"] + ) bgp_asn = napalm.base.helpers.regex_find_txt( - r"router bgp (\d+)", bgp_config_text, default=0 + r"router bgp (\d+)", bgp_config_list, default=0 ) # Get a list of all neighbors and groups in the config all_neighbors = set() all_groups = set() bgp_group_neighbors = {} all_groups.add("_") - for line in bgp_config_text: + for line in bgp_config_list: if " neighbor " in line: if re.search(IP_ADDR_REGEX, line) is not None: all_neighbors.add(re.search(IP_ADDR_REGEX, line).group()) @@ -1350,7 +1351,7 @@ def build_prefix_limit(af_table, limit, prefix_percent, prefix_timeout): if bgp_neighbor != neighbor: continue afi_list = napalm.base.helpers.netutils_parse_parents( - r"\s+address-family.*", bgp_neighbor, bgp_config_text + r"\s+address-family.*", bgp_neighbor, bgp_config_list ) try: afi = afi_list[0] @@ -1362,7 +1363,7 @@ def build_prefix_limit(af_table, limit, prefix_percent, prefix_timeout): continue else: neighbor_config = napalm.base.helpers.netutils_parse_objects( - bgp_neighbor, bgp_config_text + bgp_neighbor, bgp_config_list ) # For group_name- use peer-group name, else VRF name, else "_" for no group group_name = napalm.base.helpers.regex_find_txt( @@ -1453,15 +1454,15 @@ def build_prefix_limit(af_table, limit, prefix_percent, prefix_timeout): } continue neighbor_config = napalm.base.helpers.netutils_parse_objects( - group_name, bgp_config_text + group_name, bgp_config_list ) multipath = False afi_list = napalm.base.helpers.netutils_parse_parents( - r"\s+address-family.*", group_name, neighbor_config + r"\s+address-family.*", group_name, bgp_config_list ) for afi in afi_list: afi_config = napalm.base.helpers.netutils_parse_objects( - afi, bgp_config_text + afi, bgp_config_list ) multipath = bool( napalm.base.helpers.regex_find_txt(r" multipath", str(afi_config)) From 1ef34deaf12c7b5d35f2f6e3677d29a0ceec9c7f Mon Sep 17 00:00:00 2001 From: Xenia Mountrouidou Date: Sun, 13 Feb 2022 12:51:27 -0500 Subject: [PATCH 70/74] add default option to convert --- napalm/ios/ios.py | 4 +++- .../device_id_20chars/expected_result.json | 12 ++++++------ .../device_id_20chars_space/expected_result.json | 12 ++++++------ 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/napalm/ios/ios.py b/napalm/ios/ios.py index fd201f0d1..00f52d4da 100644 --- a/napalm/ios/ios.py +++ b/napalm/ios/ios.py @@ -906,7 +906,9 @@ def get_lldp_neighbors_detail(self, interface=""): # format chassis_id for consistency for entry in lldp_entries: entry["remote_chassis_id"] = napalm.base.helpers.convert( - napalm.base.helpers.mac, entry["remote_chassis_id"] + napalm.base.helpers.mac, + entry["remote_chassis_id"], + entry["remote_chassis_id"], ) # Older IOS versions don't have 'Local Intf' defined in LLDP detail. diff --git a/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars/expected_result.json b/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars/expected_result.json index afce5103a..1e4d13a9a 100644 --- a/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars/expected_result.json +++ b/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars/expected_result.json @@ -22,7 +22,7 @@ }, { "parent_interface": "", - "remote_chassis_id": "", + "remote_chassis_id": "hmbbkku-la", "remote_port": "port-001", "remote_port_description": "", "remote_system_capab": ["station"], @@ -32,7 +32,7 @@ }, { "parent_interface": "", - "remote_chassis_id": "", + "remote_chassis_id": "jniceel-la", "remote_port": "port-001", "remote_port_description": "", "remote_system_capab": ["station"], @@ -42,7 +42,7 @@ }, { "parent_interface": "", - "remote_chassis_id": "", + "remote_chassis_id": "swaaaer-lta", "remote_port": "port-001", "remote_port_description": "", "remote_system_capab": ["station"], @@ -52,7 +52,7 @@ }, { "parent_interface": "", - "remote_chassis_id": "", + "remote_chassis_id": "rberrrann-lta", "remote_port": "port-001", "remote_port_description": "", "remote_system_capab": ["station"], @@ -62,7 +62,7 @@ }, { "parent_interface": "", - "remote_chassis_id": "", + "remote_chassis_id": "cr-443-2", "remote_port": "port-001", "remote_port_description": "Siemens, SIMATIC NET, CP 343-1, 6GK7 443-1EX50-0XE0 , HW: 7, FW: V3.0.23, Ethernet Port, X1 P1", "remote_system_capab": ["station"], @@ -74,7 +74,7 @@ "parent_interface": "", "remote_port": "30e4.db01.2345", "remote_port_description": "Port 1", - "remote_chassis_id": "", + "remote_chassis_id": "192.168.2.44", "remote_system_name": "Cisco IP Phone SPA504G", "remote_system_description": "", "remote_system_capab": ["bridge", "telephone"], diff --git a/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars_space/expected_result.json b/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars_space/expected_result.json index 724bd3e98..14bea13ab 100644 --- a/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars_space/expected_result.json +++ b/test/ios/mocked_data/test_get_lldp_neighbors_detail/device_id_20chars_space/expected_result.json @@ -22,7 +22,7 @@ }, { "parent_interface": "", - "remote_chassis_id": "", + "remote_chassis_id": "hmbbkku-la", "remote_port": "port-001", "remote_port_description": "", "remote_system_capab": ["station"], @@ -32,7 +32,7 @@ }, { "parent_interface": "", - "remote_chassis_id": "", + "remote_chassis_id": "jniceel-la", "remote_port": "port-001", "remote_port_description": "", "remote_system_capab": ["station"], @@ -42,7 +42,7 @@ }, { "parent_interface": "", - "remote_chassis_id": "", + "remote_chassis_id": "swaaaer-lta", "remote_port": "port-001", "remote_port_description": "", "remote_system_capab": ["station"], @@ -52,7 +52,7 @@ }, { "parent_interface": "", - "remote_chassis_id": "", + "remote_chassis_id": "rberrrann-lta", "remote_port": "port-001", "remote_port_description": "", "remote_system_capab": ["station"], @@ -62,7 +62,7 @@ }, { "parent_interface": "", - "remote_chassis_id": "", + "remote_chassis_id": "cr-443-2", "remote_port": "port-001", "remote_port_description": "Siemens, SIMATIC NET, CP 343-1, 6GK7 443-1EX50-0XE0 , HW: 7, FW: V3.0.23, Ethernet Port, X1 P1", "remote_system_capab": ["station"], @@ -74,7 +74,7 @@ "parent_interface": "", "remote_port": "30e4.db01.2345", "remote_port_description": "Port 1", - "remote_chassis_id": "", + "remote_chassis_id": "192.168.2.44", "remote_system_name": "Cisco IP Phone SPA5 4G", "remote_system_description": "", "remote_system_capab": ["bridge", "telephone"], From 58e72c717211631b14f160dfd5bc2f4fa4a71445 Mon Sep 17 00:00:00 2001 From: Josh VanDeraa Date: Sun, 13 Feb 2022 13:08:15 -0600 Subject: [PATCH 71/74] Updates to remove unnecessary logging. --- napalm/base/helpers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index 1e8225ad3..56a63da1f 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -138,11 +138,9 @@ def netutils_parse_parents( :param config: The device running/startup config """ # Check if the config is a list, if it is a list, then join it to make a string. - logger.error(config) if isinstance(config, list): config = "\n".join(config) config = config + "\n" - logger.error(config) # Config tree is the entire configuration in a tree format, # followed by getting the individual lines that has the formats: From 50aa4289c3184c7e9124baf2386acb42e6801c08 Mon Sep 17 00:00:00 2001 From: Mircea Ulinic Date: Sun, 13 Feb 2022 19:06:11 +0000 Subject: [PATCH 72/74] Fix #1549: Check peer admin status from the BGP options To avoid an additional RPC request to check the actual configuration, we can probably use the Shutdown flag under the BGP extended options: ```xml Preference LocalAddress AddressFamily PeerAS Multipath Refresh VpnApplyExport PeerSpecficLoopsAllowed GracefulShutdownRcv Shutdown ``` This should be a more reliable way to check the admin status, instead of incorrectly deducing the admin state from the operational state. --- napalm/junos/junos.py | 7 +++++- napalm/junos/utils/junos_views.yml | 2 +- .../normal/expected_result.json | 21 ++++++++++++++++++ .../get-bgp-neighbor-informationfrontend.xml | 22 +++++++++++++++++++ .../get-bgp-summary-informationfrontend.xml | 10 +++++++++ 5 files changed, 60 insertions(+), 2 deletions(-) diff --git a/napalm/junos/junos.py b/napalm/junos/junos.py index 7d3b4204e..a2de18ad5 100644 --- a/napalm/junos/junos.py +++ b/napalm/junos/junos.py @@ -765,7 +765,7 @@ def get_bgp_neighbors(self): "remote_as": 0, "remote_id": "", "is_up": False, - "is_enabled": False, + "is_enabled": True, "description": "", "uptime": 0, "address_family": {}, @@ -828,6 +828,11 @@ def _get_bgp_neighbors_core( for key, value in neighbor_details.items() if key in keys } + bgp_opts = ( + neighbor_details.pop("bgp_options_extended", "").lower().split() + ) + if "shutdown" in bgp_opts: + peer["is_enabled"] = False peer["local_as"] = napalm.base.helpers.as_number(peer["local_as"]) peer["remote_as"] = napalm.base.helpers.as_number(peer["remote_as"]) peer["address_family"] = self._parse_route_stats( diff --git a/napalm/junos/utils/junos_views.yml b/napalm/junos/utils/junos_views.yml index d134ab712..5d81ae2fc 100644 --- a/napalm/junos/utils/junos_views.yml +++ b/napalm/junos/utils/junos_views.yml @@ -80,7 +80,6 @@ junos_bgp_view: description: description peer_fwd_rti: peer-fwd-rti is_up: { peer-state: True=Established } - is_enabled: { peer-state: False=True } received_prefixes: { bgp-rib/received-prefix-count: int } accepted_prefixes: { bgp-rib/accepted-prefix-count: int } sent_prefixes: { bgp-rib/advertised-prefix-count: int } @@ -90,6 +89,7 @@ junos_bgp_view: peer_as: { peer-as: int } local_id: local-id remote_id: { peer-id: unicode } + bgp_options_extended: { bgp-option-information/bgp-options-extended: str } #### #### LLDP table diff --git a/test/junos/mocked_data/test_get_bgp_neighbors/normal/expected_result.json b/test/junos/mocked_data/test_get_bgp_neighbors/normal/expected_result.json index 0f3107c3e..2a8f67f5b 100644 --- a/test/junos/mocked_data/test_get_bgp_neighbors/normal/expected_result.json +++ b/test/junos/mocked_data/test_get_bgp_neighbors/normal/expected_result.json @@ -174,6 +174,27 @@ "local_as": 20001, "is_enabled": true, "uptime": 696506 + }, + "192.169.1.2": { + "description": "", + "remote_id": "", + "address_family": { + "ipv6": { + "received_prefixes": -1, + "sent_prefixes": -1, + "accepted_prefixes": -1 + }, + "ipv4": { + "received_prefixes": -1, + "sent_prefixes": -1, + "accepted_prefixes": -1 + } + }, + "remote_as": 20000, + "is_up": false, + "local_as": 20001, + "is_enabled": false, + "uptime": 696506 } } } diff --git a/test/junos/mocked_data/test_get_bgp_neighbors/normal/get-bgp-neighbor-informationfrontend.xml b/test/junos/mocked_data/test_get_bgp_neighbors/normal/get-bgp-neighbor-informationfrontend.xml index 39de04a93..c06b3ea7e 100644 --- a/test/junos/mocked_data/test_get_bgp_neighbors/normal/get-bgp-neighbor-informationfrontend.xml +++ b/test/junos/mocked_data/test_get_bgp_neighbors/normal/get-bgp-neighbor-informationfrontend.xml @@ -250,6 +250,28 @@ 0 + + 192.169.1.2 + 20000 + 192.169.1.3 + 20001 + External + Idle + + Idle + Start + None + + NORMAL-FRONTEND + Preference LocalAddress AddressFamily PeerAS Multipath Refresh + VpnApplyExport PeerSpecficLoopsAllowed + GracefulShutdownRcv Shutdown + inet-unicast inet6-unicast + 90 + 170 + + 0 + 2a01:280:100::1 20000 diff --git a/test/junos/mocked_data/test_get_bgp_neighbors/normal/get-bgp-summary-informationfrontend.xml b/test/junos/mocked_data/test_get_bgp_neighbors/normal/get-bgp-summary-informationfrontend.xml index 762dfd9ff..efa267f9d 100644 --- a/test/junos/mocked_data/test_get_bgp_neighbors/normal/get-bgp-summary-informationfrontend.xml +++ b/test/junos/mocked_data/test_get_bgp_neighbors/normal/get-bgp-summary-informationfrontend.xml @@ -133,6 +133,16 @@ 1w1d1h Active + + 192.169.1.2 + 20000 + 0 + 0 + 0 + 0 + 1w1d1h + Idle + 2a01:280:100::1 20000 From 1391f3273a780439711748af122608a4ccf95302 Mon Sep 17 00:00:00 2001 From: Denis Mulyalin Date: Sun, 13 Feb 2022 16:06:40 -0500 Subject: [PATCH 73/74] fixed black, pylama and mypy complaining about added code --- mypy.ini | 3 +++ napalm/base/helpers.py | 11 ++++++++--- test/base/test_helpers.py | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/mypy.ini b/mypy.ini index 9819a29a8..62b35169e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -47,5 +47,8 @@ ignore_missing_imports = True [mypy-textfsm] ignore_missing_imports = True +[mypy-ttp] +ignore_missing_imports = True + [mypy-pytest] ignore_missing_imports = True diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py index 9ab99db4b..a58a681a8 100644 --- a/napalm/base/helpers.py +++ b/napalm/base/helpers.py @@ -19,7 +19,7 @@ from netaddr import mac_unix try: - from ttp import ttp, quick_parse as ttp_quick_parse + from ttp import quick_parse as ttp_quick_parse TTP_INSTALLED = True except ImportError: @@ -281,7 +281,7 @@ def ttp_parse( template: str, raw_text: str, structure: str = "flat_list", -) -> List[Dict]: +) -> Union[None, List, Dict]: """ Applies a TTP template over a raw text and return the parsing results. @@ -302,6 +302,8 @@ def ttp_parse( msg = "\nTTP is not installed. Please PIP install ttp:\n" "pip install ttp\n" raise napalm.base.exceptions.ModuleImportError(msg) + result = None + for c in cls.__class__.mro(): if c is object: continue @@ -332,12 +334,13 @@ def ttp_parse( # parse data try: - return ttp_quick_parse( + result = ttp_quick_parse( data=str(raw_text), template=template, result_kwargs={"structure": structure}, parse_kwargs={"one": True}, ) + break except Exception as e: msg = "TTP template:\n'{template}'\nError: {error}".format( template=template, error=e @@ -346,6 +349,8 @@ def ttp_parse( logging.error(msg) raise napalm.base.exceptions.TemplateRenderException(msg) + return result + def find_txt( xml_tree: etree._Element, diff --git a/test/base/test_helpers.py b/test/base/test_helpers.py index bb152a715..f6ca9717b 100644 --- a/test/base/test_helpers.py +++ b/test/base/test_helpers.py @@ -709,7 +709,7 @@ def test_ttp_parse(self): description BAR ! """ - _TTP_TEST_TEMPLATE = '\ninterface {{ interface }}\n description {{ description | re(".+") }}\n! {{ _end_ }}\n' + _TTP_TEST_TEMPLATE = '\ninterface {{ interface }}\n description {{ description | re(".+") }}\n! {{ _end_ }}\n' # noqa:E501 _EXPECTED_RESULT = [ {"description": "FOO", "interface": "Gi1/1"}, {"description": "BAR", "interface": "Gi1/2"}, From eb625e90aab4e90f9f97eaa9c487c600327c7c6f Mon Sep 17 00:00:00 2001 From: DavidVentura Date: Fri, 18 Feb 2022 13:27:46 +0100 Subject: [PATCH 74/74] Fix #1569: Parse all interfaces from vlans when separated by newlines Signed-off-by: DavidVentura --- napalm/ios/ios.py | 44 ++++++++++++++----- .../normal/expected_result.json | 6 ++- .../normal/show_vlan_all_ports.txt | 4 +- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/napalm/ios/ios.py b/napalm/ios/ios.py index e8a2dd0b5..3f501ed46 100644 --- a/napalm/ios/ios.py +++ b/napalm/ios/ios.py @@ -3590,22 +3590,42 @@ def get_vlans(self): return self._get_vlan_all_ports(output) def _get_vlan_all_ports(self, output): - find_regexp = r"^(\d+)\s+(\S+)\s+\S+\s+([A-Z][a-z].*)$" - find = re.findall(find_regexp, output, re.MULTILINE) + find_regexp = re.compile(r"^(\d+)\s+(\S+)\s+\S+(\s+[A-Z][a-z].*)?$") + continuation_regexp = re.compile(r"^\s+([A-Z][a-z].*)$") + output = output.splitlines() vlans = {} - for vlan_id, vlan_name, interfaces in find: - vlans[vlan_id] = { - "name": vlan_name, - "interfaces": [ + + was_vlan_or_cont = False + vlan_id = None + vlan_name = None + interfaces = "" + for line in output: + vlan_m = find_regexp.match(line) + if vlan_m: + was_vlan_or_cont = True + vlan_id = vlan_m.group(1) + vlan_name = vlan_m.group(2) + interfaces = vlan_m.group(3) or "" + vlans[vlan_id] = {"name": vlan_name, "interfaces": []} + + cont_m = None + if was_vlan_or_cont: + cont_m = continuation_regexp.match(line) + if cont_m: + interfaces = cont_m.group(1) + + if not cont_m and not vlan_m: + was_vlan_or_cont = False + continue + + vlans[vlan_id]["interfaces"].extend( + [ canonical_interface_name(intf.strip()) for intf in interfaces.split(",") - ], - } + if intf.strip() + ] + ) - find_regexp = r"^(\d+)\s+(\S+)\s+\S+$" - find = re.findall(find_regexp, output, re.MULTILINE) - for vlan_id, vlan_name in find: - vlans[vlan_id] = {"name": vlan_name, "interfaces": []} return vlans def _get_vlan_from_id(self): diff --git a/test/ios/mocked_data/test_get_vlans/normal/expected_result.json b/test/ios/mocked_data/test_get_vlans/normal/expected_result.json index 046ede3b7..9724b801b 100644 --- a/test/ios/mocked_data/test_get_vlans/normal/expected_result.json +++ b/test/ios/mocked_data/test_get_vlans/normal/expected_result.json @@ -681,7 +681,11 @@ "Port-channel26", "Port-channel27", "Port-channel28", - "Port-channel30" + "Port-channel30", + "TenGigabitEthernet2/2/30", + "TenGigabitEthernet2/1/15", + "TenGigabitEthernet2/2/21", + "TenGigabitEthernet1/3/8" ] }, "707": { diff --git a/test/ios/mocked_data/test_get_vlans/normal/show_vlan_all_ports.txt b/test/ios/mocked_data/test_get_vlans/normal/show_vlan_all_ports.txt index f0c4c22f3..5f9850dd7 100644 --- a/test/ios/mocked_data/test_get_vlans/normal/show_vlan_all_ports.txt +++ b/test/ios/mocked_data/test_get_vlans/normal/show_vlan_all_ports.txt @@ -38,6 +38,8 @@ VLAN Name Status Ports 612 Vlan612 active Po1, Po3, Po4, Po5, Po21, Po22, Po23, Po24, Po25, Po26, Po27, Po28, Po30 613 Vlan613 active Po1, Po3, Po4, Po5, Po21, Po22, Po23, Po24, Po25, Po26, Po27, Po28, Po30 702 Vlan702 active Te2/2/32, Po1, Po5, Po21, Po22, Po23, Po24, Po25, Po26, Po27, Po28, Po30 + Te2/2/30, Te2/1/15 + Te2/2/21, Te1/3/8 707 Vlan707 active Po1, Po5, Po7, Po21, Po22, Po23, Po24, Po25, Po26, Po27, Po28, Po30 710 Vlan710 active Te1/1/9, Te1/1/11, Te1/3/4, Te2/2/31, Te2/3/4, Te2/3/6, Po1, Po3, Po4, Po5, Po6, Po7, Po21, Po22, Po23, Po24, Po25, Po26, Po27, Po28, Po29, Po30, Po100 711 Vlan711 active Po1, Po5, Po21, Po22, Po23, Po24, Po25, Po26, Po27, Po28, Po30 @@ -141,4 +143,4 @@ VLAN Type SAID MTU Parent RingNo BridgeNo Stp BrdgMode Trans1 Trans2 1275 enet 101275 1500 - - - - - 0 0 Primary Secondary Type Ports -------- --------- ----------------- ------------------------------------------ \ No newline at end of file +------- --------- ----------------- ------------------------------------------