From fee6c1f71d164d47b878dd439d875e98991dc2ac Mon Sep 17 00:00:00 2001 From: Michael Sheinberg Date: Thu, 6 Feb 2025 18:53:43 -0800 Subject: [PATCH 1/3] Expand tests to cover ssh logic --- .github/workflows/ci.yml | 3 - changes/315.housekeeping | 1 + .../nornir_plays/formatter.py | 8 ++- .../tests/fakenos/custom_ios.yaml | 69 +++++++++++++++++++ nautobot_device_onboarding/tests/test_jobs.py | 59 ++++++++++++++++ poetry.lock | 37 +++++++++- pyproject.toml | 2 + 7 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 changes/315.housekeeping create mode 100644 nautobot_device_onboarding/tests/fakenos/custom_ios.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 135a19a5..30191f55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -146,9 +146,6 @@ jobs: - python-version: "3.11" db-backend: "postgresql" nautobot-version: "2.2.3" - # - python-version: "3.12" - # db-backend: "mysql" - # nautobot-version: "stable" runs-on: "ubuntu-22.04" env: INVOKE_NAUTOBOT_DEVICE_ONBOARDING_PYTHON_VER: "${{ matrix.python-version }}" diff --git a/changes/315.housekeeping b/changes/315.housekeeping new file mode 100644 index 00000000..c369a8d7 --- /dev/null +++ b/changes/315.housekeeping @@ -0,0 +1 @@ +Added fake SSH devices to tests to increase coverage. diff --git a/nautobot_device_onboarding/nornir_plays/formatter.py b/nautobot_device_onboarding/nornir_plays/formatter.py index d62620e5..1752c567 100755 --- a/nautobot_device_onboarding/nornir_plays/formatter.py +++ b/nautobot_device_onboarding/nornir_plays/formatter.py @@ -4,6 +4,7 @@ import logging from json.decoder import JSONDecodeError +import jinja2 from django.template import engines from django.utils.module_loading import import_string from jdiff import extract_data_from_json @@ -106,7 +107,12 @@ def extract_and_post_process(parsed_command_output, yaml_command_element, j2_dat # j2 context data changes obj(hostname) -> extracted_value for post_processor j2_data_context["obj"] = extracted_value template = j2_env.from_string(yaml_command_element["post_processor"]) - extracted_processed = template.render(**j2_data_context) + try: + extracted_processed = template.render(**j2_data_context) + except jinja2.exceptions.UndefinedError: + raise ValueError( + f"Failure Jinja parsing, context: {j2_data_context}. processor: {yaml_command_element['post_processor']}" + ) else: extracted_processed = extracted_value post_processed_data = normalize_processed_data(extracted_processed, iter_type) diff --git a/nautobot_device_onboarding/tests/fakenos/custom_ios.yaml b/nautobot_device_onboarding/tests/fakenos/custom_ios.yaml new file mode 100644 index 00000000..c9b71ebc --- /dev/null +++ b/nautobot_device_onboarding/tests/fakenos/custom_ios.yaml @@ -0,0 +1,69 @@ +--- +name: "tweaked_cisco_ios" +initial_prompt: "{base_prompt}>" +enable_prompt: "{base_prompt}#" +config_prompt: "{base_prompt}(config)#" +commands: + enable: + output: "null" + new_prompt: "{base_prompt}#" + help: "enter enable mode" + prompt: "{base_prompt}>" + show interfaces: + output: + "TenGigabitEthernet1/1/15 is up, line protocol is down (disabled)\n Hardware\ + \ is Ten Gigabit Ethernet Port, address is 6c41.6aba.b44e (bia 6c41.6aba.b44e)\n\ + \ Internet address is 127.0.0.1/32\n MTU 1500 bytes, BW 10000000 Kbit/sec,\ + \ DLY 10 usec,\n reliability 255/255, txload 1/255, rxload 1/255\n Encapsulation\ + \ ARPA, loopback not set\n Keepalive set (10 sec)\n Full-duplex, Auto-speed,\ + \ link type is auto, media type is No XCVR\n input flow-control is off, output\ + \ flow-control is off\n ARP type: ARPA, ARP Timeout 04:00:00\n Last input\ + \ never, output never, output hang never\n Last clearing of \"show interface\"\ + \ counters never\n Input queue: 0/2000/0/0 (size/max/drops/flushes); Total\ + \ output drops: 0\n Queueing strategy: fifo\n Output queue: 0/40 (size/max)\n\ + \ 5 minute input rate 0 bits/sec, 0 packets/sec\n 5 minute output rate 0\ + \ bits/sec, 0 packets/sec\n 0 packets input, 0 bytes, 0 no buffer\n \ + \ Received 0 broadcasts (0 multicasts)\n 0 runts, 0 giants, 0 throttles\n\ + \ 0 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored\n 0 input packets\ + \ with dribble condition detected\n 0 packets output, 0 bytes, 0 underruns\n\ + \ 0 output errors, 0 collisions, 1 interface resets\n 0 unknown protocol\ + \ drops\n 0 babbles, 0 late collision, 0 deferred\n 0 lost carrier,\ + \ 0 no carrier\n 0 output buffer failures, 0 output buffers swapped out" + help: "execute the command 'show interfaces'" + prompt: + - "{base_prompt}>" + - "{base_prompt}#" + show version: + output: + "Cisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.8(3)M2,\ + \ RELEASE SOFTWARE (fc2)\nTechnical Support: http://www.cisco.com/techsupport\n\ + \ Copyright (c) 1986-2019 by Cisco Systems, Inc.\nCompiled Thu 28-Mar-19 14:06\ + \ by prod_rel_team\n\n\nROM: Bootstrap program is IOSv\n\nfake-ios-01 uptime is 1\ + \ week, 3 days, 16 hours, 11 minutes\nSystem returned to ROM by reload\nSystem\ + \ image file is \"flash0:/vios-adventerprisek9-m\"\nLast reload reason: Unknown\ + \ reason\n \n\n\nThis product contains cryptographic features and is subject\ + \ to United\n States and local country laws governing import, export, transfer\ + \ and\nuse. Delivery of Cisco cryptographic products does not imply\nthird-party\ + \ authority to import, export, distribute or use encryption.\nImporters, exporters,\ + \ distributors and users are responsible for\ncompliance with U.S. and local\ + \ country laws. By using this product you\nagree to comply with applicable laws\ + \ and regulations. If you are unable\nto comply with U.S. and local laws, return\ + \ this product immediately.\n \nA summary of U.S. laws governing Cisco cryptographic\ + \ products may be found at:\nhttp://www.cisco.com/wwl/export/crypto/tool/stqrg.html\n\ + \nIf you require further assistance please contact us by sending email to\n\ + export@cisco.com.\n \nCisco IOSv (revision 1.0) with with 460137K/62464K bytes\ + \ of memory.\nProcessor board ID 991UCMIHG4UAJ1J010CQG\n4 Gigabit Ethernet interfaces\n\ + DRAM configuration is 72 bits wide with parity disabled.\n256K bytes of non-volatile\ + \ configuration memory.\n2097152K bytes of ATA System CompactFlash 0 (Read/Write)\n\ + 0K bytes of ATA CompactFlash 1 (Read/Write)\n11217K bytes of ATA CompactFlash\ + \ 2 (Read/Write)\n 0K bytes of ATA CompactFlash 3 (Read/Write)\n\n\n\nConfiguration\ + \ register is 0x0" + help: "execute the command 'show version'" + prompt: + - "{base_prompt}>" + - "{base_prompt}#" + _default_: + output: "% Invalid input detected at '^' marker." + help: "Output to print for unknown commands" + terminal width 511: {"output":"", "help":"Set terminal width to 511"} + terminal length 0: {"output":"", "help":"Set terminal length to 0"} diff --git a/nautobot_device_onboarding/tests/test_jobs.py b/nautobot_device_onboarding/tests/test_jobs.py index bb81d950..afbeefa7 100644 --- a/nautobot_device_onboarding/tests/test_jobs.py +++ b/nautobot_device_onboarding/tests/test_jobs.py @@ -1,11 +1,16 @@ """Test Jobs.""" +import os from unittest.mock import patch +from django.test import override_settings +from fakenos import FakeNOS +from fakenos.core.host import Host from nautobot.apps.testing import create_job_result_and_run_job from nautobot.core.testing import TransactionTestCase from nautobot.dcim.models import Device, Interface, Manufacturer, Platform from nautobot.extras.choices import JobResultStatusChoices +from nautobot.ipam.models import IPAddress from nautobot_device_onboarding import jobs from nautobot_device_onboarding.tests import utils @@ -126,6 +131,10 @@ class SSOTSyncNetworkDataTestCase(TransactionTestCase): databases = ("default", "job_logs") + @classmethod + def setUpClass(cls): + super().setUpClass() + def setUp(self): # pylint: disable=invalid-name """Initialize test case.""" # Setup Nautobot Objects @@ -188,3 +197,53 @@ def test_sync_network_data__success(self, device_data): if interface_data["vrf"]: self.assertEqual(interface.vrf.name, interface_data["vrf"]["name"]) + + @override_settings(CELERY_TASK_ALWAYS_EAGER=True) + @patch.dict("os.environ", {"DEVICE_USER": "admin", "DEVICE_PASS": "admin"}) + def test_sync_network_devices_with_full_ssh(self): + """Use the fakeNOS library to expand test coverage to cover SSH connectivity.""" + job_form_inputs = { + "debug": False, + "connectivity_test": False, + "dryrun": False, + "csv_file": None, + "location": self.testing_objects["location"].pk, + "namespace": self.testing_objects["namespace"].pk, + "ip_addresses": "127.0.0.1", + "port": 6222, + "timeout": 30, + "set_mgmt_only": True, + "update_devices_without_primary_ip": True, + "device_role": self.testing_objects["device_role"].pk, + "device_status": self.testing_objects["status"].pk, + "interface_status": self.testing_objects["status"].pk, + "ip_address_status": self.testing_objects["status"].pk, + "default_prefix_status": self.testing_objects["status"].pk, + "secrets_group": self.testing_objects["secrets_group"].pk, + "platform": self.testing_objects["platform_1"].pk, + "memory_profiling": False, + } + current_file_path = os.path.dirname(os.path.abspath(__file__)) + fake_ios_inventory = { + "hosts": { + "dev1": { + "username": "admin", + "password": "admin", + "platform": "tweaked_cisco_ios", + "port": 6222, + } + } + } + # This is hacky, theres clearly a bug in the fakenos library + # https://github.com/fakenos/fakenos/issues/19 + with patch.object(Host, "_check_if_platform_is_supported"): + with FakeNOS( + inventory=fake_ios_inventory, plugins=[os.path.join(current_file_path, "fakenos/custom_ios.yaml")] + ): + create_job_result_and_run_job( + module="nautobot_device_onboarding.jobs", name="SSOTSyncDevices", **job_form_inputs + ) + + newly_imported_device = Device.objects.get(name="fake-ios-01") + self.assertEqual(str(IPAddress.objects.get(id=newly_imported_device.primary_ip4_id)), "127.0.0.1/32") + self.assertEqual(newly_imported_device.serial, "991UCMIHG4UAJ1J010CQG") diff --git a/poetry.lock b/poetry.lock index b02bcf1d..12dacbec 100644 --- a/poetry.lock +++ b/poetry.lock @@ -816,6 +816,16 @@ files = [ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] +[[package]] +name = "detect" +version = "2020.12.3" +description = "detect OS and Python versions" +optional = false +python-versions = "*" +files = [ + {file = "detect-2020.12.3.tar.gz", hash = "sha256:eecd6c41c17072b4db8dbfa850d979bda40b33b4f349ab4378205bceb74d712e"}, +] + [[package]] name = "diffsync" version = "2.0.1" @@ -1353,6 +1363,30 @@ files = [ [package.extras] tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] +[[package]] +name = "fakenos" +version = "1.1.0" +description = "Fake Network Operating System" +optional = false +python-versions = ">=3.8,<3.13" +files = [] +develop = false + +[package.dependencies] +detect = "2020.12.*" +paramiko = "<4.0" +pydantic = "<3.0" +pyyaml = "<7.0" + +[package.extras] +test = [] + +[package.source] +type = "git" +url = "https://github.com/fakenos/fakenos" +reference = "master" +resolved_reference = "b465bc28b478cf72582191ee77eb8e968864a0da" + [[package]] name = "future" version = "1.0.0" @@ -3092,7 +3126,6 @@ files = [ {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, - {file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"}, @@ -4791,4 +4824,4 @@ all = [] [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.13" -content-hash = "8a645ab31340a5d29395f4c3f60cdefd86914701fe3464c2ec32607294713ab7" +content-hash = "4c234809ca69ce7c409b94934bbc84377e8b93bff549d7b4ee92a7f22ce424a0" diff --git a/pyproject.toml b/pyproject.toml index f8a02996..73616547 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,8 @@ mkdocstrings = "0.25.2" mkdocstrings-python = "1.10.8" griffe = "1.1.1" towncrier = "~23.6.0" +# Used in integration tests +fakenos = { git = "https://github.com/fakenos/fakenos", branch = "master" } [tool.poetry.extras] all = [ From 34c28d187198d8d152a331429c36038848ede890 Mon Sep 17 00:00:00 2001 From: scetron Date: Tue, 11 Feb 2025 09:18:33 -0500 Subject: [PATCH 2/3] lint --- nautobot_device_onboarding/tests/test_jobs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nautobot_device_onboarding/tests/test_jobs.py b/nautobot_device_onboarding/tests/test_jobs.py index 7c1a5d75..70c5a5e6 100644 --- a/nautobot_device_onboarding/tests/test_jobs.py +++ b/nautobot_device_onboarding/tests/test_jobs.py @@ -1,6 +1,5 @@ """Test Jobs.""" - import os from unittest.mock import ANY, patch From 0560ed111323eac8960c43e990771ba94ddc0157 Mon Sep 17 00:00:00 2001 From: scetron Date: Tue, 11 Feb 2025 09:39:10 -0500 Subject: [PATCH 3/3] lint --- nautobot_device_onboarding/tests/test_jobs.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nautobot_device_onboarding/tests/test_jobs.py b/nautobot_device_onboarding/tests/test_jobs.py index 70c5a5e6..b2fd19cb 100644 --- a/nautobot_device_onboarding/tests/test_jobs.py +++ b/nautobot_device_onboarding/tests/test_jobs.py @@ -3,13 +3,12 @@ import os from unittest.mock import ANY, patch +from django.core.files.base import ContentFile from django.test import override_settings from fakenos import FakeNOS from fakenos.core.host import Host -from django.core.files.base import ContentFile - from nautobot.apps.testing import create_job_result_and_run_job -from nautobot.core.testing import TransactionTestCaseA +from nautobot.core.testing import TransactionTestCase from nautobot.dcim.models import Device, Interface, Manufacturer, Platform from nautobot.extras.choices import JobResultStatusChoices from nautobot.extras.models import FileProxy