diff --git a/hyperglass/configuration/__init__.py b/hyperglass/configuration/__init__.py index c94a8dd..6ca6833 100644 --- a/hyperglass/configuration/__init__.py +++ b/hyperglass/configuration/__init__.py @@ -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 diff --git a/hyperglass/configuration/models/routers.py b/hyperglass/configuration/models/routers.py index 51401af..ba5e83b 100644 --- a/hyperglass/configuration/models/routers.py +++ b/hyperglass/configuration/models/routers.py @@ -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) diff --git a/hyperglass/constants.py b/hyperglass/constants.py index 4b3a259..7101c96 100644 --- a/hyperglass/constants.py +++ b/hyperglass/constants.py @@ -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 diff --git a/hyperglass/execution/execute.py b/hyperglass/execution/execute.py index f6510b7..95d0ba3 100644 --- a/hyperglass/execution/execute.py +++ b/hyperglass/execution/execute.py @@ -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( diff --git a/hyperglass/util.py b/hyperglass/util.py index 4b6c2b5..08004c5 100644 --- a/hyperglass/util.py +++ b/hyperglass/util.py @@ -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