From b97e29d50b3794462ff039527a329d47c58c593e Mon Sep 17 00:00:00 2001 From: checktheroads Date: Mon, 2 Nov 2020 23:08:07 -0700 Subject: [PATCH] fix telnet support, closes #93 --- hyperglass/configuration/main.py | 67 +++------------------ hyperglass/configuration/validation.py | 50 +++++++++++++++ hyperglass/execution/drivers/ssh_netmiko.py | 5 ++ hyperglass/models/config/devices.py | 20 +++--- hyperglass/util/__init__.py | 4 +- 5 files changed, 75 insertions(+), 71 deletions(-) create mode 100644 hyperglass/configuration/validation.py diff --git a/hyperglass/configuration/main.py b/hyperglass/configuration/main.py index a513f18..1bf4feb 100644 --- a/hyperglass/configuration/main.py +++ b/hyperglass/configuration/main.py @@ -4,12 +4,11 @@ import os import copy import json -from typing import Dict, List, Union, Callable +from typing import Dict from pathlib import Path # Third Party import yaml -from pydantic import ValidationError # Project from hyperglass.log import ( @@ -19,15 +18,12 @@ from hyperglass.log import ( enable_syslog_logging, ) from hyperglass.util import check_path, set_app_path, set_cache_env, current_log_level -from hyperglass.models import HyperglassModel from hyperglass.constants import ( - TRANSPORT_REST, SUPPORTED_QUERY_TYPES, PARSED_RESPONSE_FIELDS, - SUPPORTED_STRUCTURED_OUTPUT, __version__, ) -from hyperglass.exceptions import ConfigError, ConfigInvalid, ConfigMissing +from hyperglass.exceptions import ConfigError, ConfigMissing from hyperglass.models.commands import Commands from hyperglass.models.config.params import Params from hyperglass.models.config.devices import Devices @@ -40,6 +36,7 @@ from hyperglass.configuration.defaults import ( # Local from .markdown import get_markdown +from .validation import validate_config, validate_nos_commands set_app_path(required=True) @@ -55,18 +52,8 @@ CONFIG_FILES = ( ) -def _check_config_files(directory): - """Verify config files exist and are readable. - - Arguments: - directory {Path} -- Config directory Path object - - Raises: - ConfigMissing: Raised if a required config file does not pass checks. - - Returns: - {tuple} -- main config, devices config, commands config - """ +def _check_config_files(directory: Path): + """Verify config files exist and are readable.""" files = () for file in CONFIG_FILES: file_name, required = file @@ -124,42 +111,6 @@ def _config_optional(config_path: Path) -> Dict: return config -def _validate_nos_commands(all_nos, commands): - nos_with_commands = commands.dict().keys() - - for nos in all_nos: - valid = False - if nos in SUPPORTED_STRUCTURED_OUTPUT: - valid = True - elif nos in TRANSPORT_REST: - valid = True - elif nos in nos_with_commands: - valid = True - - if not valid: - raise ConfigError( - '"{nos}" is used on a device, ' - + 'but no command profile for "{nos}" is defined.', - nos=nos, - ) - - return True - - -def _validate_config(config: Union[Dict, List], importer: Callable) -> HyperglassModel: - validated = None - try: - if isinstance(config, Dict): - validated = importer(**config) - elif isinstance(config, List): - validated = importer(config) - except ValidationError as err: - log.error(str(err)) - raise ConfigInvalid(err.errors()) from None - - return validated - - user_config = _config_optional(CONFIG_MAIN) # Read raw debug value from config to enable debugging quickly. @@ -167,7 +118,7 @@ set_log_level(logger=log, debug=user_config.get("debug", True)) # Map imported user configuration to expected schema. log.debug("Unvalidated configuration from {}: {}", CONFIG_MAIN, user_config) -params = _validate_config(config=user_config, importer=Params) +params = validate_config(config=user_config, importer=Params) # Re-evaluate debug state after config is validated log_level = current_log_level(log) @@ -180,15 +131,15 @@ elif not params.debug and log_level == "debug": # Map imported user commands to expected schema. _user_commands = _config_optional(CONFIG_COMMANDS) log.debug("Unvalidated commands from {}: {}", CONFIG_COMMANDS, _user_commands) -commands = _validate_config(config=_user_commands, importer=Commands.import_params) +commands = validate_config(config=_user_commands, importer=Commands.import_params) # Map imported user devices to expected schema. _user_devices = _config_required(CONFIG_DEVICES) log.debug("Unvalidated devices from {}: {}", CONFIG_DEVICES, _user_devices) -devices = _validate_config(config=_user_devices.get("routers", []), importer=Devices) +devices = validate_config(config=_user_devices.get("routers", []), importer=Devices) # Validate commands are both supported and properly mapped. -_validate_nos_commands(devices.all_nos, commands) +validate_nos_commands(devices.all_nos, commands) # Set cache configurations to environment variables, so they can be # used without importing this module (Gunicorn, etc). diff --git a/hyperglass/configuration/validation.py b/hyperglass/configuration/validation.py new file mode 100644 index 0000000..900cb93 --- /dev/null +++ b/hyperglass/configuration/validation.py @@ -0,0 +1,50 @@ +"""Post-Validation Validation. + +Some validations need to occur across multiple config files. +""" +# Standard Library +from typing import Dict, List, Union, Callable + +# Third Party +from pydantic import ValidationError + +# Project +from hyperglass.log import log +from hyperglass.models import HyperglassModel +from hyperglass.constants import TRANSPORT_REST, SUPPORTED_STRUCTURED_OUTPUT +from hyperglass.exceptions import ConfigError, ConfigInvalid +from hyperglass.models.commands import Commands + + +def validate_nos_commands(all_nos: List[str], commands: Commands) -> bool: + """Ensure defined devices have associated commands.""" + custom_commands = commands.dict().keys() + + for nos in all_nos: + valid = False + if nos in (*SUPPORTED_STRUCTURED_OUTPUT, *TRANSPORT_REST, *custom_commands): + valid = True + + if not valid: + raise ConfigError( + '"{nos}" is used on a device, ' + + 'but no command profile for "{nos}" is defined.', + nos=nos, + ) + + return True + + +def validate_config(config: Union[Dict, List], importer: Callable) -> HyperglassModel: + """Validate a config dict against a model.""" + validated = None + try: + if isinstance(config, Dict): + validated = importer(**config) + elif isinstance(config, List): + validated = importer(config) + except ValidationError as err: + log.error(str(err)) + raise ConfigInvalid(err.errors()) from None + + return validated diff --git a/hyperglass/execution/drivers/ssh_netmiko.py b/hyperglass/execution/drivers/ssh_netmiko.py index 88a65a7..6ed33cb 100644 --- a/hyperglass/execution/drivers/ssh_netmiko.py +++ b/hyperglass/execution/drivers/ssh_netmiko.py @@ -71,6 +71,11 @@ class NetmikoConnection(SSHConnection): **global_args, } + if "_telnet" in self.device.nos: + # Telnet devices with a low delay factor (default) tend to + # throw login errors. + driver_kwargs["global_delay_factor"] = 2 + if self.device.credential._method == "password": # Use password auth if no key is defined. driver_kwargs[ diff --git a/hyperglass/models/config/devices.py b/hyperglass/models/config/devices.py index c8a804c..919305d 100644 --- a/hyperglass/models/config/devices.py +++ b/hyperglass/models/config/devices.py @@ -155,18 +155,16 @@ class Device(HyperglassModel): return value @validator("commands", always=True) - def validate_commands(cls, value, values): - """If a named command profile is not defined, use the NOS name. - - Arguments: - value {str} -- Reference to command profile - values {dict} -- Other already-validated fields - - Returns: - {str} -- Command profile or NOS name - """ + def validate_commands(cls, value: str, values: "Device") -> str: + """If a named command profile is not defined, use the NOS name.""" if value is None: value = values["nos"] + + # If the _telnet prefix is added, remove it from the command + # profile so the commands are the same regardless of + # protocol. + if "_telnet" in value: + value = value.replace("_telnet", "") return value @validator("vrfs", pre=True) @@ -282,7 +280,7 @@ class Devices(HyperglassModelExtra): # classes, for when iteration over all routers is required. hostnames.add(device.name) objects.add(device) - all_nos.add(device.nos) + all_nos.add(device.commands) for vrf in device.vrfs: diff --git a/hyperglass/util/__init__.py b/hyperglass/util/__init__.py index d2b3ef0..9ae0a64 100644 --- a/hyperglass/util/__init__.py +++ b/hyperglass/util/__init__.py @@ -868,14 +868,14 @@ def make_repr(_class): def validate_nos(nos): """Validate device NOS is supported.""" # Third Party - from netmiko.ssh_dispatcher import CLASS_MAPPER_BASE + from netmiko.ssh_dispatcher import CLASS_MAPPER # Project from hyperglass.constants import DRIVER_MAP result = (False, None) - all_nos = {*DRIVER_MAP.keys(), *CLASS_MAPPER_BASE.keys()} + all_nos = {*DRIVER_MAP.keys(), *CLASS_MAPPER.keys()} if nos in all_nos: result = (True, DRIVER_MAP.get(nos, "netmiko"))