1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00

fix telnet support, closes #93

This commit is contained in:
checktheroads
2020-11-02 23:08:07 -07:00
parent b6046c4411
commit b97e29d50b
5 changed files with 75 additions and 71 deletions

View File

@@ -4,12 +4,11 @@
import os import os
import copy import copy
import json import json
from typing import Dict, List, Union, Callable from typing import Dict
from pathlib import Path from pathlib import Path
# Third Party # Third Party
import yaml import yaml
from pydantic import ValidationError
# Project # Project
from hyperglass.log import ( from hyperglass.log import (
@@ -19,15 +18,12 @@ from hyperglass.log import (
enable_syslog_logging, enable_syslog_logging,
) )
from hyperglass.util import check_path, set_app_path, set_cache_env, current_log_level from hyperglass.util import check_path, set_app_path, set_cache_env, current_log_level
from hyperglass.models import HyperglassModel
from hyperglass.constants import ( from hyperglass.constants import (
TRANSPORT_REST,
SUPPORTED_QUERY_TYPES, SUPPORTED_QUERY_TYPES,
PARSED_RESPONSE_FIELDS, PARSED_RESPONSE_FIELDS,
SUPPORTED_STRUCTURED_OUTPUT,
__version__, __version__,
) )
from hyperglass.exceptions import ConfigError, ConfigInvalid, ConfigMissing from hyperglass.exceptions import ConfigError, ConfigMissing
from hyperglass.models.commands import Commands from hyperglass.models.commands import Commands
from hyperglass.models.config.params import Params from hyperglass.models.config.params import Params
from hyperglass.models.config.devices import Devices from hyperglass.models.config.devices import Devices
@@ -40,6 +36,7 @@ from hyperglass.configuration.defaults import (
# Local # Local
from .markdown import get_markdown from .markdown import get_markdown
from .validation import validate_config, validate_nos_commands
set_app_path(required=True) set_app_path(required=True)
@@ -55,18 +52,8 @@ CONFIG_FILES = (
) )
def _check_config_files(directory): def _check_config_files(directory: Path):
"""Verify config files exist and are readable. """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
"""
files = () files = ()
for file in CONFIG_FILES: for file in CONFIG_FILES:
file_name, required = file file_name, required = file
@@ -124,42 +111,6 @@ def _config_optional(config_path: Path) -> Dict:
return config 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) user_config = _config_optional(CONFIG_MAIN)
# Read raw debug value from config to enable debugging quickly. # 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. # Map imported user configuration to expected schema.
log.debug("Unvalidated configuration from {}: {}", CONFIG_MAIN, user_config) 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 # Re-evaluate debug state after config is validated
log_level = current_log_level(log) 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. # Map imported user commands to expected schema.
_user_commands = _config_optional(CONFIG_COMMANDS) _user_commands = _config_optional(CONFIG_COMMANDS)
log.debug("Unvalidated commands from {}: {}", CONFIG_COMMANDS, _user_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. # Map imported user devices to expected schema.
_user_devices = _config_required(CONFIG_DEVICES) _user_devices = _config_required(CONFIG_DEVICES)
log.debug("Unvalidated devices from {}: {}", CONFIG_DEVICES, _user_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 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 # Set cache configurations to environment variables, so they can be
# used without importing this module (Gunicorn, etc). # used without importing this module (Gunicorn, etc).

View File

@@ -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

View File

@@ -71,6 +71,11 @@ class NetmikoConnection(SSHConnection):
**global_args, **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": if self.device.credential._method == "password":
# Use password auth if no key is defined. # Use password auth if no key is defined.
driver_kwargs[ driver_kwargs[

View File

@@ -155,18 +155,16 @@ class Device(HyperglassModel):
return value return value
@validator("commands", always=True) @validator("commands", always=True)
def validate_commands(cls, value, values): def validate_commands(cls, value: str, values: "Device") -> str:
"""If a named command profile is not defined, use the NOS name. """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
"""
if value is None: if value is None:
value = values["nos"] 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 return value
@validator("vrfs", pre=True) @validator("vrfs", pre=True)
@@ -282,7 +280,7 @@ class Devices(HyperglassModelExtra):
# classes, for when iteration over all routers is required. # classes, for when iteration over all routers is required.
hostnames.add(device.name) hostnames.add(device.name)
objects.add(device) objects.add(device)
all_nos.add(device.nos) all_nos.add(device.commands)
for vrf in device.vrfs: for vrf in device.vrfs:

View File

@@ -868,14 +868,14 @@ def make_repr(_class):
def validate_nos(nos): def validate_nos(nos):
"""Validate device NOS is supported.""" """Validate device NOS is supported."""
# Third Party # Third Party
from netmiko.ssh_dispatcher import CLASS_MAPPER_BASE from netmiko.ssh_dispatcher import CLASS_MAPPER
# Project # Project
from hyperglass.constants import DRIVER_MAP from hyperglass.constants import DRIVER_MAP
result = (False, None) 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: if nos in all_nos:
result = (True, DRIVER_MAP.get(nos, "netmiko")) result = (True, DRIVER_MAP.get(nos, "netmiko"))