diff --git a/.flake8 b/.flake8
index b65a856..cacf3d3 100644
--- a/.flake8
+++ b/.flake8
@@ -8,14 +8,12 @@ filename=*.py
per-file-ignores=
# Disable redefinition warning for exception handlers
hyperglass/api.py:F811
- # Disable string length warnings so one can actually read the commands
- hyperglass/configuration/models/commands.py:E501,C0301
- hyperglass/configuration/models/docs.py:E501,C0301
- hyperglass/configuration/models/messages.py:E501,C0301
- hyperglass/api/models/response.py:E501,C0301
# Disable classmethod warning for validator decorators
- hyperglass/configuration/models/*.py:N805,E0213,R0903
hyperglass/models/*.py:N805,E0213,R0903
+ hyperglass/configuration/models/*.py:N805,E0213,R0903
+ # Disable string length warnings so one can actually read the commands
+ hyperglass/configuration/models/*.py:E501,C0301
+ hyperglass/api/models/response.py:E501,C0301
ignore=W503,C0330,R504,D202
select=B, BLK, C, D, E, F, I, II, N, P, PIE, S, R, W
disable-noqa=False
diff --git a/hyperglass/api/__init__.py b/hyperglass/api/__init__.py
index 728bb24..57d823c 100644
--- a/hyperglass/api/__init__.py
+++ b/hyperglass/api/__init__.py
@@ -51,7 +51,7 @@ app = FastAPI(
default_response_class=UJSONResponse,
docs_url=None,
redoc_url=None,
- openapi_url=params.docs.openapi_url,
+ openapi_url=params.docs.openapi_uri,
)
# Add Event Handlers
@@ -133,7 +133,10 @@ app.add_api_route(
tags=[params.docs.query.title],
response_class=UJSONResponse,
)
-app.add_api_route(path="/api/docs", endpoint=docs, include_in_schema=False)
+
+if params.docs.enable:
+ app.add_api_route(path=params.docs.uri, endpoint=docs, include_in_schema=False)
+
app.mount("/images", StaticFiles(directory=IMAGES_DIR), name="images")
app.mount("/", StaticFiles(directory=UI_DIR, html=True), name="ui")
diff --git a/hyperglass/configuration/models/cache.py b/hyperglass/configuration/models/cache.py
index 79317c2..e3e9219 100644
--- a/hyperglass/configuration/models/cache.py
+++ b/hyperglass/configuration/models/cache.py
@@ -1,6 +1,11 @@
"""Validation model for Redis cache config."""
+# Standard Library Imports
+from typing import Union
+
# Third Party Imports
+from pydantic import Field
+from pydantic import IPvAnyAddress
from pydantic import StrictBool
from pydantic import StrictInt
from pydantic import StrictStr
@@ -12,8 +17,26 @@ from hyperglass.configuration.models._utils import HyperglassModel
class Cache(HyperglassModel):
"""Validation model for params.cache."""
- host: StrictStr = "localhost"
- port: StrictInt = 6379
- database: StrictInt = 0
- timeout: StrictInt = 120
- show_text: StrictBool = True
+ host: Union[IPvAnyAddress, StrictStr] = Field(
+ "localhost", title="Host", description="Redis server IP address or hostname."
+ )
+ port: StrictInt = Field(6379, title="Port", description="Redis server TCP port.")
+ database: StrictInt = Field(
+ 0, title="Database ID", description="Redis server database ID."
+ )
+ timeout: StrictInt = Field(
+ 120,
+ title="Timeout",
+ description="Time in seconds query output will be kept in the Redis cache.",
+ )
+ show_text: StrictBool = Field(
+ True,
+ title="Show Text",
+ description="Show the [`cache`](/fixme) text in the hyperglass UI.",
+ )
+
+ class Config:
+ """Pydantic model configuration."""
+
+ title = "Cache"
+ description = "Redis server & cache timeout configuration."
diff --git a/hyperglass/configuration/models/docs.py b/hyperglass/configuration/models/docs.py
index 02b78f1..30cc5fd 100644
--- a/hyperglass/configuration/models/docs.py
+++ b/hyperglass/configuration/models/docs.py
@@ -1,5 +1,6 @@
"""Configuration for API docs feature."""
# Third Party Imports
+from pydantic import Field
from pydantic import StrictBool
from pydantic import StrictStr
from pydantic import constr
@@ -12,23 +13,49 @@ from hyperglass.configuration.models._utils import HyperglassModel
class EndpointConfig(HyperglassModel):
"""Validation model for per API endpoint documentation."""
- title: StrictStr
- description: StrictStr
- summary: StrictStr
+ title: StrictStr = Field(
+ ...,
+ title="Endpoint Title",
+ description="Displayed as the header text above the API endpoint section.",
+ )
+ description: StrictStr = Field(
+ ...,
+ title="Endpoint Description",
+ description="Displayed inside each API endpoint section.",
+ )
+ summary: StrictStr = Field(
+ ...,
+ title="Endpoint Summary",
+ description="Displayed beside the API endpoint URI.",
+ )
class Docs(HyperglassModel):
"""Validation model for params.docs."""
- enable: StrictBool = True
- mode: constr(regex=r"(swagger|redoc)") = "swagger"
- uri: AnyUri = "/docs"
- openapi_url: AnyUri = "/openapi.json"
- query: EndpointConfig = {
- "title": "Submit Query",
- "description": "Request a query response per-location.",
- "summary": "Query the Looking Glass",
- }
+ enable: StrictBool = Field(
+ True, title="Enable", description="Enable or disable API documentation."
+ )
+ mode: constr(regex=r"(swagger|redoc)") = Field(
+ "swagger",
+ title="Docs Mode",
+ description="OpenAPI UI library to use for the hyperglass API docs. Currently, the options are [Swagger UI](/fixme) and [Redoc](/fixme).",
+ )
+ uri: AnyUri = Field(
+ "/api/docs",
+ title="URI",
+ description="HTTP URI/path where API documentation can be accessed.",
+ )
+ openapi_uri: AnyUri = Field(
+ "/openapi.json",
+ title="OpenAPI URI",
+ description="Path to the automatically generated `openapi.json` file.",
+ )
+ query: EndpointConfig = EndpointConfig(
+ title="Submit Query",
+ description="Request a query response per-location.",
+ summary="Query the Looking Glass",
+ )
devices: EndpointConfig = EndpointConfig(
title="Devices",
description="List of all devices/locations with associated identifiers, display names, networks, & VRFs.",
@@ -39,3 +66,23 @@ class Docs(HyperglassModel):
description="List of supported query types.",
summary="Query Types",
)
+
+ class Config:
+ """Pydantic model configuration."""
+
+ title = "API Docs"
+ description = "API documentation configuration parameters"
+ fields = {
+ "query": {
+ "title": "Query API Endpoint",
+ "description": "`/api/query/` API documentation options.",
+ },
+ "devices": {
+ "title": "Devices API Endpoint",
+ "description": "`/api/devices` API documentation options.",
+ },
+ "queries": {
+ "title": "Queries API Endpoint",
+ "description": "`/api/devices` API documentation options.",
+ },
+ }
diff --git a/hyperglass/configuration/models/messages.py b/hyperglass/configuration/models/messages.py
index a93fefc..54f50b8 100644
--- a/hyperglass/configuration/models/messages.py
+++ b/hyperglass/configuration/models/messages.py
@@ -1,6 +1,7 @@
"""Validate error message configuration variables."""
# Third Party Imports
+from pydantic import Field
from pydantic import StrictStr
# Project Imports
@@ -10,25 +11,78 @@ from hyperglass.configuration.models._utils import HyperglassModel
class Messages(HyperglassModel):
"""Validation model for params.messages."""
- no_input: StrictStr = "{field} must be specified."
- acl_denied: StrictStr = "{target} is a member of {denied_network}, which is not allowed."
- acl_not_allowed: StrictStr = "{target} is not allowed."
- max_prefix: StrictStr = (
- "Prefix length must be shorter than /{max_length}. {target} is too specific."
+ no_input: StrictStr = Field(
+ "{field} must be specified.",
+ title="No Input",
+ description="Displayed when no a required field is not specified. `{field}` may be used to display the `display_name` of the field that was omitted.",
)
- requires_ipv6_cidr: StrictStr = (
- "{device_name} requires IPv6 BGP lookups to be in CIDR notation."
+ acl_denied: StrictStr = Field(
+ "{target} is a member of {denied_network}, which is not allowed.",
+ title="ACL - Denied",
+ description="Displayed when a query target is explicitly denied by a matched VRF's ACL entry. `{target}` and `{denied_network}` may be used to display the denied query target and the ACL entry that caused it to be denied.",
)
- feature_not_enabled: StrictStr = "{feature} is not enabled for {device_name}."
- invalid_input: StrictStr = "{target} is not a valid {query_type} target."
- invalid_field: StrictStr = "{input} is an invalid {field}."
- general: StrictStr = "Something went wrong."
- directed_cidr: StrictStr = "{query_type} queries can not be in CIDR format."
- request_timeout: StrictStr = "Request timed out."
- connection_error: StrictStr = "Error connecting to {device_name}: {error}"
- authentication_error: StrictStr = "Authentication error occurred."
- noresponse_error: StrictStr = "No response."
- vrf_not_associated: StrictStr = "VRF {vrf_name} is not associated with {device_name}."
- vrf_not_found: StrictStr = "VRF {vrf_name} is not defined."
- no_matching_vrfs: StrictStr = "No VRFs in Common"
- no_output: StrictStr = "No output."
+ acl_not_allowed: StrictStr = Field(
+ "{target} is not allowed.",
+ title="ACL - Not Allowed",
+ description="Displayed when a query target is implicitly denied by a matched VRF's ACL. `{target}` may be used to display the denied query target.",
+ )
+ feature_not_enabled: StrictStr = Field(
+ "{feature} is not enabled for {device_name}.",
+ title="Feature Not Enabled",
+ description="Displayed when a query type is submitted that is not supported or disabled. The hyperglass UI performs validation of supported query types prior to submitting any requests, so this is primarily relevant to the hyperglass API. `{feature}` and `{device_name}` may be used to display the disabled feature and the selected device/location.",
+ )
+ invalid_input: StrictStr = Field(
+ "{target} is not a valid {query_type} target.",
+ title="Invalid Input",
+ description="Displayed when a query target's value is invalid in relation to the corresponding query type. `{target}` and `{query_type}` maybe used to display the invalid target and corresponding query type.",
+ )
+ invalid_field: StrictStr = Field(
+ "{input} is an invalid {field}.",
+ title="Invalid Field",
+ description="Displayed when a query field contains an invalid or unsupported value. `{input}` and `{field}` may be used to display the invalid input value and corresponding field name.",
+ )
+ general: StrictStr = Field(
+ "Something went wrong.",
+ title="General Error",
+ description="Displayed when generalized errors occur. Seeing this error message may indicate a bug in hyperglass, as most other errors produced are highly contextual. If you see this in the wild, try enabling [debug mode](/fixme) and review the logs to pinpoint the source of the error.",
+ )
+ request_timeout: StrictStr = Field(
+ "Request timed out.",
+ title="Request Timeout",
+ description="Displayed when the [`request_timeout`](/fixme) time expires.",
+ )
+ connection_error: StrictStr = Field(
+ "Error connecting to {device_name}: {error}",
+ title="Displayed when hyperglass is unable to connect to a configured device. Usually, this indicates a configuration error. `{device_name}` and `{error}` may be used to display the device in question and the specific connection error.",
+ )
+ authentication_error: StrictStr = Field(
+ "Authentication error occurred.",
+ title="Authentication Error",
+ description="Displayed when hyperglass is unable to authenticate to a configured device. Usually, this indicates a configuration error.",
+ )
+ no_response: StrictStr = Field(
+ "No response.",
+ title="No Response",
+ description="Displayed when hyperglass can connect to a device, but no output able to be read. Seeing this error may indicate a bug in hyperglas or one of its dependencies. If you see this in the wild, try enabling [debug mode](/fixme) and review the logs to pinpoint the source of the error.",
+ )
+ vrf_not_associated: StrictStr = Field(
+ "VRF {vrf_name} is not associated with {device_name}.",
+ title="VRF Not Associated",
+ description="Displayed when a query request's VRF field value contains a VRF that is not configured or associated with the corresponding location/device. The hyperglass UI automatically filters out VRFs that are not configured on a selected device, so this error is most likely to appear when using the hyperglass API. `{vrf_name}` and `{device_name}` may be used to display the VRF in question and corresponding device.",
+ )
+ vrf_not_found: StrictStr = Field(
+ "VRF {vrf_name} is not defined.",
+ title="VRF Not Found",
+ description="Displayed when a query VRF is not configured on any devices. The hyperglass UI only shows configured VRFs, so this error is most likely to appear when using the hyperglass API. `{vrf_name}` may be used to display the VRF in question.",
+ )
+ no_output: StrictStr = Field(
+ "No output.",
+ title="No Output",
+ description="Displayed when hyperglass can connect to a device and execute a query, but the response is empty.",
+ )
+
+ class Config:
+ """Pydantic model configuration."""
+
+ title = "Messages"
+ description = "Customize almost all user-facing UI & API messages."
diff --git a/hyperglass/configuration/models/networks.py b/hyperglass/configuration/models/networks.py
index 54636e3..f3eb128 100644
--- a/hyperglass/configuration/models/networks.py
+++ b/hyperglass/configuration/models/networks.py
@@ -1,6 +1,7 @@
"""Validate network configuration variables."""
# Third Party Imports
+from pydantic import Field
from pydantic import StrictStr
# Project Imports
@@ -11,8 +12,16 @@ from hyperglass.configuration.models._utils import clean_name
class Network(HyperglassModel):
"""Validation Model for per-network/asn config in devices.yaml."""
- name: StrictStr
- display_name: StrictStr
+ name: StrictStr = Field(
+ ...,
+ title="Network Name",
+ description="Internal name of the device's primary network.",
+ )
+ display_name: StrictStr = Field(
+ ...,
+ title="Network Display Name",
+ description="Display name of the device's primary network.",
+ )
class Networks(HyperglassModel):
diff --git a/hyperglass/configuration/models/opengraph.py b/hyperglass/configuration/models/opengraph.py
index b1d6769..e9b8b9c 100644
--- a/hyperglass/configuration/models/opengraph.py
+++ b/hyperglass/configuration/models/opengraph.py
@@ -6,7 +6,7 @@ from typing import Optional
import PIL.Image as PilImage
from pydantic import FilePath
from pydantic import StrictInt
-from pydantic import validator
+from pydantic import root_validator
# Project Imports
from hyperglass.configuration.models._utils import HyperglassModel
@@ -19,8 +19,8 @@ class OpenGraph(HyperglassModel):
height: Optional[StrictInt]
image: Optional[FilePath]
- @validator("image")
- def validate_image(cls, value, values):
+ @root_validator
+ def validate_image(cls, values):
"""Set default opengraph image location.
Arguments:
@@ -29,14 +29,48 @@ class OpenGraph(HyperglassModel):
Returns:
{Path} -- Opengraph image file path object
"""
- if value is None:
- value = (
+ supported_extensions = (".jpg", ".jpeg", ".png")
+ if (
+ values["image"].suffix is not None
+ and values["image"] not in supported_extensions
+ ):
+ raise ValueError(
+ "OpenGraph image must be one of {e}".format(
+ e=", ".join(supported_extensions)
+ )
+ )
+ if values["image"] is None:
+ image = (
Path(__file__).parent.parent.parent
/ "static/images/hyperglass-opengraph.png"
)
- with PilImage.open(value) as img:
- width, height = img.size
- values["width"] = width
- values["height"] = height
+ values["image"] = "".join(str(image).split("static")[1::])
- return "".join(str(value).split("static")[1::])
+ with PilImage.open(image) as img:
+ width, height = img.size
+ if values["width"] is None:
+ values["width"] = width
+ if values["height"] is None:
+ values["height"] = height
+
+ return values
+
+ class Config:
+ """Pydantic model configuration."""
+
+ title = "OpenGraph"
+ description = "OpenGraph configuration parameters"
+ fields = {
+ "width": {
+ "title": "Width",
+ "description": "Width of OpenGraph image. If unset, the width will be automatically derived by reading the image file.",
+ },
+ "height": {
+ "title": "Height",
+ "description": "Height of OpenGraph image. If unset, the height will be automatically derived by reading the image file.",
+ },
+ "image": {
+ "title": "Image File",
+ "description": "Valid path to a JPG or PNG file to use as the OpenGraph image.",
+ },
+ }
diff --git a/hyperglass/configuration/models/params.py b/hyperglass/configuration/models/params.py
index 0535136..e6ddccc 100644
--- a/hyperglass/configuration/models/params.py
+++ b/hyperglass/configuration/models/params.py
@@ -9,6 +9,7 @@ from typing import Optional
from typing import Union
# Third Party Imports
+from pydantic import Field
from pydantic import FilePath
from pydantic import IPvAnyAddress
from pydantic import StrictBool
@@ -29,34 +30,73 @@ class Params(HyperglassModel):
"""Validation model for all configuration variables."""
# Top Level Params
- debug: StrictBool = False
- developer_mode: StrictBool = False
- primary_asn: Union[StrictInt, StrictStr] = "65001"
- org_name: StrictStr = "Beloved Hyperglass User"
- site_title: StrictStr = "hyperglass"
- site_description: StrictStr = "{org_name} Network Looking Glass"
- site_keywords: List[StrictStr] = [
+ debug: StrictBool = Field(
+ False,
+ title="Debug",
+ description="Enable debug mode. Warning: this will generate a *lot* of log output.",
+ )
+ developer_mode: StrictBool = Field(
+ False,
+ title="Developer Mode",
+ description='Enable developer mode. If enabled, the hyperglass backend (Python) and frontend (React/Javascript) applications are "unlinked", so that React tools can be used for front end development. A `