1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00

remove requirement for default VRF to be named default, closes #29

This commit is contained in:
checktheroads
2021-02-25 23:38:57 -07:00
parent cca6b60f09
commit e4f4eb85b0
11 changed files with 120 additions and 121 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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}")

View File

@@ -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<TQueryVrf> = (props: TQueryVrf) => {

View File

@@ -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.

View File

@@ -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 (
<Box
@@ -115,7 +115,7 @@ export const Tags: React.FC = () => {
<Label
bg={vrfBg}
label={web.text.query_vrf}
value={matchedVrf.display_name}
value={vrf.display_name}
fontSize={{ base: 'xs', md: 'sm' }}
/>
</motion.div>

View File

@@ -9,3 +9,4 @@ export * from './useLGState';
export * from './useOpposingColor';
export * from './useStrf';
export * from './useTableToString';
export * from './useVrf';

View File

@@ -50,6 +50,8 @@ export type TUseDevice = (
deviceId: string,
) => TDevice;
export type TUseVrf = (vrfId: string) => TDeviceVrf;
export interface TSelections {
queryLocation: TSelectOption[] | [];
queryType: TSelectOption | null;

View File

@@ -0,0 +1,33 @@
import { useCallback, useMemo } from 'react';
import { useConfig } from '~/context';
import type { TDeviceVrf } from '~/types';
import type { TUseVrf } from './types';
/**
* Get a VRF configuration from the global configuration context based on its name.
*/
export function useVrf(): TUseVrf {
const { networks } = useConfig();
const vrfs = useMemo(() => networks.map(n => n.locations.map(l => l.vrfs).flat()).flat(), []);
function getVrf(id: string): TDeviceVrf {
const matching = vrfs.find(vrf => vrf._id === id);
if (typeof matching === 'undefined') {
if (id === '__hyperglass_default') {
const anyDefault = vrfs.find(vrf => vrf.default === true);
if (typeof anyDefault !== 'undefined') {
return anyDefault;
} else {
throw new Error(`No matching VRF found for '${id}'`);
}
} else {
throw new Error(`No matching VRF found for '${id}'`);
}
}
return matching;
}
return useCallback(getVrf, []);
}

View File

@@ -105,8 +105,9 @@ export interface TConfigQueries {
}
interface TDeviceVrfBase {
id: string;
_id: string;
display_name: string;
default: boolean;
}
export interface TDeviceVrf extends TDeviceVrfBase {