Skip to content

Commit

Permalink
Add argument to send_command and send_command_timing to support raisi…
Browse files Browse the repository at this point in the history
…ng exception for parsers (ktbyers#3494)

* introduces `raise_parsing_error` parameter to `send_command`

Allow exceptions from structured data parsers to bubble up to the
callers of `send_command` and `send_command_timing`. This is useful for
example in conjunction with `nornir_netmiko`, which currently can't fail
tasks based on failed parsing, leading to successful tasks with garbage
data.

* Updates to make raise_parsing_error more ubiquitous across TTP/genie/TextFSM; additional tests

---------

Co-authored-by: Leo Kirchner <[email protected]>
  • Loading branch information
ktbyers and Kircheneer authored Sep 4, 2024
1 parent 92ca035 commit 4599c4d
Show file tree
Hide file tree
Showing 5 changed files with 248 additions and 79 deletions.
8 changes: 8 additions & 0 deletions netmiko/base_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -1516,6 +1516,7 @@ def send_command_timing(
ttp_template: Optional[str] = None,
use_genie: bool = False,
cmd_verify: bool = False,
raise_parsing_error: bool = False,
) -> Union[str, List[Any], Dict[str, Any]]:
"""Execute command_string on the SSH channel using a delay-based mechanism. Generally
used for show commands.
Expand Down Expand Up @@ -1553,6 +1554,8 @@ def send_command_timing(
:param use_genie: Process command output through PyATS/Genie parser (default: False).
:param cmd_verify: Verify command echo before proceeding (default: False).
:param raise_parsing_error: Raise exception when parsing output to structured data fails.
"""
if delay_factor is not None or max_loops is not None:
warnings.warn(DELAY_FACTOR_DEPR_SIMPLE_MSG, DeprecationWarning)
Expand Down Expand Up @@ -1586,6 +1589,7 @@ def send_command_timing(
use_genie=use_genie,
textfsm_template=textfsm_template,
ttp_template=ttp_template,
raise_parsing_error=raise_parsing_error,
)
return return_data

Expand Down Expand Up @@ -1666,6 +1670,7 @@ def send_command(
ttp_template: Optional[str] = None,
use_genie: bool = False,
cmd_verify: bool = True,
raise_parsing_error: bool = False,
) -> Union[str, List[Any], Dict[str, Any]]:
"""Execute command_string on the SSH channel using a pattern-based mechanism. Generally
used for show commands. By default this method will keep waiting to receive data until the
Expand Down Expand Up @@ -1705,6 +1710,8 @@ def send_command(
:param use_genie: Process command output through PyATS/Genie parser (default: False).
:param cmd_verify: Verify command echo before proceeding (default: True).
:param raise_parsing_error: Raise exception when parsing output to structured data fails.
"""

# Time to delay in each read loop
Expand Down Expand Up @@ -1840,6 +1847,7 @@ def send_command(
use_genie=use_genie,
textfsm_template=textfsm_template,
ttp_template=ttp_template,
raise_parsing_error=raise_parsing_error,
)
return return_val

Expand Down
6 changes: 6 additions & 0 deletions netmiko/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,9 @@ class ReadTimeout(ReadException):
"""General exception indicating an error occurred during a Netmiko read operation."""

pass


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

pass
69 changes: 58 additions & 11 deletions netmiko/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from textfsm import clitable
from textfsm.clitable import CliTableError
from netmiko import log
from netmiko.exceptions import NetmikoParsingException

# For decorators
F = TypeVar("F", bound=Callable[..., Any])
Expand Down Expand Up @@ -341,6 +342,7 @@ def _textfsm_parse(
raw_output: str,
attrs: Dict[str, str],
template_file: Optional[str] = None,
raise_parsing_error: bool = False,
) -> Union[str, List[Dict[str, str]]]:
"""Perform the actual TextFSM parsing using the CliTable object."""
tfsm_parse: Callable[..., Any] = textfsm_obj.ParseCmd
Expand All @@ -353,10 +355,19 @@ def _textfsm_parse(

structured_data = clitable_to_dict(textfsm_obj)
if structured_data == []:
if raise_parsing_error:
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):
except (FileNotFoundError, CliTableError) as error:
if raise_parsing_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 All @@ -365,6 +376,7 @@ def get_structured_data_textfsm(
platform: Optional[str] = None,
command: Optional[str] = None,
template: Optional[str] = None,
raise_parsing_error: bool = False,
) -> Union[str, List[Dict[str, str]]]:
"""
Convert raw CLI output to structured data using TextFSM template.
Expand All @@ -385,7 +397,12 @@ def get_structured_data_textfsm(
template_dir = get_template_dir()
index_file = os.path.join(template_dir, "index")
textfsm_obj = clitable.CliTable(index_file, template_dir)
output = _textfsm_parse(textfsm_obj, raw_output, attrs)
output = _textfsm_parse(
textfsm_obj,
raw_output,
attrs,
raise_parsing_error=raise_parsing_error,
)

# Retry the output if "cisco_xe" and not structured data
if platform and "cisco_xe" in platform:
Expand All @@ -400,15 +417,21 @@ def get_structured_data_textfsm(
# CliTable with no index will fall-back to a TextFSM parsing behavior
textfsm_obj = clitable.CliTable(template_dir=template_dir_alt)
return _textfsm_parse(
textfsm_obj, raw_output, attrs, template_file=template_file
textfsm_obj,
raw_output,
attrs,
template_file=template_file,
raise_parsing_error=raise_parsing_error,
)


# For compatibility
get_structured_data = get_structured_data_textfsm


def get_structured_data_ttp(raw_output: str, template: str) -> Union[str, List[Any]]:
def get_structured_data_ttp(
raw_output: str, template: str, raise_parsing_error: bool = False
) -> Union[str, List[Any]]:
"""
Convert raw CLI output to structured data using TTP template.
Expand All @@ -422,8 +445,17 @@ def get_structured_data_ttp(raw_output: str, template: str) -> Union[str, List[A
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:
except Exception as exception:
if raise_parsing_error:
raise NetmikoParsingException(
f"Failed to parse CLI output using TTP\n{exception}"
)
return raw_output


Expand Down Expand Up @@ -496,7 +528,7 @@ def run_ttp_template(


def get_structured_data_genie(
raw_output: str, platform: str, command: str
raw_output: str, platform: str, command: str, raise_parsing_error: bool = False
) -> Union[str, Dict[str, Any]]:
if not sys.version_info >= (3, 4):
raise ValueError("Genie requires Python >= 3.4")
Expand Down Expand Up @@ -544,7 +576,11 @@ def get_structured_data_genie(
get_parser(command, device)
parsed_output: Dict[str, Any] = device.parse(command, output=raw_output)
return parsed_output
except Exception:
except Exception as exception:
if raise_parsing_error:
raise NetmikoParsingException(
f"Failed to parse CLI output using Genie\n{exception}"
)
return raw_output


Expand All @@ -557,16 +593,22 @@ def structured_data_converter(
use_genie: bool = False,
textfsm_template: Optional[str] = None,
ttp_template: Optional[str] = None,
raise_parsing_error: bool = False,
) -> Union[str, List[Any], Dict[str, Any]]:
"""
Try structured data converters in the following order: TextFSM, TTP, Genie.
Return the first structured data found, else return the raw_data as-is.
Return the first structured data found, else return the raw_data as-is unless
`raise_parsing_error` is True, then bubble up the exception to the caller.
"""
command = command.strip()
if use_textfsm:
structured_output_tfsm = get_structured_data_textfsm(
raw_data, platform=platform, command=command, template=textfsm_template
raw_data,
platform=platform,
command=command,
template=textfsm_template,
raise_parsing_error=raise_parsing_error,
)
if not isinstance(structured_output_tfsm, str):
return structured_output_tfsm
Expand All @@ -579,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}}
Loading

0 comments on commit 4599c4d

Please sign in to comment.