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

consolidate name & display name fields, closes #115

This commit is contained in:
checktheroads
2021-02-10 00:43:40 -07:00
parent 470be49370
commit 72269f38f5
15 changed files with 101 additions and 110 deletions

View File

@@ -101,6 +101,7 @@ async def query(query_data: Query, request: Request, background_tasks: Backgroun
json_output = True
cached = False
runtime = 65535
if cache_response:
log.debug("Query {} exists in cache", cache_key)
@@ -195,7 +196,7 @@ async def import_certificate(encoded_request: EncodedRequest):
# Write certificate to file
import_public_key(
app_path=APP_PATH,
device_name=matched_device.name,
device_name=matched_device._id,
keystring=decoded_request,
)
except RuntimeError as err:

View File

@@ -189,52 +189,6 @@ except KeyError:
pass
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.objects:
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 _build_frontend_devices():
"""Build filtered JSON structure of devices for frontend.
@@ -310,8 +264,8 @@ def _build_networks():
if device.network.display_name == _network:
network_def["locations"].append(
{
"_id": device._id,
"name": device.name,
"display_name": device.display_name,
"network": device.network.display_name,
"vrfs": [
{
@@ -417,7 +371,6 @@ content_credit = CREDIT.format(version=__version__)
vrfs = _build_vrfs()
networks = _build_networks()
frontend_networks = _build_frontend_networks()
frontend_devices = _build_frontend_devices()
_include_fields = {
"cache": {"show_text", "timeout"},
@@ -443,7 +396,6 @@ _frontend_params.update(
{
"hyperglass_version": __version__,
"queries": {**params.queries.map, "list": params.queries.list},
"devices": frontend_devices,
"networks": networks,
"vrfs": vrfs,
"parsed_data_fields": PARSED_RESPONSE_FIELDS,

View File

@@ -42,7 +42,7 @@ class AgentConnection(Connection):
raise RestError(
"SSL Certificate for device {d} has not been imported",
level="danger",
d=self.device.display_name,
d=self.device.name,
)
http_protocol = "https"
client_params.update({"verify": str(self.device.ssl.cert)})
@@ -90,8 +90,7 @@ class AgentConnection(Connection):
elif raw_response.status_code == 204:
raise ResponseEmpty(
params.messages.no_output,
device_name=self.device.display_name,
params.messages.no_output, device_name=self.device.name,
)
else:
@@ -102,14 +101,14 @@ class AgentConnection(Connection):
log.error("Error connecting to device {}: {}", self.device.name, msg)
raise RestError(
params.messages.connection_error,
device_name=self.device.display_name,
device_name=self.device.name,
error=msg,
)
except OSError as ose:
log.critical(str(ose))
raise RestError(
params.messages.connection_error,
device_name=self.device.display_name,
device_name=self.device.name,
error="System error",
)
except CertificateError as cert_error:
@@ -117,7 +116,7 @@ class AgentConnection(Connection):
msg = parse_exception(cert_error)
raise RestError(
params.messages.connection_error,
device_name=self.device.display_name,
device_name=self.device.name,
error=f"{msg}: {cert_error}",
)
@@ -125,7 +124,7 @@ class AgentConnection(Connection):
log.error("Response code is {}", raw_response.status_code)
raise RestError(
params.messages.connection_error,
device_name=self.device.display_name,
device_name=self.device.name,
error=params.messages.general,
)
@@ -133,7 +132,7 @@ class AgentConnection(Connection):
log.error("No response from device {}", self.device.name)
raise RestError(
params.messages.connection_error,
device_name=self.device.display_name,
device_name=self.device.name,
error=params.messages.no_response,
)

View File

@@ -54,7 +54,7 @@ class SSHConnection(Connection):
)
raise ScrapeError(
params.messages.connection_error,
device_name=self.device.display_name,
device_name=self.device.name,
proxy=proxy.name,
error=str(scrape_proxy_error),
)

View File

@@ -51,7 +51,7 @@ async def execute(query: Query) -> Union[str, Sequence[Dict]]:
timeout_args = {
"unformatted_msg": params.messages.connection_error,
"device_name": query.device.display_name,
"device_name": query.device.name,
"error": params.messages.request_timeout,
}
@@ -73,9 +73,7 @@ async def execute(query: Query) -> Union[str, Sequence[Dict]]:
output = await driver.parsed_response(response)
if output == "" or output == "\n":
raise ResponseEmpty(
params.messages.no_output, device_name=query.device.display_name
)
raise ResponseEmpty(params.messages.no_output, device_name=query.device.name)
log.debug("Output for query: {}:\n{}", query.json(), repr(output))
signal.alarm(0)

View File

@@ -136,7 +136,7 @@ class Query(BaseModel):
"""Create dictionary representation of instance."""
if pretty:
items = {
"query_location": self.device.display_name,
"query_location": self.device.name,
"query_type": self.query.display_name,
"query_vrf": self.query_vrf.display_name,
"query_target": str(self.query_target),
@@ -189,7 +189,7 @@ class Query(BaseModel):
Returns:
{str} -- Valid query_location
"""
if value not in devices.hostnames:
if value not in devices._ids:
raise InputInvalid(
params.messages.invalid_field,
level="warning",
@@ -222,7 +222,7 @@ class Query(BaseModel):
raise InputInvalid(
params.messages.vrf_not_associated,
vrf_name=vrf_object.display_name,
device_name=device.display_name,
device_name=device.name,
)
return device_vrf

View File

@@ -180,7 +180,6 @@ class RoutersResponse(BaseModel):
name: StrictStr
network: Network
display_name: StrictStr
vrfs: List[Vrf]
class Config:
@@ -188,15 +187,7 @@ class RoutersResponse(BaseModel):
title = "Device"
description = "Per-device attributes"
schema_extra = {
"examples": [
{
"name": "router01-nyc01",
"location": "nyc01",
"display_name": "New York City, NY",
}
]
}
schema_extra = {"examples": [{"name": "router01-nyc01", "location": "nyc01"}]}
class CommunityResponse(BaseModel):

View File

@@ -3,12 +3,19 @@
# Standard Library
import os
import re
from typing import Any, Dict, List, Union, Optional
from typing import Any, Dict, List, Tuple, Union, Optional
from pathlib import Path
from ipaddress import IPv4Address, IPv6Address
# Third Party
from pydantic import StrictInt, StrictStr, StrictBool, validator, root_validator
from pydantic import (
StrictInt,
StrictStr,
StrictBool,
PrivateAttr,
validator,
root_validator,
)
# Project
from hyperglass.log import log
@@ -41,15 +48,43 @@ _default_vrf = {
}
def find_device_id(values: Dict) -> Tuple[str, Dict]:
"""Generate device id & handle legacy display_name field."""
def generate_id(name: str) -> str:
scrubbed = re.sub(r"[^A-Za-z0-9\_\-\s]", "", name)
return "_".join(scrubbed.split()).lower()
name = values.pop("name", None)
if name is None:
raise ValueError("name is required.")
legacy_display_name = values.pop("display_name", None)
if legacy_display_name is not None:
log.warning(
"The 'display_name' field is deprecated. Use the 'name' field instead."
)
device_id = generate_id(legacy_display_name)
display_name = legacy_display_name
else:
device_id = generate_id(name)
display_name = name
return device_id, {"name": display_name, "display_name": None, **values}
class Device(HyperglassModel):
"""Validation model for per-router config in devices.yaml."""
_id: StrictStr = PrivateAttr()
name: StrictStr
address: Union[IPv4Address, IPv6Address, StrictStr]
network: Network
credential: Credential
proxy: Optional[Proxy]
display_name: StrictStr
display_name: Optional[StrictStr]
port: StrictInt = 22
ssl: Optional[Ssl]
nos: StrictStr
@@ -59,6 +94,12 @@ class Device(HyperglassModel):
vrf_names: List[StrictStr] = []
structured_output: Optional[StrictBool]
def __init__(self, **kwargs) -> None:
"""Set the device ID."""
_id, values = find_device_id(kwargs)
super().__init__(**values)
self._id = _id
def __hash__(self) -> int:
"""Make device object hashable so the object can be deduplicated with set()."""
return hash((self.name,))
@@ -245,6 +286,7 @@ class Device(HyperglassModel):
class Devices(HyperglassModelExtra):
"""Validation model for device configurations."""
_ids: List[StrictStr] = []
hostnames: List[StrictStr] = []
vrfs: List[StrictStr] = []
display_vrfs: List[StrictStr] = []
@@ -272,6 +314,7 @@ class Devices(HyperglassModelExtra):
all_nos = set()
objects = set()
hostnames = set()
_ids = set()
init_kwargs = {}
@@ -284,6 +327,7 @@ class Devices(HyperglassModelExtra):
# list with `devices.hostnames`, same for all router
# classes, for when iteration over all routers is required.
hostnames.add(device.name)
_ids.add(device._id)
objects.add(device)
all_nos.add(device.commands)
@@ -320,19 +364,20 @@ class Devices(HyperglassModelExtra):
# Convert the de-duplicated sets to a standard list, add lists
# as class attributes. Sort router list by router name attribute
init_kwargs["_ids"] = list(_ids)
init_kwargs["hostnames"] = list(hostnames)
init_kwargs["all_nos"] = list(all_nos)
init_kwargs["vrfs"] = list(vrfs)
init_kwargs["display_vrfs"] = list(vrfs)
init_kwargs["vrf_objects"] = list(vrf_objects)
init_kwargs["objects"] = sorted(objects, key=lambda x: x.display_name)
init_kwargs["objects"] = sorted(objects, key=lambda x: x.name)
super().__init__(**init_kwargs)
def __getitem__(self, accessor: str) -> Device:
"""Get a device by its name."""
for device in self.objects:
if device.name == accessor:
if device._id == accessor:
return device
raise AttributeError(f"No device named '{accessor}'")

View File

@@ -11,8 +11,8 @@ function buildOptions(networks: TNetwork[]) {
return networks.map(net => {
const label = net.display_name;
const options = net.locations.map(loc => ({
label: loc.display_name,
value: loc.name,
label: loc.name,
value: loc._id,
group: net.display_name,
}));
return { label, options };

View File

@@ -45,7 +45,7 @@ export const Results: React.FC = () => {
<Result
index={i}
device={device}
key={device.name}
key={device._id}
queryLocation={loc.value}
queryVrf={queryVrf.value}
queryType={queryType.value}

View File

@@ -66,7 +66,7 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
]);
if (typeof data !== 'undefined') {
responses.merge({ [device.name]: data });
responses.merge({ [device._id]: data });
}
const cacheLabel = useStrf(web.text.cache_icon, { time: data?.timestamp }, [data?.timestamp]);
@@ -148,8 +148,8 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
return (
<AnimatedAccordionItem
id={device.name}
ref={ref}
id={device._id}
isDisabled={isLoading}
exit={{ opacity: 0, y: 300 }}
animate={{ opacity: 1, y: 0 }}
@@ -169,12 +169,12 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
errorMsg={errorMsg}
errorLevel={errorLevel}
runtime={data?.runtime ?? 0}
title={device.display_name}
title={device.name}
/>
</AccordionButton>
<HStack py={2} spacing={1}>
{isStructuredOutput(data) && data.level === 'success' && tableComponent && (
<Path device={device.name} />
<Path device={device._id} />
)}
<CopyButton copyValue={copyValue} isDisabled={isLoading} />
<RequeryButton requery={refetch} isDisabled={isLoading} />

View File

@@ -13,7 +13,7 @@ export function useDevice(): TUseDevice {
const devices = useMemo(() => networks.map(n => n.locations).flat(), []);
function getDevice(id: string): TDevice {
return devices.filter(dev => dev.name === id)[0];
return devices.filter(dev => dev._id === id)[0];
}
return useCallback(getDevice, []);

View File

@@ -114,9 +114,9 @@ export interface TDeviceVrf extends TDeviceVrfBase {
}
interface TDeviceBase {
_id: string;
name: string;
network: string;
display_name: string;
}
export interface TDevice extends TDeviceBase {

43
poetry.lock generated
View File

@@ -927,7 +927,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pydantic"
version = "1.6.1"
version = "1.7.3"
description = "Data validation and settings management using python 3.6 type hinting"
category = "main"
optional = false
@@ -1427,7 +1427,7 @@ testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake
[metadata]
lock-version = "1.1"
python-versions = ">=3.6.1,<4.0"
content-hash = "020be9857d03222d73418b6c7e4b966cd42d5a020f4b3e3a3dddeef6d8f29535"
content-hash = "cb0743e8f0e89938e116d9e1c2e64b260b52759ffaeacc521ce5f79d492d0b99"
[metadata.files]
aiocontextvars = [
@@ -1883,23 +1883,28 @@ pycparser = [
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
]
pydantic = [
{file = "pydantic-1.6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:418b84654b60e44c0cdd5384294b0e4bc1ebf42d6e873819424f3b78b8690614"},
{file = "pydantic-1.6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4900b8820b687c9a3ed753684337979574df20e6ebe4227381d04b3c3c628f99"},
{file = "pydantic-1.6.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b49c86aecde15cde33835d5d6360e55f5e0067bb7143a8303bf03b872935c75b"},
{file = "pydantic-1.6.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:2de562a456c4ecdc80cf1a8c3e70c666625f7d02d89a6174ecf63754c734592e"},
{file = "pydantic-1.6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f769141ab0abfadf3305d4fcf36660e5cf568a666dd3efab7c3d4782f70946b1"},
{file = "pydantic-1.6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2dc946b07cf24bee4737ced0ae77e2ea6bc97489ba5a035b603bd1b40ad81f7e"},
{file = "pydantic-1.6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:36dbf6f1be212ab37b5fda07667461a9219c956181aa5570a00edfb0acdfe4a1"},
{file = "pydantic-1.6.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:1783c1d927f9e1366e0e0609ae324039b2479a1a282a98ed6a6836c9ed02002c"},
{file = "pydantic-1.6.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:cf3933c98cb5e808b62fae509f74f209730b180b1e3c3954ee3f7949e083a7df"},
{file = "pydantic-1.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f8af9b840a9074e08c0e6dc93101de84ba95df89b267bf7151d74c553d66833b"},
{file = "pydantic-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:40d765fa2d31d5be8e29c1794657ad46f5ee583a565c83cea56630d3ae5878b9"},
{file = "pydantic-1.6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3fa799f3cfff3e5f536cbd389368fc96a44bb30308f258c94ee76b73bd60531d"},
{file = "pydantic-1.6.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:6c3f162ba175678218629f446a947e3356415b6b09122dcb364e58c442c645a7"},
{file = "pydantic-1.6.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:eb75dc1809875d5738df14b6566ccf9fd9c0bcde4f36b72870f318f16b9f5c20"},
{file = "pydantic-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:530d7222a2786a97bc59ee0e0ebbe23728f82974b1f1ad9a11cd966143410633"},
{file = "pydantic-1.6.1-py36.py37.py38-none-any.whl", hash = "sha256:b5b3489cb303d0f41ad4a7390cf606a5f2c7a94dcba20c051cd1c653694cb14d"},
{file = "pydantic-1.6.1.tar.gz", hash = "sha256:54122a8ed6b75fe1dd80797f8251ad2063ea348a03b77218d73ea9fe19bd4e73"},
{file = "pydantic-1.7.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c59ea046aea25be14dc22d69c97bee629e6d48d2b2ecb724d7fe8806bf5f61cd"},
{file = "pydantic-1.7.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a4143c8d0c456a093387b96e0f5ee941a950992904d88bc816b4f0e72c9a0009"},
{file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:d8df4b9090b595511906fa48deda47af04e7d092318bfb291f4d45dfb6bb2127"},
{file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:514b473d264671a5c672dfb28bdfe1bf1afd390f6b206aa2ec9fed7fc592c48e"},
{file = "pydantic-1.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:dba5c1f0a3aeea5083e75db9660935da90216f8a81b6d68e67f54e135ed5eb23"},
{file = "pydantic-1.7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59e45f3b694b05a69032a0d603c32d453a23f0de80844fb14d55ab0c6c78ff2f"},
{file = "pydantic-1.7.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:5b24e8a572e4b4c18f614004dda8c9f2c07328cb5b6e314d6e1bbd536cb1a6c1"},
{file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:b2b054d095b6431cdda2f852a6d2f0fdec77686b305c57961b4c5dd6d863bf3c"},
{file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:025bf13ce27990acc059d0c5be46f416fc9b293f45363b3d19855165fee1874f"},
{file = "pydantic-1.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6e3874aa7e8babd37b40c4504e3a94cc2023696ced5a0500949f3347664ff8e2"},
{file = "pydantic-1.7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e682f6442ebe4e50cb5e1cfde7dda6766fb586631c3e5569f6aa1951fd1a76ef"},
{file = "pydantic-1.7.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:185e18134bec5ef43351149fe34fda4758e53d05bb8ea4d5928f0720997b79ef"},
{file = "pydantic-1.7.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:f5b06f5099e163295b8ff5b1b71132ecf5866cc6e7f586d78d7d3fd6e8084608"},
{file = "pydantic-1.7.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:24ca47365be2a5a3cc3f4a26dcc755bcdc9f0036f55dcedbd55663662ba145ec"},
{file = "pydantic-1.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:d1fe3f0df8ac0f3a9792666c69a7cd70530f329036426d06b4f899c025aca74e"},
{file = "pydantic-1.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f6864844b039805add62ebe8a8c676286340ba0c6d043ae5dea24114b82a319e"},
{file = "pydantic-1.7.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ecb54491f98544c12c66ff3d15e701612fc388161fd455242447083350904730"},
{file = "pydantic-1.7.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:ffd180ebd5dd2a9ac0da4e8b995c9c99e7c74c31f985ba090ee01d681b1c4b95"},
{file = "pydantic-1.7.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8d72e814c7821125b16f1553124d12faba88e85405b0864328899aceaad7282b"},
{file = "pydantic-1.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:475f2fa134cf272d6631072554f845d0630907fce053926ff634cc6bc45bf1af"},
{file = "pydantic-1.7.3-py3-none-any.whl", hash = "sha256:38be427ea01a78206bcaf9a56f835784afcba9e5b88fbdce33bbbfbcd7841229"},
{file = "pydantic-1.7.3.tar.gz", hash = "sha256:213125b7e9e64713d16d988d10997dabc6a1f73f3991e1ff8e35ebb1409c7dc9"},
]
pydocstyle = [
{file = "pydocstyle-5.1.1-py3-none-any.whl", hash = "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678"},

View File

@@ -47,7 +47,7 @@ netmiko = "^3.3.2"
paramiko = "^2.7.1"
psutil = "^5.7.2"
py-cpuinfo = "^7.0.0"
pydantic = "^1.4"
pydantic = "^1.7.3"
python = ">=3.6.1,<4.0"
redis = "^3.5.3"
scrapli = {extras = ["asyncssh"], version = "^2020.9.26"}