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

allow static bgp community definitions

This commit is contained in:
checktheroads
2020-04-18 07:57:55 -07:00
parent 400685304f
commit 2895d3ae46
7 changed files with 268 additions and 201 deletions

View File

@@ -1,5 +1,8 @@
"""Validate query configuration parameters.""" """Validate query configuration parameters."""
# Standard Library
from typing import List
# Third Party # Third Party
from pydantic import Field, StrictStr, StrictBool, constr from pydantic import Field, StrictStr, StrictBool, constr
@@ -8,25 +11,7 @@ from hyperglass.models import HyperglassModel
from hyperglass.constants import SUPPORTED_QUERY_TYPES from hyperglass.constants import SUPPORTED_QUERY_TYPES
class HyperglassLevel3(HyperglassModel): class BgpCommunityPattern(HyperglassModel):
"""Automatic docs sorting subclass."""
class Config:
"""Pydantic model configuration."""
schema_extra = {"level": 3}
class HyperglassLevel4(HyperglassModel):
"""Automatic docs sorting subclass."""
class Config:
"""Pydantic model configuration."""
schema_extra = {"level": 4}
class BgpCommunityPattern(HyperglassLevel4):
"""Validation model for bgp_community regex patterns.""" """Validation model for bgp_community regex patterns."""
decimal: StrictStr = Field( decimal: StrictStr = Field(
@@ -54,7 +39,7 @@ class BgpCommunityPattern(HyperglassLevel4):
) )
class BgpAsPathPattern(HyperglassLevel4): class BgpAsPathPattern(HyperglassModel):
"""Validation model for bgp_aspath regex patterns.""" """Validation model for bgp_aspath regex patterns."""
mode: constr(regex=r"asplain|asdot") = Field( mode: constr(regex=r"asplain|asdot") = Field(
@@ -82,7 +67,15 @@ class BgpAsPathPattern(HyperglassLevel4):
) )
class BgpCommunity(HyperglassLevel3): class Community(HyperglassModel):
"""Validation model for bgp_community communities."""
display_name: StrictStr
description: StrictStr
community: StrictStr
class BgpCommunity(HyperglassModel):
"""Validation model for bgp_community configuration.""" """Validation model for bgp_community configuration."""
enable: StrictBool = Field( enable: StrictBool = Field(
@@ -96,9 +89,11 @@ class BgpCommunity(HyperglassLevel3):
description="Text displayed for the BGP Community query type in the hyperglas UI.", description="Text displayed for the BGP Community query type in the hyperglas UI.",
) )
pattern: BgpCommunityPattern = BgpCommunityPattern() pattern: BgpCommunityPattern = BgpCommunityPattern()
mode: constr(regex=r"(input|select)") = "input"
communities: List[Community] = []
class BgpRoute(HyperglassLevel3): class BgpRoute(HyperglassModel):
"""Validation model for bgp_route configuration.""" """Validation model for bgp_route configuration."""
enable: StrictBool = Field( enable: StrictBool = Field(
@@ -111,7 +106,7 @@ class BgpRoute(HyperglassLevel3):
) )
class BgpAsPath(HyperglassLevel3): class BgpAsPath(HyperglassModel):
"""Validation model for bgp_aspath configuration.""" """Validation model for bgp_aspath configuration."""
enable: StrictBool = Field( enable: StrictBool = Field(
@@ -127,7 +122,7 @@ class BgpAsPath(HyperglassLevel3):
pattern: BgpAsPathPattern = BgpAsPathPattern() pattern: BgpAsPathPattern = BgpAsPathPattern()
class Ping(HyperglassLevel3): class Ping(HyperglassModel):
"""Validation model for ping configuration.""" """Validation model for ping configuration."""
enable: StrictBool = Field( enable: StrictBool = Field(
@@ -140,7 +135,7 @@ class Ping(HyperglassLevel3):
) )
class Traceroute(HyperglassLevel3): class Traceroute(HyperglassModel):
"""Validation model for traceroute configuration.""" """Validation model for traceroute configuration."""
enable: StrictBool = Field( enable: StrictBool = Field(
@@ -168,8 +163,9 @@ class Queries(HyperglassModel):
query_obj = getattr(self, query) query_obj = getattr(self, query)
_map[query] = { _map[query] = {
"name": query, "name": query,
"display_name": query_obj.display_name, **query_obj.export_dict(
"enable": query_obj.enable, include={"display_name", "enable", "mode", "communities"}
),
} }
return _map return _map
@@ -225,4 +221,3 @@ class Queries(HyperglassModel):
"description": "Enable, disable, or configure the Traceroute query type.", "description": "Enable, disable, or configure the Traceroute query type.",
}, },
} }
schema_extra = {"level": 2}

View File

@@ -3,139 +3,151 @@ import { Text, useColorMode, useTheme } from "@chakra-ui/core";
import Select from "react-select"; import Select from "react-select";
import { opposingColor } from "~/util"; import { opposingColor } from "~/util";
export default ({ placeholder = "Select...", isFullWidth, size, children, ...props }) => { const ChakraSelect = React.forwardRef(
const theme = useTheme(); ({ placeholder = "Select...", isFullWidth, size, children, ...props }, ref) => {
const { colorMode } = useColorMode(); const theme = useTheme();
const sizeMap = { const { colorMode } = useColorMode();
lg: { height: theme.space[12] }, const sizeMap = {
md: { height: theme.space[10] }, lg: { height: theme.space[12] },
sm: { height: theme.space[8] } md: { height: theme.space[10] },
}; sm: { height: theme.space[8] },
const colorSetPrimaryBg = { dark: theme.colors.primary[300], light: theme.colors.primary[500] }; };
const colorSetPrimaryColor = opposingColor(theme, colorSetPrimaryBg[colorMode]); const colorSetPrimaryBg = {
const bg = { dark: theme.colors.whiteAlpha[100], light: theme.colors.white }; dark: theme.colors.primary[300],
const color = { dark: theme.colors.whiteAlpha[800], light: theme.colors.black }; light: theme.colors.primary[500],
const borderFocused = theme.colors.secondary[500]; };
const borderDisabled = theme.colors.whiteAlpha[100]; const colorSetPrimaryColor = opposingColor(theme, colorSetPrimaryBg[colorMode]);
const border = { dark: theme.colors.whiteAlpha[50], light: theme.colors.gray[100] }; const bg = { dark: theme.colors.whiteAlpha[100], light: theme.colors.white };
const borderRadius = theme.space[1]; const color = { dark: theme.colors.whiteAlpha[800], light: theme.colors.black };
const hoverColor = { dark: theme.colors.whiteAlpha[200], light: theme.colors.gray[300] }; const borderFocused = theme.colors.secondary[500];
const { height } = sizeMap[size]; const borderDisabled = theme.colors.whiteAlpha[100];
const optionBgActive = { dark: theme.colors.primary[400], light: theme.colors.primary[600] }; const border = { dark: theme.colors.whiteAlpha[50], light: theme.colors.gray[100] };
const optionBgColor = opposingColor(theme, optionBgActive[colorMode]); const borderRadius = theme.space[1];
const optionSelectedBg = { const hoverColor = { dark: theme.colors.whiteAlpha[200], light: theme.colors.gray[300] };
dark: theme.colors.whiteAlpha[400], const { height } = sizeMap[size];
light: theme.colors.blackAlpha[400] const optionBgActive = {
}; dark: theme.colors.primary[400],
const optionSelectedColor = opposingColor(theme, optionSelectedBg[colorMode]); light: theme.colors.primary[600],
const selectedDisabled = theme.colors.whiteAlpha[400]; };
const placeholderColor = { const optionBgColor = opposingColor(theme, optionBgActive[colorMode]);
dark: theme.colors.whiteAlpha[400], const optionSelectedBg = {
light: theme.colors.gray[400] dark: theme.colors.whiteAlpha[400],
}; light: theme.colors.blackAlpha[400],
const menuBg = { dark: theme.colors.black, light: theme.colors.white }; };
const menuColor = { dark: theme.colors.white, light: theme.colors.blackAlpha[800] }; const optionSelectedColor = opposingColor(theme, optionSelectedBg[colorMode]);
return ( const selectedDisabled = theme.colors.whiteAlpha[400];
<Select const placeholderColor = {
styles={{ dark: theme.colors.whiteAlpha[400],
container: base => ({ light: theme.colors.gray[400],
...base, };
minHeight: height, const menuBg = { dark: theme.colors.black, light: theme.colors.white };
borderRadius: borderRadius, const menuColor = { dark: theme.colors.white, light: theme.colors.blackAlpha[800] };
width: "100%" return (
}), <Select
control: (base, state) => ({ ref={ref}
...base, styles={{
minHeight: height, container: (base) => ({
backgroundColor: bg[colorMode], ...base,
color: color[colorMode], minHeight: height,
borderColor: state.isDisabled borderRadius: borderRadius,
? borderDisabled width: "100%",
: state.isFocused }),
? borderFocused control: (base, state) => ({
: border[colorMode], ...base,
borderRadius: borderRadius, minHeight: height,
"&:hover": { backgroundColor: bg[colorMode],
borderColor: hoverColor[colorMode] color: color[colorMode],
} borderColor: state.isDisabled
}), ? borderDisabled
menu: base => ({ : state.isFocused
...base, ? borderFocused
backgroundColor: menuBg[colorMode], : border[colorMode],
borderRadius: borderRadius borderRadius: borderRadius,
}), "&:hover": {
option: (base, state) => ({ borderColor: hoverColor[colorMode],
...base, },
backgroundColor: state.isDisabled }),
? selectedDisabled menu: (base) => ({
: state.isSelected ...base,
? optionSelectedBg[colorMode] backgroundColor: menuBg[colorMode],
: state.isFocused borderRadius: borderRadius,
? colorSetPrimaryBg[colorMode] }),
: "transparent", option: (base, state) => ({
color: state.isDisabled ...base,
? selectedDisabled backgroundColor: state.isDisabled
: state.isFocused ? selectedDisabled
? colorSetPrimaryColor : state.isSelected
: state.isSelected ? optionSelectedBg[colorMode]
? optionSelectedColor : state.isFocused
: menuColor[colorMode], ? colorSetPrimaryBg[colorMode]
fontSize: theme.fontSizes[size], : "transparent",
"&:active": { color: state.isDisabled
backgroundColor: optionBgActive[colorMode], ? selectedDisabled
color: optionBgColor : state.isFocused
} ? colorSetPrimaryColor
}), : state.isSelected
indicatorSeparator: base => ({ ? optionSelectedColor
...base, : menuColor[colorMode],
backgroundColor: placeholderColor[colorMode] fontSize: theme.fontSizes[size],
}), "&:active": {
dropdownIndicator: base => ({ backgroundColor: optionBgActive[colorMode],
...base, color: optionBgColor,
color: placeholderColor[colorMode], },
"&:hover": { }),
color: color[colorMode] indicatorSeparator: (base) => ({
} ...base,
}), backgroundColor: placeholderColor[colorMode],
valueContainer: base => ({ }),
...base, dropdownIndicator: (base) => ({
paddingLeft: theme.space[4], ...base,
paddingRight: theme.space[4] color: placeholderColor[colorMode],
}), "&:hover": {
multiValue: base => ({ color: color[colorMode],
...base, },
backgroundColor: colorSetPrimaryBg[colorMode] }),
}), valueContainer: (base) => ({
multiValueLabel: base => ({ ...base,
...base, paddingLeft: theme.space[4],
color: colorSetPrimaryColor paddingRight: theme.space[4],
}), }),
multiValueRemove: base => ({ multiValue: (base) => ({
...base, ...base,
color: colorSetPrimaryColor, backgroundColor: colorSetPrimaryBg[colorMode],
"&:hover": { }),
multiValueLabel: (base) => ({
...base,
color: colorSetPrimaryColor, color: colorSetPrimaryColor,
backgroundColor: "inherit" }),
} multiValueRemove: (base) => ({
}), ...base,
singleValue: base => ({ color: colorSetPrimaryColor,
...base, "&:hover": {
color: color[colorMode], color: colorSetPrimaryColor,
fontSize: theme.fontSizes[size] backgroundColor: "inherit",
}) },
}} }),
placeholder={ singleValue: (base) => ({
<Text ...base,
color={placeholderColor[colorMode]} color: color[colorMode],
fontSize={size} fontSize: theme.fontSizes[size],
fontFamily={theme.fonts.body} }),
> }}
{placeholder} placeholder={
</Text> <Text
} color={placeholderColor[colorMode]}
{...props} fontSize={size}
> fontFamily={theme.fonts.body}
{children} >
</Select> {placeholder}
); </Text>
}; }
{...props}
>
{children}
</Select>
);
}
);
ChakraSelect.displayName = "ChakraSelect";
export default ChakraSelect;

View File

@@ -0,0 +1,41 @@
import * as React from "react";
import { useEffect } from "react";
import { Text } from "@chakra-ui/core";
import { components } from "react-select";
import ChakraSelect from "~/components/ChakraSelect";
const CommunitySelect = ({ name, communities, onChange, register, unregister }) => {
const communitySelections = communities.map((c) => {
return { value: c.community, label: c.display_name, description: c.description };
});
const Option = ({ label, data, ...props }) => {
return (
<components.Option {...props}>
<Text>{label}</Text>
<Text fontSize="xs" as="span">
{data.description}
</Text>
</components.Option>
);
};
useEffect(() => {
register({ name });
return () => unregister(name);
}, [name, register, unregister]);
return (
<ChakraSelect
innerRef={register}
size="lg"
name={name}
onChange={(e) => {
onChange({ field: name, value: e.value || "" });
}}
options={communitySelections}
components={{ Option }}
/>
);
};
CommunitySelect.displayName = "CommunitySelect";
export default CommunitySelect;

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from "react"; import * as React from "react";
import { useState, useEffect } from "react";
import { Box, Flex } from "@chakra-ui/core"; import { Box, Flex } from "@chakra-ui/core";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import lodash from "lodash"; import lodash from "lodash";
@@ -9,6 +10,7 @@ import HelpModal from "~/components/HelpModal";
import QueryLocation from "~/components/QueryLocation"; import QueryLocation from "~/components/QueryLocation";
import QueryType from "~/components/QueryType"; import QueryType from "~/components/QueryType";
import QueryTarget from "~/components/QueryTarget"; import QueryTarget from "~/components/QueryTarget";
import CommunitySelect from "~/components/CommunitySelect";
import QueryVrf from "~/components/QueryVrf"; import QueryVrf from "~/components/QueryVrf";
import ResolvedTarget from "~/components/ResolvedTarget"; import ResolvedTarget from "~/components/ResolvedTarget";
import SubmitButton from "~/components/SubmitButton"; import SubmitButton from "~/components/SubmitButton";
@@ -46,9 +48,9 @@ const FormRow = ({ children, ...props }) => (
const HyperglassForm = React.forwardRef( const HyperglassForm = React.forwardRef(
({ isSubmitting, setSubmitting, setFormData, greetingAck, setGreetingAck, ...props }, ref) => { ({ isSubmitting, setSubmitting, setFormData, greetingAck, setGreetingAck, ...props }, ref) => {
const config = useConfig(); const config = useConfig();
const { handleSubmit, register, setValue, errors } = useForm({ const { handleSubmit, register, unregister, setValue, errors } = useForm({
validationSchema: formSchema(config), validationSchema: formSchema(config),
defaultValues: { query_vrf: "default" }, defaultValues: { query_vrf: "default", query_target: "" },
}); });
const [queryLocation, setQueryLocation] = useState([]); const [queryLocation, setQueryLocation] = useState([]);
@@ -136,10 +138,10 @@ const HyperglassForm = React.forwardRef(
useEffect(() => { useEffect(() => {
register({ name: "query_location" }); register({ name: "query_location" });
register({ name: "query_target" });
register({ name: "query_type" }); register({ name: "query_type" });
register({ name: "query_vrf" }); register({ name: "query_vrf" });
}); }, [register]);
Object.keys(errors).length >= 1 && console.error(errors);
return ( return (
<Box <Box
maxW={["100%", "100%", "75%", "75%"]} maxW={["100%", "100%", "75%", "75%"]}
@@ -202,19 +204,31 @@ const HyperglassForm = React.forwardRef(
) )
} }
> >
<QueryTarget {queryType === "bgp_community" &&
name="query_target" config.queries.bgp_community.mode === "select" ? (
placeholder={config.web.text.query_target} <CommunitySelect
register={register} name="query_target"
resolveTarget={["ping", "traceroute", "bgp_route"].includes( register={register}
queryType unregister={unregister}
)} onChange={handleChange}
value={queryTarget} communities={config.queries.bgp_community.communities}
setFqdn={setFqdnTarget} />
setTarget={handleChange} ) : (
displayValue={displayTarget} <QueryTarget
setDisplayValue={setDisplayTarget} name="query_target"
/> placeholder={config.web.text.query_target}
register={register}
unregister={unregister}
resolveTarget={["ping", "traceroute", "bgp_route"].includes(
queryType
)}
value={queryTarget}
setFqdn={setFqdnTarget}
setTarget={handleChange}
displayValue={displayTarget}
setDisplayValue={setDisplayTarget}
/>
)}
</FormField> </FormField>
</FormRow> </FormRow>
<FormRow mt={0} justifyContent="flex-end"> <FormRow mt={0} justifyContent="flex-end">

View File

@@ -1,15 +1,15 @@
import React from "react"; import React from "react";
import ChakraSelect from "~/components/ChakraSelect"; import ChakraSelect from "~/components/ChakraSelect";
const buildLocations = networks => { const buildLocations = (networks) => {
const locations = []; const locations = [];
networks.map(net => { networks.map((net) => {
const netLocations = []; const netLocations = [];
net.locations.map(loc => { net.locations.map((loc) => {
netLocations.push({ netLocations.push({
label: loc.display_name, label: loc.display_name,
value: loc.name, value: loc.name,
group: net.display_name group: net.display_name,
}); });
}); });
locations.push({ label: net.display_name, options: netLocations }); locations.push({ label: net.display_name, options: netLocations });
@@ -19,10 +19,10 @@ const buildLocations = networks => {
export default ({ locations, onChange }) => { export default ({ locations, onChange }) => {
const options = buildLocations(locations); const options = buildLocations(locations);
const handleChange = e => { const handleChange = (e) => {
const selected = []; const selected = [];
e && e &&
e.map(sel => { e.map((sel) => {
selected.push(sel.value); selected.push(sel.value);
}); });
onChange({ field: "query_location", value: selected }); onChange({ field: "query_location", value: selected });
@@ -34,6 +34,7 @@ export default ({ locations, onChange }) => {
onChange={handleChange} onChange={handleChange}
options={options} options={options}
isMulti isMulti
closeMenuOnSelect={false}
/> />
); );
}; };

View File

@@ -1,10 +1,10 @@
import React, { useState } from "react"; import React, { useEffect } from "react";
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import { Input, useColorMode } from "@chakra-ui/core"; import { Input, useColorMode } from "@chakra-ui/core";
const StyledInput = styled(Input)` const StyledInput = styled(Input)`
&::placeholder { &::placeholder {
color: ${props => props.placeholderColor}; color: ${(props) => props.placeholderColor};
} }
`; `;
@@ -18,13 +18,14 @@ const placeholderColor = { dark: "whiteAlpha.400", light: "gray.400" };
const QueryTarget = ({ const QueryTarget = ({
placeholder, placeholder,
register, register,
unregister,
setFqdn, setFqdn,
name, name,
value, value,
setTarget, setTarget,
resolveTarget, resolveTarget,
displayValue, displayValue,
setDisplayValue setDisplayValue,
}) => { }) => {
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
@@ -35,15 +36,19 @@ const QueryTarget = ({
setFqdn(false); setFqdn(false);
} }
}; };
const handleChange = e => { const handleChange = (e) => {
setDisplayValue(e.target.value); setDisplayValue(e.target.value);
setTarget({ field: name, value: e.target.value }); setTarget({ field: name, value: e.target.value });
}; };
const handleKeyDown = e => { const handleKeyDown = (e) => {
if ([9, 13].includes(e.keyCode)) { if ([9, 13].includes(e.keyCode)) {
handleBlur(); handleBlur();
} }
}; };
useEffect(() => {
register({ name });
return () => unregister(name);
}, [register, unregister, name]);
return ( return (
<> <>
<input hidden readOnly name={name} ref={register} value={value} /> <input hidden readOnly name={name} ref={register} value={value} />

View File

@@ -13,7 +13,7 @@ const labelBgSuccess = { dark: "success", light: "success" };
async function containingPrefix(ipAddress) { async function containingPrefix(ipAddress) {
try { try {
const prefixData = await axios.get("https://stat.ripe.net/data/network-info/data.json", { const prefixData = await axios.get("https://stat.ripe.net/data/network-info/data.json", {
params: { resource: ipAddress } params: { resource: ipAddress },
}); });
return prefixData.data?.data?.prefix; return prefixData.data?.data?.prefix;
} catch (err) { } catch (err) {
@@ -36,32 +36,32 @@ const ResolvedTarget = React.forwardRef(
params: { name: fqdnTarget, type: "A" }, params: { name: fqdnTarget, type: "A" },
headers: { accept: "application/dns-json" }, headers: { accept: "application/dns-json" },
crossdomain: true, crossdomain: true,
timeout: 1000 timeout: 1000,
}, },
6: { 6: {
url: dnsUrl, url: dnsUrl,
params: { name: fqdnTarget, type: "AAAA" }, params: { name: fqdnTarget, type: "AAAA" },
headers: { accept: "application/dns-json" }, headers: { accept: "application/dns-json" },
crossdomain: true, crossdomain: true,
timeout: 1000 timeout: 1000,
} },
}; };
const [{ data: data4, loading: loading4, error: error4 }] = useAxios(params[4]); const [{ data: data4, loading: loading4, error: error4 }] = useAxios(params[4]);
const [{ data: data6, loading: loading6, error: error6 }] = useAxios(params[6]); const [{ data: data6, loading: loading6, error: error6 }] = useAxios(params[6]);
const handleOverride = overridden => { const handleOverride = (overridden) => {
setTarget({ field: "query_target", value: overridden }); setTarget({ field: "query_target", value: overridden });
}; };
const isSelected = value => { const isSelected = (value) => {
return labelBgStatus[value === queryTarget]; return labelBgStatus[value === queryTarget];
}; };
const findAnswer = data => { const findAnswer = (data) => {
return data?.Answer?.filter( return data?.Answer?.filter(
answerData => answerData.type === data?.Question[0]?.type (answerData) => answerData.type === data?.Question[0]?.type
)[0]?.data; )[0]?.data;
}; };
@@ -74,7 +74,6 @@ const ResolvedTarget = React.forwardRef(
handleOverride(findAnswer(data4)); handleOverride(findAnswer(data4));
} }
}, [data4, data6]); }, [data4, data6]);
return ( return (
<Stack <Stack
ref={ref} ref={ref}