Skip to content

Commit

Permalink
Updates to make raise_parsing_error more ubiquitous across TTP/genie/…
Browse files Browse the repository at this point in the history
…TextFSM; additional tests
  • Loading branch information
ktbyers committed Sep 3, 2024
1 parent e5a942b commit c360856
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 86 deletions.
2 changes: 1 addition & 1 deletion netmiko/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,6 @@ class ReadTimeout(ReadException):


class NetmikoParsingException(ReadException):
"""Exception raised when there is a generic parsing error."""
"""Exception raised when there is a parsing error."""

pass
33 changes: 25 additions & 8 deletions netmiko/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,15 +356,18 @@ def _textfsm_parse(
structured_data = clitable_to_dict(textfsm_obj)
if structured_data == []:
if raise_parsing_error:
raise NetmikoParsingException(
"Failed to parse CLI output with TextFSM, empty list returned."
)
msg = """Failed to parse CLI output using TextFSM
(template found, but unexpected data?)"""
raise NetmikoParsingException(msg)
return raw_output
else:
return structured_data
except (FileNotFoundError, CliTableError) as error:
if raise_parsing_error:
raise error
msg = f"""Failed to parse CLI output using TextFSM
(template not found for command and device_type/platform?)
{error}"""
raise NetmikoParsingException(msg)
return raw_output


Expand Down Expand Up @@ -442,10 +445,17 @@ def get_structured_data_ttp(
ttp_parser = ttp(data=raw_output, template=template)
ttp_parser.parse(one=True)
result: List[Any] = ttp_parser.result(format="raw")
# TTP will treat a string as a directly passed template so try to catch this
if raise_parsing_error and result == [[{}]]:
msg = """Failed to parse CLI output using TTP
Empty results returned (TTP template could not be found?)"""
raise NetmikoParsingException(msg)
return result
except Exception as exception:
if raise_parsing_error:
raise exception
raise NetmikoParsingException(
f"Failed to parse CLI output using TTP\n{exception}"
)
return raw_output


Expand Down Expand Up @@ -568,7 +578,9 @@ def get_structured_data_genie(
return parsed_output
except Exception as exception:
if raise_parsing_error:
raise exception
raise NetmikoParsingException(
f"Failed to parse CLI output using Genie\n{exception}"
)
return raw_output


Expand Down Expand Up @@ -609,15 +621,20 @@ def structured_data_converter(
raise ValueError(msg)
else:
structured_output_ttp = get_structured_data_ttp(
raw_data, template=ttp_template
raw_data,
template=ttp_template,
raise_parsing_error=raise_parsing_error,
)

if not isinstance(structured_output_ttp, str):
return structured_output_ttp

if use_genie:
structured_output_genie = get_structured_data_genie(
raw_data, platform=platform, command=command
raw_data,
platform=platform,
command=command,
raise_parsing_error=raise_parsing_error,
)
if not isinstance(structured_output_genie, str):
return structured_output_genie
Expand Down
2 changes: 2 additions & 0 deletions tests/etc/show_run_interfaces.ttp
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
interface {{ intf_name }}
description {{ description | ORPHRASE}}
228 changes: 151 additions & 77 deletions tests/unit/test_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import sys
from os.path import dirname, join, relpath
import pytest
from textfsm.clitable import CliTableError

from netmiko import utilities
from textfsm import clitable
Expand All @@ -14,10 +13,98 @@
RESOURCE_FOLDER = join(dirname(dirname(__file__)), "etc")
RELATIVE_RESOURCE_FOLDER = join(dirname(dirname(relpath(__file__))), "etc")
CONFIG_FILENAME = join(RESOURCE_FOLDER, ".netmiko.yml")
TTP_FILE = join(RESOURCE_FOLDER, "show_run_interfaces.ttp")

is_linux = sys.platform == "linux" or sys.platform == "linux2"
skip_if_not_linux = pytest.mark.skipif(not is_linux, reason="Test Requires Linux")

SHOW_RUN_INT = """Building configuration...
Current configuration : 98 bytes
!
interface GigabitEthernet0/0/1
description *** TTP Testing ***
no ip address
shutdown
media-type rj45
negotiation auto
end"""

# SHOW_VER_HEADER is just for linters and line length
SHOW_VER_HEADER = (
"Cisco IOS Software, C3560CX Software (C3560CX-UNIVERSALK9-M), "
"Version 15.2(4)E7, RELEASE SOFTWARE (fc2)"
)
SHOW_VERSION = (
SHOW_VER_HEADER
+ """
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2018 by Cisco Systems, Inc.
Compiled Tue 18-Sep-18 13:20 by prod_rel_team
ROM: Bootstrap program is C3560CX boot loader
BOOTLDR: C3560CX Boot Loader (C3560CX-HBOOT-M) Version 15.2(4r)E5, RELEASE SOFTWARE (fc4)
3560CX uptime is 5 weeks, 1 day, 2 hours, 30 minutes
System returned to ROM by power-on
System restarted at 11:45:26 PDT Tue May 7 2019
System image file is "flash:c3560cx-universalk9-mz.152-4.E7.bin"
Last reload reason: power-on
This product contains cryptographic features and is subject to United
States and local country laws governing import, export, transfer and
use. Delivery of Cisco cryptographic products does not imply
third-party authority to import, export, distribute or use encryption.
Importers, exporters, distributors and users are responsible for
compliance with U.S. and local country laws. By using this product you
agree to comply with applicable laws and regulations. If you are unable
to comply with U.S. and local laws, return this product immediately.
A summary of U.S. laws governing Cisco cryptographic products may be found at:
http://www.cisco.com/wwl/export/crypto/tool/stqrg.html
If you require further assistance please contact us by sending email to
[email protected].
License Level: ipservices
License Type: Permanent Right-To-Use
Next reload license Level: ipservices
cisco WS-C3560CX-8PC-S (APM86XXX) processor (revision A0) with 524288K bytes of memory.
Processor board ID FOCXXXXXXXX
Last reset from power-on
5 Virtual Ethernet interfaces
12 Gigabit Ethernet interfaces
The password-recovery mechanism is enabled.
512K bytes of flash-simulated non-volatile configuration memory.
Base ethernet MAC Address : 12:34:56:78:9A:BC
Motherboard assembly number : 86-75309-01
Power supply part number : 867-5309-01
Motherboard serial number : FOCXXXXXXXX
Power supply serial number : FOCXXXXXXXX
Model revision number : A0
Motherboard revision number : A0
Model number : WS-C3560CX-8PC-S
System serial number : FOCXXXXXXXX
Top Assembly Part Number : 86-7530-91
Top Assembly Revision Number : A0
Version ID : V01
CLEI Code Number : CMM1400DRA
Hardware Board Revision Number : 0x02
Switch Ports Model SW Version SW Image
------ ----- ----- ---------- ----------
* 1 12 WS-C3560CX-8PC-S 15.2(4)E7 C3560CX-UNIVERSALK9-M
Configuration register is 0xF
"""
)


def test_load_yaml_file():
"""Read a YAML file successfully"""
Expand Down Expand Up @@ -307,20 +394,18 @@ def test_textfsm_missing_template():
[
# Invalid output with valid template/platform
("Unparsable output", "cisco_ios", "show version", NetmikoParsingException),
# Valid output with invalid template/platform
# Valid output with invalid template/platform (template not found)
(
"Cisco IOS Software, Catalyst 4500 L3 Switch Software",
"cisco_ios",
"show invalid-command",
CliTableError,
NetmikoParsingException,
),
# Missing platform (behaviour consistent between `raise_parsing_error=True|False`
("", "", "show version", CliTableError),
# Missing platform
("", "", "show version", NetmikoParsingException),
],
)
def test_textfsm_parsing_error_invalid_command(
raw_output, platform, command, exception
):
def test_parsing_errors_textfsm(raw_output, platform, command, exception):
"""Verify that an error is raised if TextFSM parsing fails and `raise_parsing_error` is set."""
with pytest.raises(exception):
utilities.get_structured_data_textfsm(
Expand All @@ -331,80 +416,50 @@ def test_textfsm_parsing_error_invalid_command(
)


def test_get_structured_data_genie():
"""Convert raw CLI output to structured data using Genie"""

header_line = (
"Cisco IOS Software, C3560CX Software (C3560CX-UNIVERSALK9-M), "
"Version 15.2(4)E7, RELEASE SOFTWARE (fc2)"
@pytest.mark.parametrize(
"raw_output,template",
[
(
SHOW_RUN_INT,
TTP_FILE,
),
],
)
def test_get_structured_data_ttp(raw_output, template):
"""Verify TTP Parsing works."""
data_struct = utilities.get_structured_data_ttp(
raw_output,
template=template,
raise_parsing_error=True,
)
raw_output = """
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2018 by Cisco Systems, Inc.
Compiled Tue 18-Sep-18 13:20 by prod_rel_team
assert isinstance(data_struct, list)
assert isinstance(data_struct[0][0], dict)
assert "*** TTP Testing ***" in data_struct[0][0]["description"]

ROM: Bootstrap program is C3560CX boot loader
BOOTLDR: C3560CX Boot Loader (C3560CX-HBOOT-M) Version 15.2(4r)E5, RELEASE SOFTWARE (fc4)
3560CX uptime is 5 weeks, 1 day, 2 hours, 30 minutes
System returned to ROM by power-on
System restarted at 11:45:26 PDT Tue May 7 2019
System image file is "flash:c3560cx-universalk9-mz.152-4.E7.bin"
Last reload reason: power-on
This product contains cryptographic features and is subject to United
States and local country laws governing import, export, transfer and
use. Delivery of Cisco cryptographic products does not imply
third-party authority to import, export, distribute or use encryption.
Importers, exporters, distributors and users are responsible for
compliance with U.S. and local country laws. By using this product you
agree to comply with applicable laws and regulations. If you are unable
to comply with U.S. and local laws, return this product immediately.
A summary of U.S. laws governing Cisco cryptographic products may be found at:
http://www.cisco.com/wwl/export/crypto/tool/stqrg.html
If you require further assistance please contact us by sending email to
[email protected].
License Level: ipservices
License Type: Permanent Right-To-Use
Next reload license Level: ipservices
cisco WS-C3560CX-8PC-S (APM86XXX) processor (revision A0) with 524288K bytes of memory.
Processor board ID FOCXXXXXXXX
Last reset from power-on
5 Virtual Ethernet interfaces
12 Gigabit Ethernet interfaces
The password-recovery mechanism is enabled.
512K bytes of flash-simulated non-volatile configuration memory.
Base ethernet MAC Address : 12:34:56:78:9A:BC
Motherboard assembly number : 86-75309-01
Power supply part number : 867-5309-01
Motherboard serial number : FOCXXXXXXXX
Power supply serial number : FOCXXXXXXXX
Model revision number : A0
Motherboard revision number : A0
Model number : WS-C3560CX-8PC-S
System serial number : FOCXXXXXXXX
Top Assembly Part Number : 86-7530-91
Top Assembly Revision Number : A0
Version ID : V01
CLEI Code Number : CMM1400DRA
Hardware Board Revision Number : 0x02

@pytest.mark.parametrize(
"raw_output,template,exception",
[
(
SHOW_RUN_INT,
"invalid_path.ttp",
NetmikoParsingException,
),
],
)
def test_parsing_errors_ttp(raw_output, template, exception):
with pytest.raises(exception):
utilities.get_structured_data_ttp(
raw_output,
template=template,
raise_parsing_error=True,
)

Switch Ports Model SW Version SW Image
------ ----- ----- ---------- ----------
* 1 12 WS-C3560CX-8PC-S 15.2(4)E7 C3560CX-UNIVERSALK9-M

def test_get_structured_data_genie():
"""Convert raw CLI output to structured data using Genie"""

Configuration register is 0xF
"""
raw_output = header_line + raw_output
raw_output = SHOW_VERSION
result = utilities.get_structured_data_genie(
raw_output, platform="cisco_xe", command="show version"
)
Expand All @@ -425,6 +480,26 @@ def test_get_structured_data_genie():
assert nexthop_gw == "172.16.0.1"


@pytest.mark.parametrize(
"raw_output,platform,command,exception",
[
# Invalid show command
(
SHOW_VERSION,
"cisco_xe",
"show invalid-command",
NetmikoParsingException,
),
],
)
def test_parsing_errors_genie(raw_output, platform, command, exception):
"""Verify that an error is raised if Genie parsing fails and `raise_parsing_error` is set."""
with pytest.raises(exception):
utilities.get_structured_data_genie(
raw_output, platform=platform, command=command, raise_parsing_error=True
)


@pytest.mark.parametrize(
"max_loops,delay_factor,timeout,result",
[
Expand All @@ -445,7 +520,6 @@ def test_delay_factor_compat(max_loops, delay_factor, timeout, result):
loop_delay=0.2,
old_timeout=timeout,
)
print(read_timeout)
assert read_timeout == result


Expand Down

0 comments on commit c360856

Please sign in to comment.