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:
@@ -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).
|
||||||
|
|||||||
50
hyperglass/configuration/validation.py
Normal file
50
hyperglass/configuration/validation.py
Normal 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
|
||||||
@@ -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[
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
Reference in New Issue
Block a user