739 lines
24 KiB
Python
739 lines
24 KiB
Python
|
|
"""Miscellaneous utility functions."""
|
||
|
|
|
||
|
|
from typing import (
|
||
|
|
Any,
|
||
|
|
AnyStr,
|
||
|
|
TypeVar,
|
||
|
|
Callable,
|
||
|
|
cast,
|
||
|
|
Optional,
|
||
|
|
Union,
|
||
|
|
List,
|
||
|
|
Dict,
|
||
|
|
Tuple,
|
||
|
|
)
|
||
|
|
from typing import TYPE_CHECKING
|
||
|
|
import re
|
||
|
|
from glob import glob
|
||
|
|
import sys
|
||
|
|
import io
|
||
|
|
import os
|
||
|
|
from pathlib import Path
|
||
|
|
import functools
|
||
|
|
from datetime import datetime
|
||
|
|
import importlib.resources as pkg_resources
|
||
|
|
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])
|
||
|
|
|
||
|
|
|
||
|
|
if TYPE_CHECKING:
|
||
|
|
from netmiko.base_connection import BaseConnection
|
||
|
|
from os import PathLike
|
||
|
|
|
||
|
|
try:
|
||
|
|
from ttp import ttp
|
||
|
|
|
||
|
|
TTP_INSTALLED = True
|
||
|
|
|
||
|
|
except ImportError:
|
||
|
|
TTP_INSTALLED = False
|
||
|
|
|
||
|
|
try:
|
||
|
|
from genie.conf.base import Device
|
||
|
|
from genie.libs.parser.utils import get_parser
|
||
|
|
from pyats.datastructures import AttrDict
|
||
|
|
|
||
|
|
GENIE_INSTALLED = True
|
||
|
|
except ImportError:
|
||
|
|
GENIE_INSTALLED = False
|
||
|
|
|
||
|
|
try:
|
||
|
|
import serial.tools.list_ports
|
||
|
|
|
||
|
|
PYSERIAL_INSTALLED = True
|
||
|
|
except ImportError:
|
||
|
|
PYSERIAL_INSTALLED = False
|
||
|
|
|
||
|
|
# Dictionary mapping 'show run' for vendors with different command
|
||
|
|
SHOW_RUN_MAPPER = {
|
||
|
|
"brocade_fos": "configShow",
|
||
|
|
"juniper": "show configuration",
|
||
|
|
"juniper_junos": "show configuration",
|
||
|
|
"extreme": "show configuration",
|
||
|
|
"extreme_ers": "show running-config",
|
||
|
|
"extreme_exos": "show configuration",
|
||
|
|
"extreme_netiron": "show running-config",
|
||
|
|
"extreme_nos": "show running-config",
|
||
|
|
"extreme_slx": "show running-config",
|
||
|
|
"extreme_vdx": "show running-config",
|
||
|
|
"extreme_vsp": "show running-config",
|
||
|
|
"extreme_wing": "show running-config",
|
||
|
|
"ericsson_ipos": "show configuration",
|
||
|
|
"hp_comware": "display current-configuration",
|
||
|
|
"huawei": "display current-configuration",
|
||
|
|
"fortinet": "show full-configuration",
|
||
|
|
"checkpoint": "show configuration",
|
||
|
|
"cisco_wlc": "show run-config",
|
||
|
|
"enterasys": "show running-config",
|
||
|
|
"dell_force10": "show running-config",
|
||
|
|
"avaya_vsp": "show running-config",
|
||
|
|
"avaya_ers": "show running-config",
|
||
|
|
"brocade_vdx": "show running-config",
|
||
|
|
"brocade_nos": "show running-config",
|
||
|
|
"brocade_fastiron": "show running-config",
|
||
|
|
"brocade_netiron": "show running-config",
|
||
|
|
"alcatel_aos": "show configuration snapshot",
|
||
|
|
"cros_mtbr": "show running-config",
|
||
|
|
}
|
||
|
|
|
||
|
|
# Expand SHOW_RUN_MAPPER to include '_ssh' key
|
||
|
|
new_dict = {}
|
||
|
|
for k, v in SHOW_RUN_MAPPER.items():
|
||
|
|
new_key = k + "_ssh"
|
||
|
|
new_dict[k] = v
|
||
|
|
new_dict[new_key] = v
|
||
|
|
SHOW_RUN_MAPPER = new_dict
|
||
|
|
|
||
|
|
# Default location of netmiko temp directory for netmiko tools
|
||
|
|
NETMIKO_BASE_DIR = "~/.netmiko"
|
||
|
|
|
||
|
|
|
||
|
|
def load_yaml_file(yaml_file: Union[str, bytes, "PathLike[Any]"]) -> Any:
|
||
|
|
"""Read YAML file."""
|
||
|
|
try:
|
||
|
|
import yaml
|
||
|
|
except ImportError:
|
||
|
|
sys.exit("Unable to import yaml module.")
|
||
|
|
try:
|
||
|
|
with io.open(yaml_file, "rt", encoding="utf-8") as fname:
|
||
|
|
return yaml.safe_load(fname)
|
||
|
|
except IOError:
|
||
|
|
sys.exit("Unable to open YAML file")
|
||
|
|
|
||
|
|
|
||
|
|
def load_netmiko_yml(file_name: Union[str, bytes, "PathLike[Any]", None] = None) -> Any:
|
||
|
|
"""
|
||
|
|
Load and parse the .netmiko.yml as determined by 'find_cfg_file'.
|
||
|
|
|
||
|
|
Parsing:
|
||
|
|
Retrieve and extract 'config' parameters: __meta__ field
|
||
|
|
Determine if encryption is being used and decrypt any encrypted fields
|
||
|
|
"""
|
||
|
|
yaml_devices_file = find_cfg_file(file_name)
|
||
|
|
netmiko_yaml_data = load_yaml_file(yaml_devices_file)
|
||
|
|
config_params = netmiko_yaml_data.pop("__meta__", {})
|
||
|
|
return config_params, netmiko_yaml_data
|
||
|
|
|
||
|
|
|
||
|
|
def load_devices(file_name: Union[str, bytes, "PathLike[Any]", None] = None) -> Any:
|
||
|
|
"""Find and load .netmiko.yml file."""
|
||
|
|
yaml_devices_file = find_cfg_file(file_name)
|
||
|
|
return load_yaml_file(yaml_devices_file)
|
||
|
|
|
||
|
|
|
||
|
|
def find_cfg_file(
|
||
|
|
file_name: Union[str, bytes, "PathLike[Any]", None] = None
|
||
|
|
) -> Union[str, bytes, "PathLike[Any]"]:
|
||
|
|
"""
|
||
|
|
Search for netmiko_tools inventory file in the following order:
|
||
|
|
NETMIKO_TOOLS_CFG environment variable
|
||
|
|
Current directory
|
||
|
|
Home directory
|
||
|
|
Look for file named: .netmiko.yml or netmiko.yml
|
||
|
|
Also allow NETMIKO_TOOLS_CFG to point directly at a file
|
||
|
|
"""
|
||
|
|
if file_name and os.path.isfile(file_name):
|
||
|
|
return file_name
|
||
|
|
optional_path = os.environ.get("NETMIKO_TOOLS_CFG", "")
|
||
|
|
if os.path.isfile(optional_path):
|
||
|
|
return optional_path
|
||
|
|
search_paths = [optional_path, ".", os.path.expanduser("~")]
|
||
|
|
# Filter optional_path if null
|
||
|
|
search_paths = [path for path in search_paths if path]
|
||
|
|
for path in search_paths:
|
||
|
|
files = glob(f"{path}/.netmiko.yml") + glob(f"{path}/netmiko.yml")
|
||
|
|
if files:
|
||
|
|
return files[0]
|
||
|
|
raise IOError(
|
||
|
|
".netmiko.yml file not found in NETMIKO_TOOLS_CFG environment variable directory,"
|
||
|
|
" current directory, or home directory."
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def display_inventory(my_devices: Dict[str, Union[List[str], Dict[str, Any]]]) -> None:
|
||
|
|
"""Print out inventory devices and groups."""
|
||
|
|
config_params = my_devices.pop("__meta__", {}) # noqa
|
||
|
|
inventory_groups = ["all"]
|
||
|
|
inventory_devices = []
|
||
|
|
for k, v in my_devices.items():
|
||
|
|
if isinstance(v, list):
|
||
|
|
inventory_groups.append(k)
|
||
|
|
elif isinstance(v, dict):
|
||
|
|
inventory_devices.append((k, v["device_type"]))
|
||
|
|
|
||
|
|
inventory_groups.sort()
|
||
|
|
inventory_devices.sort(key=lambda x: x[0])
|
||
|
|
print("\nDevices:")
|
||
|
|
print("-" * 40)
|
||
|
|
for a_device, device_type in inventory_devices:
|
||
|
|
device_type = f" ({device_type})"
|
||
|
|
print(f"{a_device:<25}{device_type:>15}")
|
||
|
|
print("\n\nGroups:")
|
||
|
|
print("-" * 40)
|
||
|
|
for a_group in inventory_groups:
|
||
|
|
print(a_group)
|
||
|
|
print()
|
||
|
|
|
||
|
|
|
||
|
|
def obtain_all_devices(
|
||
|
|
my_devices: Dict[str, Union[List[str], Dict[str, Any]]]
|
||
|
|
) -> Dict[str, Dict[str, Any]]:
|
||
|
|
"""Dynamically create 'all' group."""
|
||
|
|
new_devices = {}
|
||
|
|
for device_name, device_or_group in my_devices.items():
|
||
|
|
# Skip any groups
|
||
|
|
if not isinstance(device_or_group, list):
|
||
|
|
new_devices[device_name] = device_or_group
|
||
|
|
return new_devices
|
||
|
|
|
||
|
|
|
||
|
|
def obtain_netmiko_filename(device_name: str) -> str:
|
||
|
|
"""Create file name based on device_name."""
|
||
|
|
_, netmiko_full_dir = find_netmiko_dir()
|
||
|
|
return f"{netmiko_full_dir}/{device_name}.txt"
|
||
|
|
|
||
|
|
|
||
|
|
def write_tmp_file(device_name: str, output: str) -> str:
|
||
|
|
file_name = obtain_netmiko_filename(device_name)
|
||
|
|
with open(file_name, "w") as f:
|
||
|
|
f.write(output)
|
||
|
|
return file_name
|
||
|
|
|
||
|
|
|
||
|
|
def ensure_dir_exists(verify_dir: str) -> None:
|
||
|
|
"""Ensure directory exists. Create if necessary."""
|
||
|
|
if not os.path.exists(verify_dir):
|
||
|
|
# Doesn't exist create dir
|
||
|
|
os.makedirs(verify_dir)
|
||
|
|
else:
|
||
|
|
# Exists
|
||
|
|
if not os.path.isdir(verify_dir):
|
||
|
|
# Not a dir, raise an exception
|
||
|
|
raise ValueError(f"{verify_dir} is not a directory")
|
||
|
|
|
||
|
|
|
||
|
|
def find_netmiko_dir() -> Tuple[str, str]:
|
||
|
|
"""Check environment first, then default dir"""
|
||
|
|
try:
|
||
|
|
netmiko_base_dir = os.environ["NETMIKO_DIR"]
|
||
|
|
except KeyError:
|
||
|
|
netmiko_base_dir = NETMIKO_BASE_DIR
|
||
|
|
netmiko_base_dir = os.path.expanduser(netmiko_base_dir)
|
||
|
|
if netmiko_base_dir == "/":
|
||
|
|
raise ValueError("/ cannot be netmiko_base_dir")
|
||
|
|
netmiko_full_dir = f"{netmiko_base_dir}/tmp"
|
||
|
|
return (netmiko_base_dir, netmiko_full_dir)
|
||
|
|
|
||
|
|
|
||
|
|
def write_bytes(out_data: AnyStr, encoding: str = "utf-8") -> bytes:
|
||
|
|
"""Ensure output is properly encoded bytes."""
|
||
|
|
if isinstance(out_data, str):
|
||
|
|
return out_data.encode(encoding)
|
||
|
|
elif isinstance(out_data, bytes):
|
||
|
|
return out_data
|
||
|
|
msg = f"Invalid value for out_data neither unicode nor byte string: {str(out_data)}"
|
||
|
|
raise ValueError(msg)
|
||
|
|
|
||
|
|
|
||
|
|
def check_serial_port(name: str) -> str:
|
||
|
|
"""returns valid COM Port."""
|
||
|
|
|
||
|
|
if not PYSERIAL_INSTALLED:
|
||
|
|
msg = (
|
||
|
|
"\npyserial is not installed. Please PIP install pyserial:\n\n"
|
||
|
|
"pip install pyserial\n\n"
|
||
|
|
)
|
||
|
|
raise ValueError(msg)
|
||
|
|
|
||
|
|
try:
|
||
|
|
cdc = next(serial.tools.list_ports.grep(name))
|
||
|
|
serial_port = cdc[0]
|
||
|
|
assert isinstance(serial_port, str)
|
||
|
|
return serial_port
|
||
|
|
except StopIteration:
|
||
|
|
msg = f"device {name} not found. "
|
||
|
|
msg += "available devices are: "
|
||
|
|
ports = list(serial.tools.list_ports.comports())
|
||
|
|
for p in ports:
|
||
|
|
msg += f"{str(p)},"
|
||
|
|
raise ValueError(msg)
|
||
|
|
|
||
|
|
|
||
|
|
def get_template_dir(_skip_ntc_package: bool = False) -> str:
|
||
|
|
"""
|
||
|
|
Find and return the directory containing the TextFSM index file.
|
||
|
|
|
||
|
|
Order of preference is:
|
||
|
|
1) Find directory in `NET_TEXTFSM` Environment Variable.
|
||
|
|
2) Check for pip installed `ntc-templates` location in this environment.
|
||
|
|
3) ~/ntc-templates/ntc_templates/templates.
|
||
|
|
|
||
|
|
If `index` file is not found in any of these locations, raise ValueError
|
||
|
|
|
||
|
|
:return: directory containing the TextFSM index file
|
||
|
|
|
||
|
|
"""
|
||
|
|
|
||
|
|
msg = """
|
||
|
|
Directory containing TextFSM index file not found.
|
||
|
|
|
||
|
|
Please set the NET_TEXTFSM environment variable to point at the directory containing your TextFSM
|
||
|
|
index file.
|
||
|
|
|
||
|
|
Alternatively, `pip install ntc-templates` (if using ntc-templates).
|
||
|
|
|
||
|
|
"""
|
||
|
|
|
||
|
|
# Try NET_TEXTFSM environment variable
|
||
|
|
template_dir = os.environ.get("NET_TEXTFSM")
|
||
|
|
if template_dir is not None:
|
||
|
|
template_dir = os.path.expanduser(template_dir)
|
||
|
|
index = os.path.join(template_dir, "index")
|
||
|
|
if not os.path.isfile(index):
|
||
|
|
# Assume only base ./ntc-templates specified
|
||
|
|
template_dir = os.path.join(template_dir, "templates")
|
||
|
|
|
||
|
|
else:
|
||
|
|
# Try 'pip installed' ntc-templates
|
||
|
|
try:
|
||
|
|
# New API for Python 3.13+
|
||
|
|
if sys.version_info >= (3, 13):
|
||
|
|
with pkg_resources.path("ntc_templates", "parse.py") as posix_path:
|
||
|
|
# Example: /venv/netmiko/lib/python3.13/site-packages/ntc_templates/templates
|
||
|
|
template_dir = str(posix_path.parent.joinpath("templates"))
|
||
|
|
# This is for Netmiko automated testing
|
||
|
|
if _skip_ntc_package:
|
||
|
|
raise ModuleNotFoundError()
|
||
|
|
else:
|
||
|
|
with pkg_resources.path(
|
||
|
|
package="ntc_templates", resource="parse.py"
|
||
|
|
) as posix_path:
|
||
|
|
# Example: /opt/venv/netmiko/lib/python3.9/site-packages/ntc_templates/templates
|
||
|
|
template_dir = str(posix_path.parent.joinpath("templates"))
|
||
|
|
# This is for Netmiko automated testing
|
||
|
|
if _skip_ntc_package:
|
||
|
|
raise ModuleNotFoundError()
|
||
|
|
except ModuleNotFoundError:
|
||
|
|
# Finally check in ~/ntc-templates/ntc_templates/templates
|
||
|
|
home_dir = os.path.expanduser("~")
|
||
|
|
template_dir = os.path.join(
|
||
|
|
home_dir, "ntc-templates", "ntc_templates", "templates"
|
||
|
|
)
|
||
|
|
|
||
|
|
index = os.path.join(template_dir, "index")
|
||
|
|
if not os.path.isdir(template_dir) or not os.path.isfile(index):
|
||
|
|
raise ValueError(msg)
|
||
|
|
return os.path.abspath(template_dir)
|
||
|
|
|
||
|
|
|
||
|
|
def clitable_to_dict(cli_table: clitable.CliTable) -> List[Dict[str, str]]:
|
||
|
|
"""Converts TextFSM cli_table object to list of dictionaries."""
|
||
|
|
return_list = []
|
||
|
|
for row in cli_table:
|
||
|
|
temp_dict = {}
|
||
|
|
for index, element in enumerate(row):
|
||
|
|
temp_dict[cli_table.header[index].lower()] = element
|
||
|
|
return_list.append(temp_dict)
|
||
|
|
return return_list
|
||
|
|
|
||
|
|
|
||
|
|
def _textfsm_parse(
|
||
|
|
textfsm_obj: clitable.CliTable,
|
||
|
|
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
|
||
|
|
try:
|
||
|
|
# Parse output through template
|
||
|
|
if template_file is not None:
|
||
|
|
tfsm_parse(raw_output, templates=template_file)
|
||
|
|
else:
|
||
|
|
tfsm_parse(raw_output, attrs)
|
||
|
|
|
||
|
|
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) 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
|
||
|
|
|
||
|
|
|
||
|
|
def get_structured_data_textfsm(
|
||
|
|
raw_output: str,
|
||
|
|
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.
|
||
|
|
|
||
|
|
You can use a straight TextFSM file i.e. specify "template". If no template is specified,
|
||
|
|
then you must use an CliTable index file.
|
||
|
|
"""
|
||
|
|
if platform is None or command is None:
|
||
|
|
attrs = {}
|
||
|
|
else:
|
||
|
|
attrs = {"Command": command, "Platform": platform}
|
||
|
|
|
||
|
|
if template is None:
|
||
|
|
if attrs == {}:
|
||
|
|
raise ValueError(
|
||
|
|
"Either 'platform/command' or 'template' must be specified."
|
||
|
|
)
|
||
|
|
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,
|
||
|
|
raise_parsing_error=raise_parsing_error,
|
||
|
|
)
|
||
|
|
|
||
|
|
# Retry the output if "cisco_xe" and not structured data
|
||
|
|
if platform and "cisco_xe" in platform:
|
||
|
|
if not isinstance(output, list):
|
||
|
|
attrs["Platform"] = "cisco_ios"
|
||
|
|
output = _textfsm_parse(textfsm_obj, raw_output, attrs)
|
||
|
|
return output
|
||
|
|
else:
|
||
|
|
template_path = Path(os.path.expanduser(template))
|
||
|
|
template_file = template_path.name
|
||
|
|
template_dir_alt = template_path.parents[0]
|
||
|
|
# 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,
|
||
|
|
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, raise_parsing_error: bool = False
|
||
|
|
) -> Union[str, List[Any]]:
|
||
|
|
"""
|
||
|
|
Convert raw CLI output to structured data using TTP template.
|
||
|
|
|
||
|
|
You can use a straight TextFSM file i.e. specify "template"
|
||
|
|
"""
|
||
|
|
if not TTP_INSTALLED:
|
||
|
|
msg = "\nTTP is not installed. Please PIP install ttp:\n\npip install ttp\n"
|
||
|
|
raise ValueError(msg)
|
||
|
|
|
||
|
|
try:
|
||
|
|
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 NetmikoParsingException(
|
||
|
|
f"Failed to parse CLI output using TTP\n{exception}"
|
||
|
|
)
|
||
|
|
return raw_output
|
||
|
|
|
||
|
|
|
||
|
|
def run_ttp_template(
|
||
|
|
connection: "BaseConnection",
|
||
|
|
template: Union[str, bytes, "PathLike[Any]"],
|
||
|
|
res_kwargs: Dict[str, Any],
|
||
|
|
**kwargs: Any,
|
||
|
|
) -> Any:
|
||
|
|
"""
|
||
|
|
Helper function to run TTP template parsing.
|
||
|
|
|
||
|
|
:param connection: Netmiko connection object
|
||
|
|
|
||
|
|
:param template: TTP template
|
||
|
|
|
||
|
|
:param res_kwargs: ``**res_kwargs`` arguments for TTP result method
|
||
|
|
|
||
|
|
:param kwargs: ``**kwargs`` for TTP object instantiation
|
||
|
|
"""
|
||
|
|
if not TTP_INSTALLED:
|
||
|
|
msg = "\nTTP is not installed. Please PIP install ttp:\n" "pip install ttp\n"
|
||
|
|
raise ValueError(msg)
|
||
|
|
|
||
|
|
parser = ttp(template=template, **kwargs)
|
||
|
|
|
||
|
|
# get inputs load for TTP template
|
||
|
|
ttp_inputs_load = parser.get_input_load()
|
||
|
|
log.debug("run_ttp_template: inputs load - {}".format(ttp_inputs_load))
|
||
|
|
|
||
|
|
# go over template's inputs and collect output from devices
|
||
|
|
for template_name, inputs in ttp_inputs_load.items():
|
||
|
|
for input_name, input_params in inputs.items():
|
||
|
|
method = input_params.get("method", "send_command")
|
||
|
|
method_kwargs = input_params.get("kwargs", {})
|
||
|
|
commands = input_params.get("commands", None)
|
||
|
|
|
||
|
|
# run sanity checks
|
||
|
|
if method not in dir(connection):
|
||
|
|
log.warning(
|
||
|
|
"run_ttp_template: '{}' input, unsupported method '{}', skipping".format(
|
||
|
|
input_name, method
|
||
|
|
)
|
||
|
|
)
|
||
|
|
continue
|
||
|
|
elif not commands:
|
||
|
|
log.warning(
|
||
|
|
"run_ttp_template: '{}' input no commands to collect, skipping".format(
|
||
|
|
input_name
|
||
|
|
)
|
||
|
|
)
|
||
|
|
continue
|
||
|
|
|
||
|
|
# collect commands output from device
|
||
|
|
out_list = [
|
||
|
|
getattr(connection, method)(command_string=command, **method_kwargs)
|
||
|
|
for command in commands
|
||
|
|
]
|
||
|
|
output = "\n".join(out_list)
|
||
|
|
|
||
|
|
# add collected output to TTP parser object
|
||
|
|
parser.add_input(
|
||
|
|
data=output, input_name=input_name, template_name=template_name
|
||
|
|
)
|
||
|
|
|
||
|
|
# run parsing in single process
|
||
|
|
parser.parse(one=True)
|
||
|
|
|
||
|
|
return parser.result(**res_kwargs)
|
||
|
|
|
||
|
|
|
||
|
|
def get_structured_data_genie(
|
||
|
|
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")
|
||
|
|
|
||
|
|
if not GENIE_INSTALLED:
|
||
|
|
msg = (
|
||
|
|
"\nGenie and PyATS are not installed. Please PIP install both Genie and PyATS:\n"
|
||
|
|
"pip install genie\npip install pyats\n"
|
||
|
|
)
|
||
|
|
raise ValueError(msg)
|
||
|
|
|
||
|
|
if "cisco" not in platform and "linux" not in platform:
|
||
|
|
return raw_output
|
||
|
|
|
||
|
|
genie_device_mapper = {
|
||
|
|
"cisco_ios": "ios",
|
||
|
|
"cisco_xe": "iosxe",
|
||
|
|
"cisco_xr": "iosxr",
|
||
|
|
"cisco_nxos": "nxos",
|
||
|
|
"cisco_asa": "asa",
|
||
|
|
"linux": "linux",
|
||
|
|
"corelight_linux": "linux",
|
||
|
|
"f5_linux": "linux",
|
||
|
|
"ovs_linux": "linux",
|
||
|
|
}
|
||
|
|
|
||
|
|
os = None
|
||
|
|
# platform might be _ssh, _telnet, _serial strip that off
|
||
|
|
if platform.count("_") > 1:
|
||
|
|
base_list = platform.split("_")[:-1]
|
||
|
|
base_platform = "_".join(base_list)
|
||
|
|
else:
|
||
|
|
base_platform = platform
|
||
|
|
|
||
|
|
os = genie_device_mapper.get(base_platform)
|
||
|
|
if os is None:
|
||
|
|
return raw_output
|
||
|
|
|
||
|
|
# Genie specific construct for doing parsing (based on Genie in Ansible)
|
||
|
|
device = Device("new_device", os=os)
|
||
|
|
device.custom.setdefault("abstraction", {})
|
||
|
|
device.custom["abstraction"]["order"] = ["os"]
|
||
|
|
device.cli = AttrDict({"execute": None})
|
||
|
|
try:
|
||
|
|
# Test whether there is a parser for given command (return Exception if fails)
|
||
|
|
get_parser(command, device)
|
||
|
|
parsed_output: Dict[str, Any] = device.parse(command, output=raw_output)
|
||
|
|
return parsed_output
|
||
|
|
except Exception as exception:
|
||
|
|
if raise_parsing_error:
|
||
|
|
raise NetmikoParsingException(
|
||
|
|
f"Failed to parse CLI output using Genie\n{exception}"
|
||
|
|
)
|
||
|
|
return raw_output
|
||
|
|
|
||
|
|
|
||
|
|
def structured_data_converter(
|
||
|
|
raw_data: str,
|
||
|
|
command: str,
|
||
|
|
platform: str,
|
||
|
|
use_textfsm: bool = False,
|
||
|
|
use_ttp: bool = False,
|
||
|
|
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 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,
|
||
|
|
raise_parsing_error=raise_parsing_error,
|
||
|
|
)
|
||
|
|
if not isinstance(structured_output_tfsm, str):
|
||
|
|
return structured_output_tfsm
|
||
|
|
|
||
|
|
if use_ttp:
|
||
|
|
if ttp_template is None:
|
||
|
|
msg = """
|
||
|
|
The argument 'ttp_template=/path/to/template.ttp' must be set when use_ttp=True
|
||
|
|
"""
|
||
|
|
raise ValueError(msg)
|
||
|
|
else:
|
||
|
|
structured_output_ttp = get_structured_data_ttp(
|
||
|
|
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,
|
||
|
|
raise_parsing_error=raise_parsing_error,
|
||
|
|
)
|
||
|
|
if not isinstance(structured_output_genie, str):
|
||
|
|
return structured_output_genie
|
||
|
|
return raw_data
|
||
|
|
|
||
|
|
|
||
|
|
def select_cmd_verify(func: F) -> F:
|
||
|
|
"""Override function cmd_verify argument with global setting."""
|
||
|
|
|
||
|
|
@functools.wraps(func)
|
||
|
|
def wrapper_decorator(self: "BaseConnection", *args: Any, **kwargs: Any) -> Any:
|
||
|
|
if self.global_cmd_verify is not None:
|
||
|
|
kwargs["cmd_verify"] = self.global_cmd_verify
|
||
|
|
return func(self, *args, **kwargs)
|
||
|
|
|
||
|
|
return cast(F, wrapper_decorator)
|
||
|
|
|
||
|
|
|
||
|
|
def m_exec_time(func: F) -> F:
|
||
|
|
@functools.wraps(func)
|
||
|
|
def wrapper_decorator(self: Any, *args: Any, **kwargs: Any) -> Any:
|
||
|
|
start_time = datetime.now()
|
||
|
|
result = func(self, *args, **kwargs)
|
||
|
|
end_time = datetime.now()
|
||
|
|
method_name = str(func)
|
||
|
|
print(f"{method_name}: Elapsed time: {end_time - start_time}")
|
||
|
|
return result
|
||
|
|
|
||
|
|
return cast(F, wrapper_decorator)
|
||
|
|
|
||
|
|
|
||
|
|
def f_exec_time(func: F) -> F:
|
||
|
|
@functools.wraps(func)
|
||
|
|
def wrapper_decorator(*args: Any, **kwargs: Any) -> Any:
|
||
|
|
start_time = datetime.now()
|
||
|
|
result = func(*args, **kwargs)
|
||
|
|
end_time = datetime.now()
|
||
|
|
print(f"Elapsed time: {end_time - start_time}")
|
||
|
|
return result
|
||
|
|
|
||
|
|
return cast(F, wrapper_decorator)
|
||
|
|
|
||
|
|
|
||
|
|
def calc_old_timeout(
|
||
|
|
max_loops: Optional[int] = None,
|
||
|
|
delay_factor: Optional[float] = None,
|
||
|
|
loop_delay: float = 0.2,
|
||
|
|
old_timeout: int = 100,
|
||
|
|
) -> float:
|
||
|
|
"""
|
||
|
|
loop_delay is .2 in Netmiko 3.x
|
||
|
|
delay_factor would multiple the loop delay
|
||
|
|
Number of loops was typically 500
|
||
|
|
|
||
|
|
Thus each loop would sleep (loop_delay * delay_factor) seconds
|
||
|
|
That sleep would happen max_loops time
|
||
|
|
|
||
|
|
Formula is (loop_delay * delay_factor) * max_loops
|
||
|
|
|
||
|
|
There was a way Netmiko's self.timeout could override the default settings and essentially be
|
||
|
|
used to adjust max_loops (this was probably rarely used).
|
||
|
|
"""
|
||
|
|
if max_loops is None:
|
||
|
|
max_loops = 500
|
||
|
|
if delay_factor is None:
|
||
|
|
delay_factor = 1.0
|
||
|
|
# This is the logic for having self.timeout override max_loops
|
||
|
|
if delay_factor == 1 and max_loops == 500:
|
||
|
|
max_loops = int(old_timeout / loop_delay)
|
||
|
|
|
||
|
|
return max_loops * loop_delay * delay_factor
|
||
|
|
|
||
|
|
|
||
|
|
def nokia_context_filter(data: str, re_flags: int = re.M) -> str:
|
||
|
|
"""
|
||
|
|
Nokia context from string. Examples:
|
||
|
|
|
||
|
|
(ro)[]
|
||
|
|
|
||
|
|
(ex)[configure router "Base" bgp]
|
||
|
|
|
||
|
|
Converted over to a standalone function for easier unit testing.
|
||
|
|
"""
|
||
|
|
context_pattern = r"^\!?\*?(\((ex|gl|pr|ro)\))?\[.*\]"
|
||
|
|
return re.sub(context_pattern, "", data, flags=re_flags)
|