diff --git a/hyperglass/configuration/main.py b/hyperglass/configuration/main.py index 8efaf1b..5710462 100644 --- a/hyperglass/configuration/main.py +++ b/hyperglass/configuration/main.py @@ -4,7 +4,7 @@ import os import copy import json -from typing import Dict +from typing import Dict, List from pathlib import Path # Third Party @@ -220,6 +220,7 @@ def _build_frontend_devices(): { "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 } @@ -235,6 +236,7 @@ def _build_frontend_devices(): { "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 } @@ -246,15 +248,8 @@ def _build_frontend_devices(): return frontend_dict -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 - """ +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})) @@ -269,8 +264,9 @@ def _build_networks(): "network": device.network.display_name, "vrfs": [ { - "id": vrf.name, + "_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 } @@ -285,33 +281,13 @@ def _build_networks(): return networks -def _build_vrfs(): - vrfs = [] - for device in devices.objects: - for vrf in device.vrfs: - - vrf_dict = { - "id": vrf.name, - "display_name": vrf.display_name, - } - - if vrf_dict not in vrfs: - vrfs.append(vrf_dict) - - return vrfs - - content_params = json.loads( params.json(include={"primary_asn", "org_name", "site_title", "site_description"}) ) -def _build_vrf_help(): - """Build a dict of vrfs as keys, help content as values. - - Returns: - {dict} -- Formatted VRF help - """ +def _build_vrf_help() -> Dict: + """Build a dict of vrfs as keys, help content as values.""" all_help = {} for vrf in devices.vrf_objects: @@ -343,7 +319,7 @@ def _build_vrf_help(): } ) - all_help.update({vrf.name: vrf_help}) + all_help.update({vrf._id: vrf_help}) return all_help @@ -369,7 +345,6 @@ content_terms = get_markdown( ) content_credit = CREDIT.format(version=__version__) -vrfs = _build_vrfs() networks = _build_networks() frontend_devices = _build_frontend_devices() _include_fields = { @@ -397,7 +372,6 @@ _frontend_params.update( "hyperglass_version": __version__, "queries": {**params.queries.map, "list": params.queries.list}, "networks": networks, - "vrfs": vrfs, "parsed_data_fields": PARSED_RESPONSE_FIELDS, "content": { "help_menu": content_help, diff --git a/hyperglass/models/api/query.py b/hyperglass/models/api/query.py index bacf848..6fc130b 100644 --- a/hyperglass/models/api/query.py +++ b/hyperglass/models/api/query.py @@ -21,33 +21,24 @@ from .validators import ( validate_community_input, validate_community_select, ) +from ..config.vrf import Vrf -def get_vrf_object(vrf_name): - """Match VRF object from VRF name. +def get_vrf_object(vrf_name: str) -> Vrf: + """Match VRF object from VRF name.""" - Arguments: - vrf_name {str} -- VRF name - - Raises: - InputInvalid: Raised if no VRF is matched. - - Returns: - {object} -- Valid VRF object - """ - matched = None for vrf_obj in devices.vrf_objects: if vrf_name is not None: - if vrf_name == vrf_obj.name or vrf_name == vrf_obj.display_name: - matched = vrf_obj - break + if vrf_name == vrf_obj._id or vrf_name == vrf_obj.display_name: + return vrf_obj + + elif vrf_name == "__hyperglass_default" and vrf_obj.default: + return vrf_obj elif vrf_name is None: - if vrf_obj.name == "default": - matched = vrf_obj - break - if matched is None: - raise InputInvalid(params.messages.vrf_not_found, vrf_name=vrf_name) - return matched + if vrf_obj.default: + return vrf_obj + + raise InputInvalid(params.messages.vrf_not_found, vrf_name=vrf_name) class Query(BaseModel): @@ -145,7 +136,7 @@ class Query(BaseModel): items = { "query_location": self.query_location, "query_type": self.query_type, - "query_vrf": self.query_vrf.name, + "query_vrf": self.query_vrf._id, "query_target": str(self.query_target), } return items @@ -156,17 +147,7 @@ class Query(BaseModel): @validator("query_type") def validate_query_type(cls, value): - """Ensure query_type is enabled. - - Arguments: - value {str} -- Query Type - - Raises: - InputInvalid: Raised if query_type is disabled. - - Returns: - {str} -- Valid query_type - """ + """Ensure query_type is enabled.""" query = params.queries[value] if not query.enable: raise InputInvalid( @@ -178,17 +159,7 @@ class Query(BaseModel): @validator("query_location") def validate_query_location(cls, value): - """Ensure query_location is defined. - - Arguments: - value {str} -- Unvalidated query_location - - Raises: - InputInvalid: Raised if query_location is not defined. - - Returns: - {str} -- Valid query_location - """ + """Ensure query_location is defined.""" if value not in devices._ids: raise InputInvalid( params.messages.invalid_field, @@ -200,17 +171,7 @@ class Query(BaseModel): @validator("query_vrf") def validate_query_vrf(cls, value, values): - """Ensure query_vrf is defined. - - Arguments: - value {str} -- Unvalidated query_vrf - - Raises: - InputInvalid: Raised if query_vrf is not defined. - - Returns: - {str} -- Valid query_vrf - """ + """Ensure query_vrf is defined.""" vrf_object = get_vrf_object(value) device = devices[values["query_location"]] device_vrf = None diff --git a/hyperglass/models/config/devices.py b/hyperglass/models/config/devices.py index de37828..e57f90e 100644 --- a/hyperglass/models/config/devices.py +++ b/hyperglass/models/config/devices.py @@ -237,7 +237,7 @@ class Device(HyperglassModel): """ vrfs = [] for vrf in value: - vrf_name = vrf.get("name") + vrf_default = vrf.get("default", False) for afi in ("ipv4", "ipv6"): vrf_afi = vrf.get(afi) @@ -259,9 +259,7 @@ class Device(HyperglassModel): # 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 - ): + if not vrf_default and not isinstance(vrf.get("display_name"), str): new_name = vrf["name"] new_name = re.sub(r"[^a-zA-Z0-9]", " ", new_name) new_name = re.split(" ", new_name) @@ -272,7 +270,7 @@ class Device(HyperglassModel): f"Generated '{vrf['display_name']}'" ) - elif vrf_name == "default" and vrf.get("display_name") is None: + elif vrf_default and vrf.get("display_name") is None: vrf["display_name"] = "Global" # Validate the non-default VRF against the standard diff --git a/hyperglass/models/config/vrf.py b/hyperglass/models/config/vrf.py index 1e678dc..f3d7569 100644 --- a/hyperglass/models/config/vrf.py +++ b/hyperglass/models/config/vrf.py @@ -1,7 +1,8 @@ """Validate VRF configuration variables.""" # Standard Library -from typing import List, Optional +import re +from typing import Dict, List, Optional from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network # Third Party @@ -10,18 +11,39 @@ from pydantic import ( FilePath, StrictStr, StrictBool, + PrivateAttr, conint, constr, validator, root_validator, ) +# Project +from hyperglass.log import log + # Local from ..main import HyperglassModel, HyperglassModelExtra ACLAction = constr(regex=r"permit|deny") +def find_vrf_id(values: Dict) -> str: + """Generate (private) VRF ID.""" + + def generate_id(name: str) -> str: + scrubbed = re.sub(r"[^A-Za-z0-9\_\-\s]", "", name) + return "_".join(scrubbed.split()).lower() + + display_name = values.get("display_name") + + if display_name is None: + raise ValueError("display_name is required.") + + vrf_id = generate_id(display_name) + + return vrf_id + + class AccessList4(HyperglassModel): """Validation model for IPv4 access-lists.""" @@ -195,23 +217,33 @@ class DeviceVrf6(HyperglassModelExtra): class Vrf(HyperglassModel): """Validation model for per VRF/afi config in devices.yaml.""" + _id: StrictStr = PrivateAttr() name: StrictStr display_name: StrictStr info: Info = Info() ipv4: Optional[DeviceVrf4] ipv6: Optional[DeviceVrf6] + default: StrictBool = False + + def __init__(self, **kwargs) -> None: + """Set the VRF ID.""" + _id = find_vrf_id(kwargs) + super().__init__(**kwargs) + self._id = _id @root_validator - def set_dynamic(cls, values): - """Set dynamic attributes before VRF initialization. + def set_dynamic(cls, values: Dict) -> Dict: + """Set dynamic attributes before VRF initialization.""" - Arguments: - values {dict} -- Post-validation VRF attributes - - Returns: - {dict} -- VRF with new attributes set - """ if values["name"] == "default": + log.warning( + """You have set the VRF name to 'default'. This is no longer the way to +designate a VRF as the default (or global routing table) VRF. Instead, +add 'default: true' to the VRF definition. +""" + ) + + if values.get("default", False) is True: protocol4 = "ipv4_default" protocol6 = "ipv6_default" @@ -227,7 +259,7 @@ class Vrf(HyperglassModel): values["ipv6"].protocol = protocol6 values["ipv6"].version = 6 - if values.get("name") == "default" and values.get("display_name") is None: + if values.get("default", False) and values.get("display_name") is None: values["display_name"] = "Global" return values @@ -245,7 +277,7 @@ class Vrf(HyperglassModel): {object} -- AFI object """ if i not in (4, 6): - raise AttributeError(f"Must be 4 or 6, got '{i}") + raise AttributeError(f"Must be 4 or 6, got '{i}'") return getattr(self, f"ipv{i}") diff --git a/hyperglass/ui/components/form/queryVrf.tsx b/hyperglass/ui/components/form/queryVrf.tsx index 80d5ec8..4c7d2ed 100644 --- a/hyperglass/ui/components/form/queryVrf.tsx +++ b/hyperglass/ui/components/form/queryVrf.tsx @@ -2,11 +2,11 @@ import { useMemo } from 'react'; import { Select } from '~/components'; import { useLGMethods, useLGState } from '~/hooks'; -import { TDeviceVrf, TSelectOption } from '~/types'; +import type { TDeviceVrf, TSelectOption } from '~/types'; import type { TQueryVrf } from './types'; function buildOptions(queryVrfs: TDeviceVrf[]): TSelectOption[] { - return queryVrfs.map(q => ({ value: q.id, label: q.display_name })); + return queryVrfs.map(q => ({ value: q._id, label: q.display_name })); } export const QueryVrf: React.FC = (props: TQueryVrf) => { diff --git a/hyperglass/ui/components/lookingGlass.tsx b/hyperglass/ui/components/lookingGlass.tsx index 541029b..6aef729 100644 --- a/hyperglass/ui/components/lookingGlass.tsx +++ b/hyperglass/ui/components/lookingGlass.tsx @@ -129,17 +129,14 @@ export const LookingGlass: React.FC = () => { // Use _.intersectionWith to create an array of VRFs common to all selected locations. const intersecting = intersectionWith( ...allVrfs, - (a: TDeviceVrf, b: TDeviceVrf) => a.id === b.id, + (a: TDeviceVrf, b: TDeviceVrf) => a._id === b._id, ); availVrfs.set(intersecting); // If there are no intersecting VRFs, use the default VRF. - if ( - intersecting.filter(i => i.id === queryVrf.value).length === 0 && - queryVrf.value !== 'default' - ) { - queryVrf.set('default'); + if (intersecting.filter(i => i._id === queryVrf.value).length === 0) { + queryVrf.set('__hyperglass_default'); } // Determine which address families are available in the intersecting VRFs. diff --git a/hyperglass/ui/components/results/tags.tsx b/hyperglass/ui/components/results/tags.tsx index a807e5a..26de570 100644 --- a/hyperglass/ui/components/results/tags.tsx +++ b/hyperglass/ui/components/results/tags.tsx @@ -2,7 +2,7 @@ import { Box, Stack, useToken } from '@chakra-ui/react'; import { motion, AnimatePresence } from 'framer-motion'; import { Label } from '~/components'; import { useConfig, useBreakpointValue } from '~/context'; -import { useLGState } from '~/hooks'; +import { useLGState, useVrf } from '~/hooks'; import { isQueryType } from '~/types'; import type { Transition } from 'framer-motion'; @@ -10,7 +10,7 @@ import type { Transition } from 'framer-motion'; const transition = { duration: 0.3, delay: 0.5 } as Transition; export const Tags: React.FC = () => { - const { queries, vrfs, web } = useConfig(); + const { queries, web } = useConfig(); const { queryLocation, queryTarget, queryType, queryVrf } = useLGState(); const targetBg = useToken('colors', 'teal.600'); @@ -64,8 +64,8 @@ export const Tags: React.FC = () => { queryTypeLabel = queries[queryType.value].display_name; } - const matchedVrf = - vrfs.filter(v => v.id === queryVrf.value)[0] ?? vrfs.filter(v => v.id === 'default')[0]; + const getVrf = useVrf(); + const vrf = getVrf(queryVrf.value); return ( {