diff --git a/hyperglass/api/__init__.py b/hyperglass/api/__init__.py
index a2539ca..24ef8b9 100644
--- a/hyperglass/api/__init__.py
+++ b/hyperglass/api/__init__.py
@@ -18,6 +18,7 @@ from starlette.middleware.cors import CORSMiddleware
from hyperglass.log import log
from hyperglass.util import cpu_count
from hyperglass.constants import TRANSPORT_REST, __version__
+from hyperglass.models.ui import UIParameters
from hyperglass.api.events import on_startup, on_shutdown
from hyperglass.api.routes import (
docs,
@@ -25,8 +26,8 @@ from hyperglass.api.routes import (
query,
queries,
routers,
- communities,
ui_props,
+ communities,
import_certificate,
)
from hyperglass.exceptions import HyperglassError
@@ -246,6 +247,8 @@ app.add_api_route(
endpoint=ui_props,
methods=["GET", "OPTIONS"],
response_class=JSONResponse,
+ response_model=UIParameters,
+ response_model_by_alias=True,
)
# Enable certificate import route only if a device using
diff --git a/hyperglass/api/routes.py b/hyperglass/api/routes.py
index 6065efd..cd2ba9a 100644
--- a/hyperglass/api/routes.py
+++ b/hyperglass/api/routes.py
@@ -20,7 +20,7 @@ from hyperglass.api.tasks import process_headers, import_public_key
from hyperglass.constants import __version__
from hyperglass.exceptions import HyperglassError
from hyperglass.models.api import Query, EncodedRequest
-from hyperglass.configuration import REDIS_CONFIG, params, devices, frontend_params
+from hyperglass.configuration import REDIS_CONFIG, params, devices, ui_params
from hyperglass.execution.main import execute
# Local
@@ -266,7 +266,7 @@ async def info():
async def ui_props():
"""Serve UI configration."""
- return frontend_params
+ return ui_params
endpoints = [query, docs, routers, info, ui_props]
diff --git a/hyperglass/configuration/__init__.py b/hyperglass/configuration/__init__.py
index 073a8a1..aa652bc 100644
--- a/hyperglass/configuration/__init__.py
+++ b/hyperglass/configuration/__init__.py
@@ -10,5 +10,17 @@ from .main import (
params,
devices,
commands,
- frontend_params,
+ ui_params,
+)
+
+__all__ = (
+ "URL_DEV",
+ "URL_PROD",
+ "CONFIG_PATH",
+ "STATIC_PATH",
+ "REDIS_CONFIG",
+ "params",
+ "devices",
+ "commands",
+ "ui_params",
)
diff --git a/hyperglass/configuration/main.py b/hyperglass/configuration/main.py
index e1ad872..7875887 100644
--- a/hyperglass/configuration/main.py
+++ b/hyperglass/configuration/main.py
@@ -2,7 +2,6 @@
# Standard Library
import os
-import json
from typing import Dict, List, Generator
from pathlib import Path
@@ -20,6 +19,7 @@ from hyperglass.log import (
from hyperglass.util import set_app_path, set_cache_env, current_log_level
from hyperglass.defaults import CREDIT
from hyperglass.constants import PARSED_RESPONSE_FIELDS, __version__
+from hyperglass.models.ui import UIParameters
from hyperglass.util.files import check_path
from hyperglass.exceptions.private import ConfigError, ConfigMissing
from hyperglass.models.config.params import Params
@@ -204,35 +204,6 @@ except KeyError:
pass
-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}))
-
- for _network in _networks:
- network_def = {"display_name": _network, "locations": []}
- for device in devices.objects:
- if device.network.display_name == _network:
- network_def["locations"].append(
- {
- "_id": device._id,
- "name": device.name,
- "network": device.network.display_name,
- "directives": [c.frontend(params) for c in device.commands],
- }
- )
- networks.append(network_def)
-
- if not networks:
- raise ConfigError(message="Unable to build network to device mapping")
- return networks
-
-
-content_params = json.loads(
- params.json(include={"primary_asn", "org_name", "site_title", "site_description"})
-)
-
-
content_greeting = get_markdown(
config_path=params.web.greeting,
default="",
@@ -242,38 +213,17 @@ content_greeting = get_markdown(
content_credit = CREDIT.format(version=__version__)
-networks = _build_networks()
+_ui_params = params.frontend()
+_ui_params["web"]["logo"]["light_format"] = params.web.logo.light.suffix
+_ui_params["web"]["logo"]["dark_format"] = params.web.logo.dark.suffix
-_include_fields = {
- "cache": {"show_text", "timeout"},
- "debug": ...,
- "developer_mode": ...,
- "primary_asn": ...,
- "request_timeout": ...,
- "org_name": ...,
- "google_analytics": ...,
- "site_title": ...,
- "site_description": ...,
- "site_keywords": ...,
- "web": ...,
- "messages": ...,
-}
-_frontend_params = params.dict(include=_include_fields)
-
-
-_frontend_params["web"]["logo"]["light_format"] = params.web.logo.light.suffix
-_frontend_params["web"]["logo"]["dark_format"] = params.web.logo.dark.suffix
-
-_frontend_params.update(
- {
- "hyperglass_version": __version__,
- "queries": {**params.queries.map, "list": params.queries.list},
- "networks": networks,
- "parsed_data_fields": PARSED_RESPONSE_FIELDS,
- "content": {"credit": content_credit, "greeting": content_greeting},
- }
+ui_params = UIParameters(
+ **_ui_params,
+ version=__version__,
+ networks=devices.networks(params),
+ parsed_data_fields=PARSED_RESPONSE_FIELDS,
+ content={"credit": content_credit, "greeting": content_greeting},
)
-frontend_params = _frontend_params
URL_DEV = f"http://localhost:{str(params.listen_port)}/"
URL_PROD = "/api/"
diff --git a/hyperglass/main.py b/hyperglass/main.py
index b9e0d55..ecd48ee 100644
--- a/hyperglass/main.py
+++ b/hyperglass/main.py
@@ -41,7 +41,7 @@ from .configuration import (
CONFIG_PATH,
REDIS_CONFIG,
params,
- frontend_params,
+ ui_params,
)
from .util.frontend import build_frontend
@@ -85,7 +85,7 @@ async def build_ui() -> bool:
dev_mode=params.developer_mode,
dev_url=URL_DEV,
prod_url=URL_PROD,
- params=frontend_params,
+ params=ui_params,
app_path=CONFIG_PATH,
)
return True
diff --git a/hyperglass/models/commands/generic.py b/hyperglass/models/commands/generic.py
index 3174fc4..b33df39 100644
--- a/hyperglass/models/commands/generic.py
+++ b/hyperglass/models/commands/generic.py
@@ -2,7 +2,6 @@
# Standard Library
import re
-import json
from typing import Dict, List, Union, Literal, Optional
from ipaddress import IPv4Network, IPv6Network, ip_network
@@ -267,20 +266,10 @@ class Directive(HyperglassModel):
}
if self.info is not None:
- content_params = json.loads(
- params.json(
- include={
- "primary_asn",
- "org_name",
- "site_title",
- "site_description",
- }
- )
- )
with self.info.open() as md:
value["info"] = {
"enable": True,
- "params": content_params,
+ "params": params.content_params(),
"content": md.read(),
}
diff --git a/hyperglass/models/config/cache.py b/hyperglass/models/config/cache.py
index ca4499c..e5fd908 100644
--- a/hyperglass/models/config/cache.py
+++ b/hyperglass/models/config/cache.py
@@ -10,15 +10,20 @@ from pydantic import SecretStr, StrictInt, StrictStr, StrictBool, IPvAnyAddress
from ..main import HyperglassModel
-class Cache(HyperglassModel):
+class CachePublic(HyperglassModel):
+ """Public cache parameters."""
+
+ timeout: StrictInt = 120
+ show_text: StrictBool = True
+
+
+class Cache(CachePublic):
"""Validation model for params.cache."""
host: Union[IPvAnyAddress, StrictStr] = "localhost"
port: StrictInt = 6379
database: StrictInt = 1
password: Optional[SecretStr]
- timeout: StrictInt = 120
- show_text: StrictBool = True
class Config:
"""Pydantic model configuration."""
diff --git a/hyperglass/models/config/devices.py b/hyperglass/models/config/devices.py
index 6fe43a4..8bd1ff1 100644
--- a/hyperglass/models/config/devices.py
+++ b/hyperglass/models/config/devices.py
@@ -28,6 +28,7 @@ from hyperglass.models.commands.generic import Directive
from .ssl import Ssl
from ..main import HyperglassModel, HyperglassModelExtra
from .proxy import Proxy
+from .params import Params
from ..fields import SupportedDriver
from .network import Network
from .credential import Credential
@@ -274,3 +275,23 @@ class Devices(HyperglassModelExtra):
return device
raise AttributeError(f"No device named '{accessor}'")
+
+ def networks(self, params: Params) -> List[Dict[str, Any]]:
+ """Group devices by network."""
+ names = {device.network.display_name for device in self.objects}
+ return [
+ {
+ "display_name": name,
+ "locations": [
+ {
+ "id": device._id,
+ "name": device.name,
+ "network": device.network.display_name,
+ "directives": [c.frontend(params) for c in device.commands],
+ }
+ for device in self.objects
+ if device.network.display_name in names
+ ],
+ }
+ for name in names
+ ]
diff --git a/hyperglass/models/config/params.py b/hyperglass/models/config/params.py
index dc35064..8f7ae4d 100644
--- a/hyperglass/models/config/params.py
+++ b/hyperglass/models/config/params.py
@@ -1,19 +1,11 @@
"""Configuration validation entry point."""
# Standard Library
-from typing import List, Union, Optional
+from typing import Any, Dict, List, Union, Literal, Optional
from ipaddress import ip_address
# Third Party
-from pydantic import (
- Field,
- StrictInt,
- StrictStr,
- StrictBool,
- IPvAnyAddress,
- constr,
- validator,
-)
+from pydantic import Field, StrictInt, StrictStr, StrictBool, IPvAnyAddress, validator
# Local
from .web import Web
@@ -26,13 +18,12 @@ from .queries import Queries
from .messages import Messages
from .structured import Structured
-Localhost = constr(regex=r"localhost")
+Localhost = Literal["localhost"]
-class Params(HyperglassModel):
- """Validation model for all configuration variables."""
+class ParamsPublic(HyperglassModel):
+ """Public configuration parameters."""
- # Top Level Params
debug: StrictBool = Field(
False,
title="Debug",
@@ -43,10 +34,10 @@ class Params(HyperglassModel):
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 `` convenience component is also displayed in the UI for easier UI development.',
)
- fake_output: StrictBool = Field(
- False,
- title="Fake Output",
- description="If enabled, the hyperglass backend will return static fake output for development/testing purposes.",
+ request_timeout: StrictInt = Field(
+ 90,
+ title="Request Timeout",
+ description="Global timeout in seconds for all requests. The frontend application (UI) uses this field's exact value when submitting queries. The backend application uses this field's value, minus one second, for its own timeout handling. This is to ensure a contextual timeout error is presented to the end user in the event of a backend application timeout.",
)
primary_asn: Union[StrictInt, StrictStr] = Field(
"65001",
@@ -58,6 +49,7 @@ class Params(HyperglassModel):
title="Organization Name",
description="Your organization's name. This field is used in the UI & API documentation to set fields such as `` HTML tags for SEO and the terms & conditions footer component.",
)
+ google_analytics: Optional[StrictStr]
site_title: StrictStr = Field(
"hyperglass",
title="Site Title",
@@ -90,10 +82,17 @@ class Params(HyperglassModel):
title="Site Keywords",
description='Keywords pertaining to your hyperglass site. This field is used to generate `` HTML tags, which helps tremendously with SEO.',
)
- request_timeout: StrictInt = Field(
- 90,
- title="Request Timeout",
- description="Global timeout in seconds for all requests. The frontend application (UI) uses this field's exact value when submitting queries. The backend application uses this field's value, minus one second, for its own timeout handling. This is to ensure a contextual timeout error is presented to the end user in the event of a backend application timeout.",
+
+
+class Params(ParamsPublic, HyperglassModel):
+ """Validation model for all configuration variables."""
+
+ # Top Level Params
+
+ fake_output: StrictBool = Field(
+ False,
+ title="Fake Output",
+ description="If enabled, the hyperglass backend will return static fake output for development/testing purposes.",
)
listen_address: Optional[Union[IPvAnyAddress, Localhost]] = Field(
None,
@@ -115,7 +114,6 @@ class Params(HyperglassModel):
title="Netmiko Delay Factor",
description="Override the netmiko global delay factor.",
)
- google_analytics: Optional[StrictStr]
# Sub Level Params
cache: Cache = Cache()
@@ -183,3 +181,29 @@ class Params(HyperglassModel):
if not isinstance(value, str):
value = str(value)
return value
+
+ def content_params(self) -> Dict[str, Any]:
+ """Export content-specific parameters."""
+ return self.dict(
+ include={"primary_asn", "org_name", "site_title", "site_description"}
+ )
+
+ def frontend(self) -> Dict[str, Any]:
+ """Export UI-specific parameters."""
+
+ return self.dict(
+ include={
+ "cache": {"show_text", "timeout"},
+ "debug": ...,
+ "developer_mode": ...,
+ "primary_asn": ...,
+ "request_timeout": ...,
+ "org_name": ...,
+ "google_analytics": ...,
+ "site_title": ...,
+ "site_description": ...,
+ "site_keywords": ...,
+ "web": ...,
+ "messages": ...,
+ }
+ )
diff --git a/hyperglass/models/config/web.py b/hyperglass/models/config/web.py
index 34ff11d..b778a90 100644
--- a/hyperglass/models/config/web.py
+++ b/hyperglass/models/config/web.py
@@ -125,6 +125,13 @@ class Logo(HyperglassModel):
height: Optional[Union[StrictInt, Percentage]]
+class LogoPublic(Logo):
+ """Public logo configuration."""
+
+ light_format: StrictStr
+ dark_format: StrictStr
+
+
class Text(HyperglassModel):
"""Validation model for params.branding.text."""
@@ -258,3 +265,9 @@ class Web(HyperglassModel):
opengraph: OpenGraph = OpenGraph()
text: Text = Text()
theme: Theme = Theme()
+
+
+class WebPublic(Web):
+ """Public web configuration."""
+
+ logo: LogoPublic
diff --git a/hyperglass/models/main.py b/hyperglass/models/main.py
index 7e41a21..12eaae1 100644
--- a/hyperglass/models/main.py
+++ b/hyperglass/models/main.py
@@ -2,12 +2,13 @@
# Standard Library
import re
+from typing import Type, TypeVar
# Third Party
from pydantic import HttpUrl, BaseModel
-_WEBHOOK_TITLE = "hyperglass received a valid query with the following data"
-_ICON_URL = "https://res.cloudinary.com/hyperglass/image/upload/v1593192484/icon.png"
+# Project
+from hyperglass.util import snake_to_camel
def clean_name(_name: str) -> str:
@@ -22,11 +23,14 @@ def clean_name(_name: str) -> str:
return _scrubbed.lower()
+AsUIModel = TypeVar("AsUIModel", bound="BaseModel")
+
+
class HyperglassModel(BaseModel):
"""Base model for all hyperglass configuration models."""
class Config:
- """Default Pydantic configuration.
+ """Pydantic model configuration.
See https://pydantic-docs.helpmanual.io/usage/model_config
"""
@@ -38,40 +42,28 @@ class HyperglassModel(BaseModel):
json_encoders = {HttpUrl: lambda v: str(v)}
def export_json(self, *args, **kwargs):
- """Return instance as JSON.
+ """Return instance as JSON."""
- Returns:
- {str} -- Stringified JSON.
- """
+ export_kwargs = {"by_alias": True, "exclude_unset": False}
- export_kwargs = {
- "by_alias": True,
- "exclude_unset": False,
- **kwargs,
- }
+ for key in export_kwargs.keys():
+ export_kwargs.pop(key, None)
- return self.json(*args, **export_kwargs)
+ return self.json(*args, **export_kwargs, **kwargs)
def export_dict(self, *args, **kwargs):
- """Return instance as dictionary.
+ """Return instance as dictionary."""
- Returns:
- {dict} -- Python dictionary.
- """
- export_kwargs = {
- "by_alias": True,
- "exclude_unset": False,
- **kwargs,
- }
+ export_kwargs = {"by_alias": True, "exclude_unset": False}
- return self.dict(*args, **export_kwargs)
+ for key in export_kwargs.keys():
+ export_kwargs.pop(key, None)
+
+ return self.dict(*args, **export_kwargs, **kwargs)
def export_yaml(self, *args, **kwargs):
- """Return instance as YAML.
+ """Return instance as YAML."""
- Returns:
- {str} -- Stringified YAML.
- """
# Standard Library
import json
@@ -91,9 +83,22 @@ class HyperglassModel(BaseModel):
class HyperglassModelExtra(HyperglassModel):
"""Model for hyperglass configuration models with dynamic fields."""
- pass
-
class Config:
- """Default pydantic configuration."""
+ """Pydantic model configuration."""
extra = "allow"
+
+
+class HyperglassUIModel(HyperglassModel):
+ """Base class for UI configuration parameters."""
+
+ class Config:
+ """Pydantic model configuration."""
+
+ alias_generator = snake_to_camel
+ allow_population_by_field_name = True
+
+
+def as_ui_model(name: str, model: Type[AsUIModel]) -> Type[AsUIModel]:
+ """Override a model's configuration to confirm to a UI model."""
+ return type(name, (model, HyperglassUIModel), {})
diff --git a/hyperglass/models/ui.py b/hyperglass/models/ui.py
new file mode 100644
index 0000000..823e961
--- /dev/null
+++ b/hyperglass/models/ui.py
@@ -0,0 +1,76 @@
+"""UI Configuration models."""
+
+# Standard Library
+from typing import Any, Dict, List, Tuple, Union, Literal, Optional
+
+# Third Party
+from pydantic import StrictStr, StrictBool
+
+# Local
+from .main import HyperglassUIModel, as_ui_model
+from .config.web import WebPublic
+from .config.cache import CachePublic
+from .config.params import ParamsPublic
+from .config.messages import Messages
+
+Alignment = Union[Literal["left"], Literal["center"], Literal["right"], None]
+StructuredDataField = Tuple[str, str, Alignment]
+
+CacheUI = as_ui_model("CacheUI", CachePublic)
+WebUI = as_ui_model("WebUI", WebPublic)
+MessagesUI = as_ui_model("MessagesUI", Messages)
+
+
+class UIDirectiveInfo(HyperglassUIModel):
+ """UI: Directive Info."""
+
+ enable: StrictBool
+ params: Dict[str, str]
+ content: StrictStr
+
+
+class UIDirective(HyperglassUIModel):
+ """UI: Directive."""
+
+ id: StrictStr
+ name: StrictStr
+ field_type: StrictStr
+ groups: List[StrictStr]
+ description: StrictStr
+ info: Optional[UIDirectiveInfo] = None
+ options: Optional[List[Dict[str, Any]]]
+
+
+class UILocation(HyperglassUIModel):
+ """UI: Location (Device)."""
+
+ id: StrictStr
+ name: StrictStr
+ network: StrictStr
+ directives: List[UIDirective] = []
+
+
+class UINetwork(HyperglassUIModel):
+ """UI: Network."""
+
+ display_name: StrictStr
+ locations: List[UILocation] = []
+
+
+class UIContent(HyperglassUIModel):
+ """UI: Content."""
+
+ credit: StrictStr
+ greeting: StrictStr
+
+
+class UIParameters(HyperglassUIModel, ParamsPublic):
+ """UI Configuration Parameters."""
+
+ cache: CacheUI
+ web: WebUI
+ messages: MessagesUI
+ version: StrictStr
+ networks: List[UINetwork] = []
+ parsed_data_fields: Tuple[StructuredDataField, ...]
+ content: UIContent
diff --git a/hyperglass/util/frontend.py b/hyperglass/util/frontend.py
index e5cf382..68fa0f5 100644
--- a/hyperglass/util/frontend.py
+++ b/hyperglass/util/frontend.py
@@ -12,6 +12,7 @@ from pathlib import Path
# Project
from hyperglass.log import log
+from hyperglass.models.ui import UIParameters
# Local
from .files import copyfiles, check_path
@@ -221,7 +222,7 @@ def generate_opengraph(
return True
-def migrate_images(app_path: Path, params: dict):
+def migrate_images(app_path: Path, params: UIParameters):
"""Migrate images from source code to install directory."""
images_dir = app_path / "static" / "images"
favicon_dir = images_dir / "favicons"
@@ -230,7 +231,7 @@ def migrate_images(app_path: Path, params: dict):
dst_files = ()
for image in ("light", "dark", "favicon"):
- src = Path(params["web"]["logo"][image])
+ src: Path = getattr(params.web.logo, image)
dst = images_dir / f"{image + src.suffix}"
src_files += (src,)
dst_files += (dst,)
@@ -241,7 +242,7 @@ async def build_frontend( # noqa: C901
dev_mode: bool,
dev_url: str,
prod_url: str,
- params: Dict,
+ params: UIParameters,
app_path: Path,
force: bool = False,
timeout: int = 180,
@@ -259,18 +260,6 @@ async def build_frontend( # noqa: C901
After the build is successful, the temporary file is automatically
closed during garbage collection.
-
- Arguments:
- dev_mode {bool} -- Development Mode
- dev_url {str} -- Development Mode URL
- prod_url {str} -- Production Mode URL
- params {dict} -- Frontend Config paramters
-
- Raises:
- RuntimeError: Raised if errors occur during build process.
-
- Returns:
- {bool} -- True if successful
"""
# Standard Library
import hashlib
@@ -326,7 +315,7 @@ async def build_frontend( # noqa: C901
if not favicon_dir.exists():
favicon_dir.mkdir()
async with Favicons(
- source=params["web"]["logo"]["favicon"],
+ source=params.web.logo.favicon,
output_directory=favicon_dir,
base_url="/images/favicons/",
) as favicons:
@@ -334,7 +323,7 @@ async def build_frontend( # noqa: C901
log.debug("Generated {} favicons", favicons.completed)
env_vars["hyperglass"].update({"favicons": favicons.formats()})
build_data = {
- "params": params,
+ "params": params.export_dict(),
"version": __version__,
"package_json": package_json,
}
@@ -379,11 +368,11 @@ async def build_frontend( # noqa: C901
migrate_images(app_path, params)
generate_opengraph(
- Path(params["web"]["opengraph"]["image"]),
+ params.web.opengraph.image,
1200,
630,
images_dir,
- params["web"]["theme"]["colors"]["black"],
+ params.web.theme.colors.black,
)
except Exception as err: