def _build_networks() -> List[Dict]: """Build filtered JSON Structure of networks & devices for Jinja templates.""" networks = [] _networks = list( set({device.network.display_name for device in devices.objects})) for _network in _networks: network_def = {"display_name": _network, "locations": []} for device in devices.objects: if device.network.display_name == _network: network_def["locations"].append({ "_id": device._id, "name": device.name, "network": device.network.display_name, "vrfs": [ { "_id": vrf._id, "display_name": vrf.display_name, "default": vrf.default, "ipv4": True if vrf.ipv4 else False, # noqa: IF100 "ipv6": True if vrf.ipv6 else False, # noqa: IF100 } for vrf in device.vrfs ], }) networks.append(network_def) if not networks: raise ConfigError( error_msg="Unable to build network to device mapping") return networks
async def response(self): """Initiate query validation and execution.""" device = getattr(devices, self.query_location) 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 connect = Connect(device, self.query_data, transport) if supported and transport == "rest": output = await connect.rest() 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(params.messages.no_output, device_name=device.display_name) log.debug( f"Output for query: {self.query_data.json()}:\n{repr(output)}") return output
def _build_frontend_devices(): """Build filtered JSON structure of devices for frontend. Schema: { "device.name": { "display_name": "device.display_name", "vrfs": [ "Global", "vrf.display_name" ] } } Raises: ConfigError: Raised if parsing/building error occurs. Returns: {dict} -- Frontend devices """ frontend_dict = {} for device in devices.objects: if device.name in frontend_dict: frontend_dict[device.name].update({ "network": device.network.display_name, "display_name": device.display_name, "vrfs": [ { "id": vrf.name, "display_name": vrf.display_name, "default": vrf.default, "ipv4": True if vrf.ipv4 else False, # noqa: IF100 "ipv6": True if vrf.ipv6 else False, # noqa: IF100 } for vrf in device.vrfs ], }) elif device.name not in frontend_dict: frontend_dict[device.name] = { "network": device.network.display_name, "display_name": device.display_name, "vrfs": [ { "id": vrf.name, "display_name": vrf.display_name, "default": vrf.default, "ipv4": True if vrf.ipv4 else False, # noqa: IF100 "ipv6": True if vrf.ipv6 else False, # noqa: IF100 } for vrf in device.vrfs ], } if not frontend_dict: raise ConfigError( error_msg="Unable to build network to device mapping") return frontend_dict
def _config_required(config_path: Path) -> dict: try: with config_path.open("r") as cf: config = yaml.safe_load(cf) log.debug("Unvalidated data from file '{f}': {c}", f=str(config_path), c=config) except (yaml.YAMLError, yaml.MarkedYAMLError) as yaml_error: raise ConfigError(str(yaml_error)) return config
def validate_address(cls, value, values): """Ensure a hostname is resolvable.""" if not isinstance(value, (IPv4Address, IPv6Address)): if not any(resolve_hostname(value)): raise ConfigError( "Device '{d}' has an address of '{a}', which is not resolvable.", d=values["name"], a=value, ) return value
def _config_required(config_path: Path) -> Dict: try: with config_path.open("r") as cf: config = yaml.safe_load(cf) except (yaml.YAMLError, yaml.MarkedYAMLError) as yaml_error: raise ConfigError(str(yaml_error)) if config is None: log.critical("{} appears to be empty", str(config_path)) raise ConfigMissing(missing_item=config_path.name) return config
def _config_optional(config_path: Path) -> Dict: if config_path is None: config = {} else: try: with config_path.open("r") as cf: config = yaml.safe_load(cf) or {} except (yaml.YAMLError, yaml.MarkedYAMLError) as yaml_error: raise ConfigError(error_msg=str(yaml_error)) return config
def _config_optional(config_path: Path) -> dict: if config_path is None: config = {} else: try: with config_path.open("r") as cf: config = yaml.safe_load(cf) or {} log.debug( "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)) return config
def validate_structured_output(cls, value: bool, values: Dict) -> bool: """Validate structured output is supported on the device & set a default.""" if value is True and values["nos"] not in SUPPORTED_STRUCTURED_OUTPUT: raise ConfigError( "The 'structured_output' field is set to 'true' on device '{d}' with " + "NOS '{n}', which does not support structured output", d=values["name"], n=values["nos"], ) elif value is None and values["nos"] in SUPPORTED_STRUCTURED_OUTPUT: value = True else: value = False return value
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 _build_frontend_networks(): """Build filtered JSON structure of networks for frontend. Schema: { "device.network.display_name": { "device.name": { "display_name": "device.display_name", "vrfs": [ "Global", "vrf.display_name" ] } } } Raises: ConfigError: Raised if parsing/building error occurs. Returns: {dict} -- Frontend networks """ frontend_dict = {} for device in devices.routers: if device.network.display_name in frontend_dict: frontend_dict[device.network.display_name].update({ device.name: { "display_name": device.network.display_name, "vrfs": [vrf.display_name for vrf in device.vrfs], } }) elif device.network.display_name not in frontend_dict: frontend_dict[device.network.display_name] = { device.name: { "display_name": device.network.display_name, "vrfs": [vrf.display_name for vrf in device.vrfs], } } frontend_dict["default_vrf"] = devices.default_vrf if not frontend_dict: raise ConfigError( error_msg="Unable to build network to device mapping") return frontend_dict
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_structured_output(cls, value, values): """Validate structured output is supported on the device & set a default. Raises: ConfigError: Raised if true on a device that doesn't support structured output. Returns: {bool} -- True if hyperglass should return structured output for this device. """ if value is True and values["nos"] not in SUPPORTED_STRUCTURED_OUTPUT: raise ConfigError( "The 'structured_output' field is set to 'true' on device '{d}' with " + "NOS '{n}', which does not support structured output", d=values["name"], n=values["nos"], ) elif value is None and values["nos"] in SUPPORTED_STRUCTURED_OUTPUT: value = True else: value = False return value
def _build_networks(): """Build filtered JSON Structure of networks & devices for Jinja templates. Raises: ConfigError: Raised if parsing/building error occurs. Returns: {dict} -- Networks & devices """ networks = [] _networks = list( set({device.network.display_name for device in devices.routers})) for _network in _networks: network_def = {"display_name": _network, "locations": []} for device in devices.routers: if device.network.display_name == _network: network_def["locations"].append({ "name": device.name, "display_name": device.display_name, "network": device.network.display_name, "vrfs": [{ "id": vrf.name, "display_name": vrf.display_name } for vrf in device.vrfs], }) networks.append(network_def) if not networks: raise ConfigError( error_msg="Unable to build network to device mapping") return networks
def validate_vrfs(cls, value, values): """Validate VRF definitions. - Ensures source IP addresses are set for the default VRF (global routing table). - Initializes the default VRF with the DefaultVRF() class so that specific defaults can be set for the global routing table. - If the 'display_name' is not set for a non-default VRF, try to make one that looks pretty based on the 'name'. Arguments: value {list} -- List of VRFs values {dict} -- Other already-validated fields Raises: ConfigError: Raised if the VRF is missing a source address Returns: {list} -- List of valid VRFs """ vrfs = [] for vrf in value: vrf_name = vrf.get("name") for afi in ("ipv4", "ipv6"): vrf_afi = vrf.get(afi) # If AFI is actually defined (enabled), and if the # source_address field is not set, raise an error if vrf_afi is not None and vrf_afi.get( "source_address") is None: raise ConfigError( ("VRF '{vrf}' in router '{router}' is missing a source " "{afi} address."), vrf=vrf.get("name"), router=values.get("name"), afi=afi.replace("ip", "IP"), ) # If no display_name is set for a non-default VRF, try # to make one by replacing non-alphanumeric characters # with whitespaces and using str.title() to make each # word look "pretty". if vrf_name != "default" and not isinstance( vrf.get("display_name"), StrictStr): new_name = vrf["name"] new_name = re.sub(r"[^a-zA-Z0-9]", " ", new_name) new_name = re.split(" ", new_name) vrf["display_name"] = " ".join([w.title() for w in new_name]) log.debug( f'Field "display_name" for VRF "{vrf["name"]}" was not set. ' f"Generated '{vrf['display_name']}'") elif vrf_name == "default" and vrf.get("display_name") is None: vrf["display_name"] = "Global" # Validate the non-default VRF against the standard # Vrf() class. vrf = Vrf(**vrf) vrfs.append(vrf) return vrfs