improve supported NOS validation, should actually fix #41

This commit is contained in:
checktheroads 2020-05-30 08:48:15 -07:00
parent 3a6cc82862
commit be1c6831a4
5 changed files with 76 additions and 281 deletions

View file

@ -22,9 +22,11 @@ from hyperglass.constants import (
CREDIT,
DEFAULT_HELP,
DEFAULT_TERMS,
TRANSPORT_REST,
DEFAULT_DETAILS,
SUPPORTED_QUERY_TYPES,
PARSED_RESPONSE_FIELDS,
SUPPORTED_STRUCTURED_OUTPUT,
__version__,
)
from hyperglass.exceptions import ConfigError, ConfigInvalid, ConfigMissing
@ -93,7 +95,7 @@ def _config_required(config_path: Path) -> dict:
"Unvalidated data from file '{f}': {c}", f=str(config_path), c=config
)
except (yaml.YAMLError, yaml.MarkedYAMLError) as yaml_error:
raise ConfigError(error_msg=str(yaml_error))
raise ConfigError(str(yaml_error))
return config
@ -114,6 +116,28 @@ 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
user_config = _config_optional(CONFIG_MAIN)
# Read raw debug value from config to enable debugging quickly.
@ -136,6 +160,8 @@ except ValidationError as validation_errors:
error_msg=error["msg"],
)
_validate_nos_commands(devices.all_nos, commands)
set_cache_env(db=params.cache.database, host=params.cache.host, port=params.cache.port)
# Re-evaluate debug state after config is validated

View file

@ -8,17 +8,12 @@ from pathlib import Path
# Third Party
from pydantic import StrictInt, StrictStr, StrictBool, validator
from netmiko.ssh_dispatcher import CLASS_MAPPER_BASE as NETMIKO_SUPPORTED
# Project
from hyperglass.log import log
from hyperglass.util import clean_name
from hyperglass.util import clean_name, validate_nos
from hyperglass.models import HyperglassModel, HyperglassModelExtra
from hyperglass.constants import (
SCRAPE_HELPERS,
TRANSPORT_REST,
SUPPORTED_STRUCTURED_OUTPUT,
)
from hyperglass.constants import SCRAPE_HELPERS, SUPPORTED_STRUCTURED_OUTPUT
from hyperglass.exceptions import ConfigError, UnsupportedDevice
from hyperglass.configuration.models.ssl import Ssl
from hyperglass.configuration.models.vrfs import Vrf, Info
@ -100,8 +95,10 @@ class Router(HyperglassModel):
if value in SCRAPE_HELPERS.keys():
value = SCRAPE_HELPERS[value]
if value not in (*TRANSPORT_REST, *NETMIKO_SUPPORTED.keys()):
raise UnsupportedDevice('NOS "{n}" is not supported.', n=value)
supported, _ = validate_nos(value)
if not supported:
raise UnsupportedDevice('"{nos}" is not supported.', nos=value)
return value
@ -250,6 +247,7 @@ class Routers(HyperglassModelExtra):
networks = set()
display_vrfs = set()
vrf_objects = set()
all_nos = set()
router_objects = []
routers = Routers()
routers.hostnames = []
@ -260,64 +258,53 @@ class Routers(HyperglassModelExtra):
# Validate each router config against Router() model/schema
router = Router(**definition)
"""
Set a class attribute for each router so each router's
attributes can be accessed with `devices.router_hostname`
"""
# Set a class attribute for each router so each router's
# attributes can be accessed with `devices.router_hostname`
setattr(routers, router.name, router)
"""
Add router-level attributes (assumed to be unique) to
class lists, e.g. so all hostnames can be accessed as a
list with `devices.hostnames`, same for all router
classes, for when iteration over all routers is required.
"""
# Add router-level attributes (assumed to be unique) to
# class lists, e.g. so all hostnames can be accessed as a
# list with `devices.hostnames`, same for all router
# classes, for when iteration over all routers is required.
routers.hostnames.append(router.name)
router_objects.append(router)
all_nos.add(router.nos)
for vrf in router.vrfs:
"""
For each configured router VRF, add its name and
display_name to a class set (for automatic de-duping).
"""
# For each configured router VRF, add its name and
# display_name to a class set (for automatic de-duping).
vrfs.add(vrf.name)
display_vrfs.add(vrf.display_name)
"""
Also add the names to a router-level list so each
router's VRFs and display VRFs can be easily accessed.
"""
# Also add the names to a router-level list so each
# router's VRFs and display VRFs can be easily accessed.
router.display_vrfs.append(vrf.display_name)
router.vrf_names.append(vrf.name)
"""
Add a 'default_vrf' attribute to the devices class
which contains the configured default VRF display name.
"""
# Add a 'default_vrf' attribute to the devices class
# which contains the configured default VRF display name.
if vrf.name == "default" and not hasattr(cls, "default_vrf"):
routers.default_vrf = {
"name": vrf.name,
"display_name": vrf.display_name,
}
"""
Add the native VRF objects to a set (for automatic
de-duping), but exlcude device-specific fields.
"""
# Add the native VRF objects to a set (for automatic
# de-duping), but exlcude device-specific fields.
_copy_params = {
"deep": True,
"exclude": {"ipv4": {"source_address"}, "ipv6": {"source_address"}},
}
vrf_objects.add(vrf.copy(**_copy_params))
"""
Convert the de-duplicated sets to a standard list, add lists
as class attributes.
"""
# Convert the de-duplicated sets to a standard list, add lists
# as class attributes.
routers.vrfs = list(vrfs)
routers.display_vrfs = list(display_vrfs)
routers.vrf_objects = list(vrf_objects)
routers.networks = list(networks)
routers.all_nos = list(all_nos)
# Sort router list by router name attribute
routers.routers = sorted(router_objects, key=lambda x: x.display_name)

View file

@ -185,242 +185,7 @@ FUNC_COLOR_MAP = {
TRANSPORT_REST = ("frr", "bird")
TRANSPORT_SCRAPE = (
"a10",
"accedian",
"alcatel_aos",
"alcatel_sros",
"apresia_aeos",
"arista_eos",
"aruba_os",
"avaya_ers",
"avaya_vsp",
"brocade_fastiron",
"brocade_netiron",
"brocade_nos",
"brocade_vdx",
"brocade_vyos",
"checkpoint_gaia",
"calix_b6",
"ciena_saos",
"cisco_asa",
"cisco_ios",
"cisco_ios_telnet",
"cisco_nxos",
"cisco_s300",
"cisco_tp",
"cisco_wlc",
"cisco_xe",
"cisco_xr",
"coriant",
"dell_dnos9",
"dell_force10",
"dell_os6",
"dell_os9",
"dell_os10",
"dell_powerconnect",
"dell_isilon",
"eltex",
"enterasys",
"extreme",
"extreme_ers",
"extreme_exos",
"extreme_netiron",
"extreme_nos",
"extreme_slx",
"extreme_vdx",
"extreme_vsp",
"extreme_wing",
"f5_ltm",
"f5_tmsh",
"f5_linux",
"fortinet",
"generic_termserver",
"hp_comware",
"hp_procurve",
"huawei",
"huawei_vrpv8",
"ipinfusion_ocnos",
"juniper",
"juniper_junos",
"linux",
"mellanox",
"mrv_optiswitch",
"netapp_cdot",
"netscaler",
"ovs_linux",
"paloalto_panos",
"pluribus",
"quanta_mesh",
"rad_etx",
"ruckus_fastiron",
"ubiquiti_edge",
"ubiquiti_edgeswitch",
"vyatta_vyos",
"vyos",
"oneaccess_oneos",
)
SCRAPE_HELPERS = {
"junos": "juniper",
"ios": "cisco_ios",
}
class Supported:
"""Define items supported by hyperglass.
query_types: Supported query types used to validate Flask input.
rest: Supported REST API platforms
scrape: Supported "scrape" platforms which will be accessed via
Netmiko. List updated 07/2019.
"""
query_parameters = ("query_location", "query_type", "query_target", "query_vrf")
query_types = ("bgp_route", "bgp_community", "bgp_aspath", "ping", "traceroute")
rest = ("frr", "bird")
scrape = (
"a10",
"accedian",
"alcatel_aos",
"alcatel_sros",
"apresia_aeos",
"arista_eos",
"aruba_os",
"avaya_ers",
"avaya_vsp",
"brocade_fastiron",
"brocade_netiron",
"brocade_nos",
"brocade_vdx",
"brocade_vyos",
"checkpoint_gaia",
"calix_b6",
"ciena_saos",
"cisco_asa",
"cisco_ios",
"cisco_ios_telnet",
"cisco_nxos",
"cisco_s300",
"cisco_tp",
"cisco_wlc",
"cisco_xe",
"cisco_xr",
"coriant",
"dell_dnos9",
"dell_force10",
"dell_os6",
"dell_os9",
"dell_os10",
"dell_powerconnect",
"dell_isilon",
"eltex",
"enterasys",
"extreme",
"extreme_ers",
"extreme_exos",
"extreme_netiron",
"extreme_nos",
"extreme_slx",
"extreme_vdx",
"extreme_vsp",
"extreme_wing",
"f5_ltm",
"f5_tmsh",
"f5_linux",
"fortinet",
"generic_termserver",
"hp_comware",
"hp_procurve",
"huawei",
"huawei_vrpv8",
"ipinfusion_ocnos",
"juniper",
"juniper_junos",
"linux",
"mellanox",
"mrv_optiswitch",
"netapp_cdot",
"netscaler",
"ovs_linux",
"paloalto_panos",
"pluribus",
"quanta_mesh",
"rad_etx",
"ruckus_fastiron",
"ubiquiti_edge",
"ubiquiti_edgeswitch",
"vyatta_vyos",
"vyos",
"oneaccess_oneos",
)
@staticmethod
def is_supported(nos):
"""Verify if NOS is supported.
Arguments:
nos {str} -- NOS short name
Returns:
{bool} -- True if supported
"""
return bool(nos in Supported.rest + Supported.scrape)
@staticmethod
def is_scrape(nos):
"""Verify if NOS transport is scrape.
Arguments:
nos {str} -- NOS short name
Returns:
{bool} -- True if scrape
"""
return bool(nos in Supported.scrape)
@staticmethod
def is_rest(nos):
"""Verify if NOS transport is REST.
Arguments:
nos {str} -- NOS short name
Returns:
{bool} -- True if REST
"""
return bool(nos in Supported.rest)
@staticmethod
def is_supported_query(query_type):
"""Verify if query type is supported.
Arguments:
query_type {str} -- query type
Returns:
{bool} -- True if supported
"""
return bool(query_type in Supported.query_types)
@staticmethod
def map_transport(nos):
"""Map NOS to transport name.
Arguments:
nos {str} -- NOS short name
Returns:
{str} -- Transport name
"""
transport = None
if nos in Supported.scrape:
transport = "scrape"
elif nos in Supported.rest:
transport = "rest"
return transport

View file

@ -23,13 +23,13 @@ from netmiko import (
# Project
from hyperglass.log import log
from hyperglass.util import parse_exception
from hyperglass.util import validate_nos, parse_exception
from hyperglass.compat import _sshtunnel as sshtunnel
from hyperglass.encode import jwt_decode, jwt_encode
from hyperglass.constants import Supported
from hyperglass.exceptions import (
AuthError,
RestError,
ConfigError,
ScrapeError,
DeviceTimeout,
ResponseEmpty,
@ -413,20 +413,22 @@ class Execute:
log.debug(f"Received query for {self.query_data}")
log.debug(f"Matched device config: {device}")
supported, transport = validate_nos(device.nos)
connect = None
output = params.messages.general
transport = Supported.map_transport(device.nos)
connect = Connect(device, self.query_data, transport)
if Supported.is_rest(device.nos):
if supported and transport == "rest":
output = await connect.rest()
elif Supported.is_scrape(device.nos):
elif supported and transport == "scrape":
if device.proxy:
output = await connect.scrape_proxied()
else:
output = await connect.scrape_direct()
else:
raise ConfigError('"{nos}" is not supported.', nos=device.nos)
if output == "" or output == "\n":
raise ResponseEmpty(

View file

@ -763,3 +763,18 @@ def make_repr(_class):
yield f"{attr}={str(attr_val)}"
return f'{_class.__name__}({", ".join(_process_attrs(dir(_class)))})'
def validate_nos(nos):
"""Validate device NOS is supported."""
from hyperglass.constants import TRANSPORT_REST
from netmiko.ssh_dispatcher import CLASS_MAPPER_BASE
result = (False, None)
if nos in TRANSPORT_REST:
result = (True, "rest")
elif nos in CLASS_MAPPER_BASE.keys():
result = (True, "scrape")
return result