upgrade major dependencies

This commit is contained in:
thatmattlove 2024-03-16 23:17:54 -04:00
parent e00cccb0a1
commit 77c0a31256
62 changed files with 1036 additions and 1382 deletions

View file

@ -8,7 +8,7 @@ from pathlib import Path
# Third Party # Third Party
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.exceptions import ValidationError, RequestValidationError from fastapi.exceptions import ValidationException, RequestValidationError
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.exceptions import HTTPException as StarletteHTTPException
from fastapi.openapi.utils import get_openapi from fastapi.openapi.utils import get_openapi
@ -99,7 +99,7 @@ app.add_exception_handler(HyperglassError, app_handler)
app.add_exception_handler(RequestValidationError, validation_handler) app.add_exception_handler(RequestValidationError, validation_handler)
# App Validation Error Handler # App Validation Error Handler
app.add_exception_handler(ValidationError, validation_handler) app.add_exception_handler(ValidationException, validation_handler)
# Uncaught Error Handler # Uncaught Error Handler
app.add_exception_handler(Exception, default_handler) app.add_exception_handler(Exception, default_handler)

View file

@ -64,7 +64,7 @@ def init_params() -> "Params":
# from params. # from params.
try: try:
params.web.text.subtitle = params.web.text.subtitle.format( params.web.text.subtitle = params.web.text.subtitle.format(
**params.dict(exclude={"web", "queries", "messages"}) **params.model_dump(exclude={"web", "queries", "messages"})
) )
except KeyError: except KeyError:
pass pass

View file

@ -1,7 +1,13 @@
"""Default Arista Directives.""" """Default Arista Directives."""
# Project # Project
from hyperglass.models.directive import Rule, Text, BuiltinDirective from hyperglass.models.directive import (
BuiltinDirective,
RuleWithIPv4,
RuleWithIPv6,
RuleWithPattern,
Text,
)
__all__ = ( __all__ = (
"AristaBGPRoute", "AristaBGPRoute",
@ -18,12 +24,12 @@ AristaBGPRoute = BuiltinDirective(
id="__hyperglass_arista_eos_bgp_route__", id="__hyperglass_arista_eos_bgp_route__",
name="BGP Route", name="BGP Route",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="show ip bgp {target}", command="show ip bgp {target}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="show ipv6 bgp {target}", command="show ipv6 bgp {target}",
@ -38,7 +44,7 @@ AristaBGPASPath = BuiltinDirective(
id="__hyperglass_arista_eos_bgp_aspath__", id="__hyperglass_arista_eos_bgp_aspath__",
name="BGP AS Path", name="BGP AS Path",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -56,7 +62,7 @@ AristaBGPCommunity = BuiltinDirective(
id="__hyperglass_arista_eos_bgp_community__", id="__hyperglass_arista_eos_bgp_community__",
name="BGP Community", name="BGP Community",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -75,12 +81,12 @@ AristaPing = BuiltinDirective(
id="__hyperglass_arista_eos_ping__", id="__hyperglass_arista_eos_ping__",
name="Ping", name="Ping",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="ping ip {target} source {source4}", command="ping ip {target} source {source4}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="ping ipv6 {target} source {source6}", command="ping ipv6 {target} source {source6}",
@ -94,12 +100,12 @@ AristaTraceroute = BuiltinDirective(
id="__hyperglass_arista_eos_traceroute__", id="__hyperglass_arista_eos_traceroute__",
name="Traceroute", name="Traceroute",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="traceroute ip {target} source {source4}", command="traceroute ip {target} source {source4}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="traceroute ipv6 {target} source {source6}", command="traceroute ipv6 {target} source {source6}",
@ -115,12 +121,12 @@ AristaBGPRouteTable = BuiltinDirective(
id="__hyperglass_arista_eos_bgp_route_table__", id="__hyperglass_arista_eos_bgp_route_table__",
name="BGP Route", name="BGP Route",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="show ip bgp {target} | json", command="show ip bgp {target} | json",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="show ipv6 bgp {target} | json", command="show ipv6 bgp {target} | json",
@ -134,7 +140,7 @@ AristaBGPASPathTable = BuiltinDirective(
id="__hyperglass_arista_eos_bgp_aspath_table__", id="__hyperglass_arista_eos_bgp_aspath_table__",
name="BGP AS Path", name="BGP AS Path",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -151,7 +157,7 @@ AristaBGPCommunityTable = BuiltinDirective(
id="__hyperglass_arista_eos_bgp_community_table__", id="__hyperglass_arista_eos_bgp_community_table__",
name="BGP Community", name="BGP Community",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[

View file

@ -1,7 +1,13 @@
"""Default BIRD Directives.""" """Default BIRD Directives."""
# Project # Project
from hyperglass.models.directive import Rule, Text, BuiltinDirective from hyperglass.models.directive import (
RuleWithIPv4,
RuleWithIPv6,
RuleWithPattern,
Text,
BuiltinDirective,
)
__all__ = ( __all__ = (
"BIRD_BGPASPath", "BIRD_BGPASPath",
@ -15,12 +21,12 @@ BIRD_BGPRoute = BuiltinDirective(
id="__hyperglass_bird_bgp_route__", id="__hyperglass_bird_bgp_route__",
name="BGP Route", name="BGP Route",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command='birdc "show route all where {target} ~ net"', command='birdc "show route all where {target} ~ net"',
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command='birdc "show route all where {target} ~ net"', command='birdc "show route all where {target} ~ net"',
@ -34,7 +40,7 @@ BIRD_BGPASPath = BuiltinDirective(
id="__hyperglass_bird_bgp_aspath__", id="__hyperglass_bird_bgp_aspath__",
name="BGP AS Path", name="BGP AS Path",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -50,7 +56,7 @@ BIRD_BGPCommunity = BuiltinDirective(
id="__hyperglass_bird_bgp_community__", id="__hyperglass_bird_bgp_community__",
name="BGP Community", name="BGP Community",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -66,12 +72,12 @@ BIRD_Ping = BuiltinDirective(
id="__hyperglass_bird_ping__", id="__hyperglass_bird_ping__",
name="Ping", name="Ping",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="ping -4 -c 5 -I {source4} {target}", command="ping -4 -c 5 -I {source4} {target}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="ping -6 -c 5 -I {source6} {target}", command="ping -6 -c 5 -I {source6} {target}",
@ -85,12 +91,12 @@ BIRD_Traceroute = BuiltinDirective(
id="__hyperglass_bird_traceroute__", id="__hyperglass_bird_traceroute__",
name="Traceroute", name="Traceroute",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="traceroute -4 -w 1 -q 1 -s {source4} {target}", command="traceroute -4 -w 1 -q 1 -s {source4} {target}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="traceroute -6 -w 1 -q 1 -s {source6} {target}", command="traceroute -6 -w 1 -q 1 -s {source6} {target}",

View file

@ -1,7 +1,13 @@
"""Default Cisco IOS Directives.""" """Default Cisco IOS Directives."""
# Project # Project
from hyperglass.models.directive import Rule, Text, BuiltinDirective from hyperglass.models.directive import (
RuleWithIPv4,
RuleWithIPv6,
RuleWithPattern,
Text,
BuiltinDirective,
)
__all__ = ( __all__ = (
"CiscoIOS_BGPASPath", "CiscoIOS_BGPASPath",
@ -15,12 +21,12 @@ CiscoIOS_BGPRoute = BuiltinDirective(
id="__hyperglass_cisco_ios_bgp_route__", id="__hyperglass_cisco_ios_bgp_route__",
name="BGP Route", name="BGP Route",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="show bgp ipv4 unicast {target} | exclude pathid:|Epoch", command="show bgp ipv4 unicast {target} | exclude pathid:|Epoch",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="show bgp ipv6 unicast {target} | exclude pathid:|Epoch", command="show bgp ipv6 unicast {target} | exclude pathid:|Epoch",
@ -34,7 +40,7 @@ CiscoIOS_BGPASPath = BuiltinDirective(
id="__hyperglass_cisco_ios_bgp_aspath__", id="__hyperglass_cisco_ios_bgp_aspath__",
name="BGP AS Path", name="BGP AS Path",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -51,7 +57,7 @@ CiscoIOS_BGPCommunity = BuiltinDirective(
id="__hyperglass_cisco_ios_bgp_community__", id="__hyperglass_cisco_ios_bgp_community__",
name="BGP Community", name="BGP Community",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -68,12 +74,12 @@ CiscoIOS_Ping = BuiltinDirective(
id="__hyperglass_cisco_ios_ping__", id="__hyperglass_cisco_ios_ping__",
name="Ping", name="Ping",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="ping {target} repeat 5 source {source4}", command="ping {target} repeat 5 source {source4}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="ping ipv6 {target} repeat 5 source {source6}", command="ping ipv6 {target} repeat 5 source {source6}",
@ -87,12 +93,12 @@ CiscoIOS_Traceroute = BuiltinDirective(
id="__hyperglass_cisco_ios_traceroute__", id="__hyperglass_cisco_ios_traceroute__",
name="Traceroute", name="Traceroute",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="traceroute {target} timeout 1 probe 2 source {source4}", command="traceroute {target} timeout 1 probe 2 source {source4}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="traceroute ipv6 {target} timeout 1 probe 2 source {source6}", command="traceroute ipv6 {target} timeout 1 probe 2 source {source6}",

View file

@ -1,7 +1,13 @@
"""Default Cisco NX-OS Directives.""" """Default Cisco NX-OS Directives."""
# Project # Project
from hyperglass.models.directive import Rule, Text, BuiltinDirective from hyperglass.models.directive import (
RuleWithIPv4,
RuleWithIPv6,
RuleWithPattern,
Text,
BuiltinDirective,
)
__all__ = ( __all__ = (
"CiscoNXOS_BGPASPath", "CiscoNXOS_BGPASPath",
@ -15,12 +21,12 @@ CiscoNXOS_BGPRoute = BuiltinDirective(
id="__hyperglass_cisco_nxos_bgp_route__", id="__hyperglass_cisco_nxos_bgp_route__",
name="BGP Route", name="BGP Route",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="show bgp ipv4 unicast {target}", command="show bgp ipv4 unicast {target}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="show bgp ipv6 unicast {target}", command="show bgp ipv6 unicast {target}",
@ -34,7 +40,7 @@ CiscoNXOS_BGPASPath = BuiltinDirective(
id="__hyperglass_cisco_nxos_bgp_aspath__", id="__hyperglass_cisco_nxos_bgp_aspath__",
name="BGP AS Path", name="BGP AS Path",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -51,7 +57,7 @@ CiscoNXOS_BGPCommunity = BuiltinDirective(
id="__hyperglass_cisco_nxos_bgp_community__", id="__hyperglass_cisco_nxos_bgp_community__",
name="BGP Community", name="BGP Community",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -68,12 +74,12 @@ CiscoNXOS_Ping = BuiltinDirective(
id="__hyperglass_cisco_nxos_ping__", id="__hyperglass_cisco_nxos_ping__",
name="Ping", name="Ping",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="ping {target} source {source4}", command="ping {target} source {source4}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="ping6 {target} source {source6}", command="ping6 {target} source {source6}",
@ -87,12 +93,12 @@ CiscoNXOS_Traceroute = BuiltinDirective(
id="__hyperglass_cisco_nxos_traceroute__", id="__hyperglass_cisco_nxos_traceroute__",
name="Traceroute", name="Traceroute",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="traceroute {target} source {source4}", command="traceroute {target} source {source4}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="traceroute6 {target} source {source6}", command="traceroute6 {target} source {source6}",

View file

@ -1,7 +1,13 @@
"""Default Cisco IOS-XR Directives.""" """Default Cisco IOS-XR Directives."""
# Project # Project
from hyperglass.models.directive import Rule, Text, BuiltinDirective from hyperglass.models.directive import (
RuleWithIPv4,
RuleWithIPv6,
RuleWithPattern,
Text,
BuiltinDirective,
)
__all__ = ( __all__ = (
"CiscoXR_BGPASPath", "CiscoXR_BGPASPath",
@ -15,12 +21,12 @@ CiscoXR_BGPRoute = BuiltinDirective(
id="__hyperglass_cisco_xr_bgp_route__", id="__hyperglass_cisco_xr_bgp_route__",
name="BGP Route", name="BGP Route",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="show bgp ipv4 unicast {target}", command="show bgp ipv4 unicast {target}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="show bgp ipv6 unicast {target}", command="show bgp ipv6 unicast {target}",
@ -34,7 +40,7 @@ CiscoXR_BGPASPath = BuiltinDirective(
id="__hyperglass_cisco_xr_bgp_aspath__", id="__hyperglass_cisco_xr_bgp_aspath__",
name="BGP AS Path", name="BGP AS Path",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -51,7 +57,7 @@ CiscoXR_BGPCommunity = BuiltinDirective(
id="__hyperglass_cisco_xr_bgp_community__", id="__hyperglass_cisco_xr_bgp_community__",
name="BGP Community", name="BGP Community",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -68,12 +74,12 @@ CiscoXR_Ping = BuiltinDirective(
id="__hyperglass_cisco_xr_ping__", id="__hyperglass_cisco_xr_ping__",
name="Ping", name="Ping",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="ping ipv4 {target} count 5 source {source4}", command="ping ipv4 {target} count 5 source {source4}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="ping ipv6 {target} count 5 source {source6}", command="ping ipv6 {target} count 5 source {source6}",
@ -87,12 +93,12 @@ CiscoXR_Traceroute = BuiltinDirective(
id="__hyperglass_cisco_xr_traceroute__", id="__hyperglass_cisco_xr_traceroute__",
name="Traceroute", name="Traceroute",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="traceroute ipv4 {target} timeout 1 probe 2 source {source4}", command="traceroute ipv4 {target} timeout 1 probe 2 source {source4}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="traceroute ipv6 {target} timeout 1 probe 2 source {source6}", command="traceroute ipv6 {target} timeout 1 probe 2 source {source6}",

View file

@ -1,7 +1,13 @@
"""Default FRRouting Directives.""" """Default FRRouting Directives."""
# Project # Project
from hyperglass.models.directive import Rule, Text, BuiltinDirective from hyperglass.models.directive import (
RuleWithIPv4,
RuleWithIPv6,
RuleWithPattern,
Text,
BuiltinDirective,
)
__all__ = ( __all__ = (
"FRRouting_BGPASPath", "FRRouting_BGPASPath",
@ -15,12 +21,12 @@ FRRouting_BGPRoute = BuiltinDirective(
id="__hyperglass_frr_bgp_route__", id="__hyperglass_frr_bgp_route__",
name="BGP Route", name="BGP Route",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command='vtysh -c "show bgp ipv4 unicast {target}"', command='vtysh -c "show bgp ipv4 unicast {target}"',
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command='vtysh -c "show bgp ipv6 unicast {target}"', command='vtysh -c "show bgp ipv6 unicast {target}"',
@ -34,7 +40,7 @@ FRRouting_BGPASPath = BuiltinDirective(
id="__hyperglass_frr_bgp_aspath__", id="__hyperglass_frr_bgp_aspath__",
name="BGP AS Path", name="BGP AS Path",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -51,7 +57,7 @@ FRRouting_BGPCommunity = BuiltinDirective(
id="__hyperglass_frr_bgp_community__", id="__hyperglass_frr_bgp_community__",
name="BGP Community", name="BGP Community",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -68,12 +74,12 @@ FRRouting_Ping = BuiltinDirective(
id="__hyperglass_frr_ping__", id="__hyperglass_frr_ping__",
name="Ping", name="Ping",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="ping -4 -c 5 -I {source4} {target}", command="ping -4 -c 5 -I {source4} {target}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="ping -6 -c 5 -I {source6} {target}", command="ping -6 -c 5 -I {source6} {target}",
@ -87,12 +93,12 @@ FRRouting_Traceroute = BuiltinDirective(
id="__hyperglass_frr_traceroute__", id="__hyperglass_frr_traceroute__",
name="Traceroute", name="Traceroute",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="traceroute -4 -w 1 -q 1 -s {source4} {target}", command="traceroute -4 -w 1 -q 1 -s {source4} {target}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="traceroute -6 -w 1 -q 1 -s {source6} {target}", command="traceroute -6 -w 1 -q 1 -s {source6} {target}",

View file

@ -1,7 +1,13 @@
"""Default Huawei Directives.""" """Default Huawei Directives."""
# Project # Project
from hyperglass.models.directive import Rule, Text, BuiltinDirective from hyperglass.models.directive import (
RuleWithIPv4,
RuleWithIPv6,
RuleWithPattern,
Text,
BuiltinDirective,
)
__all__ = ( __all__ = (
"Huawei_BGPASPath", "Huawei_BGPASPath",
@ -15,12 +21,12 @@ Huawei_BGPRoute = BuiltinDirective(
id="__hyperglass_huawei_bgp_route__", id="__hyperglass_huawei_bgp_route__",
name="BGP Route", name="BGP Route",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="display bgp routing-table {target}", command="display bgp routing-table {target}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="display bgp ipv6 routing-table {target}", command="display bgp ipv6 routing-table {target}",
@ -34,7 +40,7 @@ Huawei_BGPASPath = BuiltinDirective(
id="__hyperglass_huawei_bgp_aspath__", id="__hyperglass_huawei_bgp_aspath__",
name="BGP AS Path", name="BGP AS Path",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -51,7 +57,7 @@ Huawei_BGPCommunity = BuiltinDirective(
id="__hyperglass_huawei_bgp_community__", id="__hyperglass_huawei_bgp_community__",
name="BGP Community", name="BGP Community",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -68,12 +74,12 @@ Huawei_Ping = BuiltinDirective(
id="__hyperglass_huawei_ping__", id="__hyperglass_huawei_ping__",
name="Ping", name="Ping",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="ping -c 5 -a {source4} {target}", command="ping -c 5 -a {source4} {target}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="ping ipv6 -c 5 -a {source6} {target}", command="ping ipv6 -c 5 -a {source6} {target}",
@ -87,12 +93,12 @@ Huawei_Traceroute = BuiltinDirective(
id="__hyperglass_huawei_traceroute__", id="__hyperglass_huawei_traceroute__",
name="Traceroute", name="Traceroute",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="tracert -q 2 -f 1 -a {source4} {target}", command="tracert -q 2 -f 1 -a {source4} {target}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="tracert -q 2 -f 1 -a {source6} {target}", command="tracert -q 2 -f 1 -a {source6} {target}",

View file

@ -1,7 +1,13 @@
"""Default Juniper Directives.""" """Default Juniper Directives."""
# Project # Project
from hyperglass.models.directive import Rule, Text, BuiltinDirective from hyperglass.models.directive import (
RuleWithIPv4,
RuleWithIPv6,
RuleWithPattern,
Text,
BuiltinDirective,
)
__all__ = ( __all__ = (
"JuniperBGPRoute", "JuniperBGPRoute",
@ -18,12 +24,12 @@ JuniperBGPRoute = BuiltinDirective(
id="__hyperglass_juniper_bgp_route__", id="__hyperglass_juniper_bgp_route__",
name="BGP Route", name="BGP Route",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="show route protocol bgp table inet.0 {target} detail", command="show route protocol bgp table inet.0 {target} detail",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="show route protocol bgp table inet6.0 {target} detail", command="show route protocol bgp table inet6.0 {target} detail",
@ -38,7 +44,7 @@ JuniperBGPASPath = BuiltinDirective(
id="__hyperglass_juniper_bgp_aspath__", id="__hyperglass_juniper_bgp_aspath__",
name="BGP AS Path", name="BGP AS Path",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -56,7 +62,7 @@ JuniperBGPCommunity = BuiltinDirective(
id="__hyperglass_juniper_bgp_community__", id="__hyperglass_juniper_bgp_community__",
name="BGP Community", name="BGP Community",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -75,12 +81,12 @@ JuniperPing = BuiltinDirective(
id="__hyperglass_juniper_ping__", id="__hyperglass_juniper_ping__",
name="Ping", name="Ping",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="ping inet {target} count 5 source {source4}", command="ping inet {target} count 5 source {source4}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="ping inet6 {target} count 5 source {source6}", command="ping inet6 {target} count 5 source {source6}",
@ -94,12 +100,12 @@ JuniperTraceroute = BuiltinDirective(
id="__hyperglass_juniper_traceroute__", id="__hyperglass_juniper_traceroute__",
name="Traceroute", name="Traceroute",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="traceroute inet {target} wait 1 source {source4}", command="traceroute inet {target} wait 1 source {source4}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="traceroute inet6 {target} wait 1 source {source6}", command="traceroute inet6 {target} wait 1 source {source6}",
@ -115,12 +121,12 @@ JuniperBGPRouteTable = BuiltinDirective(
id="__hyperglass_juniper_bgp_route_table__", id="__hyperglass_juniper_bgp_route_table__",
name="BGP Route", name="BGP Route",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="show route protocol bgp table inet.0 {target} best detail | display xml", command="show route protocol bgp table inet.0 {target} best detail | display xml",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="show route protocol bgp table inet6.0 {target} best detail | display xml", command="show route protocol bgp table inet6.0 {target} best detail | display xml",
@ -134,7 +140,7 @@ JuniperBGPASPathTable = BuiltinDirective(
id="__hyperglass_juniper_bgp_aspath_table__", id="__hyperglass_juniper_bgp_aspath_table__",
name="BGP AS Path", name="BGP AS Path",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -151,7 +157,7 @@ JuniperBGPCommunityTable = BuiltinDirective(
id="__hyperglass_juniper_bgp_community_table__", id="__hyperglass_juniper_bgp_community_table__",
name="BGP Community", name="BGP Community",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[

View file

@ -1,7 +1,13 @@
"""Default Mikrotik Directives.""" """Default Mikrotik Directives."""
# Project # Project
from hyperglass.models.directive import Rule, Text, BuiltinDirective from hyperglass.models.directive import (
RuleWithIPv4,
RuleWithIPv6,
RuleWithPattern,
Text,
BuiltinDirective,
)
__all__ = ( __all__ = (
"Mikrotik_BGPASPath", "Mikrotik_BGPASPath",
@ -15,12 +21,12 @@ Mikrotik_BGPRoute = BuiltinDirective(
id="__hyperglass_mikrotik_bgp_route__", id="__hyperglass_mikrotik_bgp_route__",
name="BGP Route", name="BGP Route",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="ip route print where dst-address={target}", command="ip route print where dst-address={target}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="ipv6 route print where dst-address={target}", command="ipv6 route print where dst-address={target}",
@ -34,7 +40,7 @@ Mikrotik_BGPASPath = BuiltinDirective(
id="__hyperglass_mikrotik_bgp_aspath__", id="__hyperglass_mikrotik_bgp_aspath__",
name="BGP AS Path", name="BGP AS Path",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -51,7 +57,7 @@ Mikrotik_BGPCommunity = BuiltinDirective(
id="__hyperglass_mikrotik_bgp_community__", id="__hyperglass_mikrotik_bgp_community__",
name="BGP Community", name="BGP Community",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -68,12 +74,12 @@ Mikrotik_Ping = BuiltinDirective(
id="__hyperglass_mikrotik_ping__", id="__hyperglass_mikrotik_ping__",
name="Ping", name="Ping",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="ping src-address={source4} count=5 {target}", command="ping src-address={source4} count=5 {target}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="ping src-address={source6} count=5 {target}", command="ping src-address={source6} count=5 {target}",
@ -87,12 +93,12 @@ Mikrotik_Traceroute = BuiltinDirective(
id="__hyperglass_mikrotik_traceroute__", id="__hyperglass_mikrotik_traceroute__",
name="Traceroute", name="Traceroute",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="tool traceroute src-address={source4} timeout=1 duration=5 count=1 {target}", command="tool traceroute src-address={source4} timeout=1 duration=5 count=1 {target}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="tool traceroute src-address={source6} timeout=1 duration=5 count=1 {target}", command="tool traceroute src-address={source6} timeout=1 duration=5 count=1 {target}",

View file

@ -1,7 +1,13 @@
"""Default Nokia SR-OS Directives.""" """Default Nokia SR-OS Directives."""
# Project # Project
from hyperglass.models.directive import Rule, Text, BuiltinDirective from hyperglass.models.directive import (
RuleWithIPv4,
RuleWithIPv6,
RuleWithPattern,
Text,
BuiltinDirective,
)
__all__ = ( __all__ = (
"NokiaSROS_BGPASPath", "NokiaSROS_BGPASPath",
@ -15,12 +21,12 @@ NokiaSROS_BGPRoute = BuiltinDirective(
id="__hyperglass_nokia_sros_bgp_route__", id="__hyperglass_nokia_sros_bgp_route__",
name="BGP Route", name="BGP Route",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="/show router bgp routes {target} ipv4 hunt", command="/show router bgp routes {target} ipv4 hunt",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="/show router bgp routes {target} ipv6 hunt", command="/show router bgp routes {target} ipv6 hunt",
@ -34,7 +40,7 @@ NokiaSROS_BGPASPath = BuiltinDirective(
id="__hyperglass_nokia_sros_bgp_aspath__", id="__hyperglass_nokia_sros_bgp_aspath__",
name="BGP AS Path", name="BGP AS Path",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -50,7 +56,7 @@ NokiaSROS_BGPCommunity = BuiltinDirective(
id="__hyperglass_nokia_sros_bgp_community__", id="__hyperglass_nokia_sros_bgp_community__",
name="BGP Community", name="BGP Community",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -66,12 +72,12 @@ NokiaSROS_Ping = BuiltinDirective(
id="__hyperglass_nokia_sros_ping__", id="__hyperglass_nokia_sros_ping__",
name="Ping", name="Ping",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="/ping {target} source-address {source4}", command="/ping {target} source-address {source4}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="/ping {target} source-address {source6}", command="/ping {target} source-address {source6}",
@ -85,12 +91,12 @@ NokiaSROS_Traceroute = BuiltinDirective(
id="__hyperglass_nokia_sros_traceroute__", id="__hyperglass_nokia_sros_traceroute__",
name="Traceroute", name="Traceroute",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="/traceroute {target} source-address {source4} wait 2 seconds", command="/traceroute {target} source-address {source4} wait 2 seconds",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="/traceroute {target} source-address {source6} wait 2 seconds", command="/traceroute {target} source-address {source6} wait 2 seconds",

View file

@ -1,7 +1,13 @@
"""Default FRRouting Directives.""" """Default FRRouting Directives."""
# Project # Project
from hyperglass.models.directive import Rule, Text, BuiltinDirective from hyperglass.models.directive import (
RuleWithIPv4,
RuleWithIPv6,
RuleWithPattern,
Text,
BuiltinDirective,
)
__all__ = ( __all__ = (
"OpenBGPD_BGPASPath", "OpenBGPD_BGPASPath",
@ -15,12 +21,12 @@ OpenBGPD_BGPRoute = BuiltinDirective(
id="__hyperglass_openbgpd_bgp_route__", id="__hyperglass_openbgpd_bgp_route__",
name="BGP Route", name="BGP Route",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="bgpctl show rib inet {target}", command="bgpctl show rib inet {target}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="bgpctl show rib inet6 {target}", command="bgpctl show rib inet6 {target}",
@ -34,7 +40,7 @@ OpenBGPD_BGPASPath = BuiltinDirective(
id="__hyperglass_openbgpd_bgp_aspath__", id="__hyperglass_openbgpd_bgp_aspath__",
name="BGP AS Path", name="BGP AS Path",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -51,7 +57,7 @@ OpenBGPD_BGPCommunity = BuiltinDirective(
id="__hyperglass_openbgpd_bgp_community__", id="__hyperglass_openbgpd_bgp_community__",
name="BGP Community", name="BGP Community",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -68,12 +74,12 @@ OpenBGPD_Ping = BuiltinDirective(
id="__hyperglass_openbgpd_ping__", id="__hyperglass_openbgpd_ping__",
name="Ping", name="Ping",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="ping -4 -c 5 -I {source4} {target}", command="ping -4 -c 5 -I {source4} {target}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="ping -6 -c 5 -I {source6} {target}", command="ping -6 -c 5 -I {source6} {target}",
@ -87,12 +93,12 @@ OpenBGPD_Traceroute = BuiltinDirective(
id="__hyperglass_openbgpd_traceroute__", id="__hyperglass_openbgpd_traceroute__",
name="Traceroute", name="Traceroute",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="traceroute -4 -w 1 -q 1 -s {source4} {target}", command="traceroute -4 -w 1 -q 1 -s {source4} {target}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="traceroute -6 -w 1 -q 1 -s {source6} {target}", command="traceroute -6 -w 1 -q 1 -s {source6} {target}",

View file

@ -1,7 +1,13 @@
"""Default TNSR Directives.""" """Default TNSR Directives."""
# Project # Project
from hyperglass.models.directive import Rule, Text, BuiltinDirective from hyperglass.models.directive import (
RuleWithIPv4,
RuleWithIPv6,
RuleWithPattern,
Text,
BuiltinDirective,
)
__all__ = ( __all__ = (
"TNSR_BGPASPath", "TNSR_BGPASPath",
@ -15,12 +21,12 @@ TNSR_BGPRoute = BuiltinDirective(
id="__hyperglass_tnsr_bgp_route__", id="__hyperglass_tnsr_bgp_route__",
name="BGP Route", name="BGP Route",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command='dataplane shell sudo vtysh -c "show bgp ipv4 unicast {target}"', command='dataplane shell sudo vtysh -c "show bgp ipv4 unicast {target}"',
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command='dataplane shell sudo vtysh -c "show bgp ipv6 unicast {target}"', command='dataplane shell sudo vtysh -c "show bgp ipv6 unicast {target}"',
@ -34,7 +40,7 @@ TNSR_BGPASPath = BuiltinDirective(
id="__hyperglass_tnsr_bgp_aspath__", id="__hyperglass_tnsr_bgp_aspath__",
name="BGP AS Path", name="BGP AS Path",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -51,7 +57,7 @@ TNSR_BGPCommunity = BuiltinDirective(
id="__hyperglass_tnsr_bgp_community__", id="__hyperglass_tnsr_bgp_community__",
name="BGP Community", name="BGP Community",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -68,12 +74,12 @@ TNSR_Ping = BuiltinDirective(
id="__hyperglass_tnsr_ping__", id="__hyperglass_tnsr_ping__",
name="Ping", name="Ping",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="ping {target} ipv4 source {source4} count 5 timeout 1", command="ping {target} ipv4 source {source4} count 5 timeout 1",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="ping {target} ipv6 source {source6} count 5 timeout 1", command="ping {target} ipv6 source {source6} count 5 timeout 1",
@ -87,12 +93,12 @@ TNSR_Traceroute = BuiltinDirective(
id="__hyperglass_tnsr_traceroute__", id="__hyperglass_tnsr_traceroute__",
name="Traceroute", name="Traceroute",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="traceroute {target} ipv4 source {source4} timeout 1 waittime 1", command="traceroute {target} ipv4 source {source4} timeout 1 waittime 1",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="traceroute {target} ipv6 source {source6} timeout 1 waittime 1", command="traceroute {target} ipv6 source {source6} timeout 1 waittime 1",

View file

@ -1,7 +1,13 @@
"""Default VyOS Directives.""" """Default VyOS Directives."""
# Project # Project
from hyperglass.models.directive import Rule, Text, BuiltinDirective from hyperglass.models.directive import (
RuleWithIPv4,
RuleWithIPv6,
RuleWithPattern,
Text,
BuiltinDirective,
)
__all__ = ( __all__ = (
"VyOS_BGPASPath", "VyOS_BGPASPath",
@ -15,12 +21,12 @@ VyOS_BGPRoute = BuiltinDirective(
id="__hyperglass_vyos_bgp_route__", id="__hyperglass_vyos_bgp_route__",
name="BGP Route", name="BGP Route",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="show ip bgp {target}", command="show ip bgp {target}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="show ipv6 bgp {target}", command="show ipv6 bgp {target}",
@ -34,7 +40,7 @@ VyOS_BGPASPath = BuiltinDirective(
id="__hyperglass_vyos_bgp_aspath__", id="__hyperglass_vyos_bgp_aspath__",
name="BGP AS Path", name="BGP AS Path",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -51,7 +57,7 @@ VyOS_BGPCommunity = BuiltinDirective(
id="__hyperglass_vyos_bgp_community__", id="__hyperglass_vyos_bgp_community__",
name="BGP Community", name="BGP Community",
rules=[ rules=[
Rule( RuleWithPattern(
condition="*", condition="*",
action="permit", action="permit",
commands=[ commands=[
@ -68,12 +74,12 @@ VyOS_Ping = BuiltinDirective(
id="__hyperglass_vyos_ping__", id="__hyperglass_vyos_ping__",
name="Ping", name="Ping",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="ping {target} count 5 interface {source4}", command="ping {target} count 5 interface {source4}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="ping {target} count 5 interface {source6}", command="ping {target} count 5 interface {source6}",
@ -87,12 +93,12 @@ VyOS_Traceroute = BuiltinDirective(
id="__hyperglass_vyos_traceroute__", id="__hyperglass_vyos_traceroute__",
name="Traceroute", name="Traceroute",
rules=[ rules=[
Rule( RuleWithIPv4(
condition="0.0.0.0/0", condition="0.0.0.0/0",
action="permit", action="permit",
command="mtr -4 -G 1 -c 1 -w -o SAL -a {source4} {target}", command="mtr -4 -G 1 -c 1 -w -o SAL -a {source4} {target}",
), ),
Rule( RuleWithIPv6(
condition="::/0", condition="::/0",
action="permit", action="permit",
command="mtr -6 -G 1 -c 1 -w -o SAL -a {source6} {target}", command="mtr -6 -G 1 -c 1 -w -o SAL -a {source6} {target}",

View file

@ -48,7 +48,7 @@ class HttpClient(Connection):
key: value.format( key: value.format(
**{ **{
str(v): str(getattr(self.query_data, k, None)) str(v): str(getattr(self.query_data, k, None))
for k, v in self.config.attribute_map.dict().items() for k, v in self.config.attribute_map.model_dump().items()
if v in get_fmt_keys(value) if v in get_fmt_keys(value)
} }
) )
@ -107,10 +107,10 @@ class HttpClient(Connection):
responses += (data,) responses += (data,)
except (httpx.TimeoutException) as error: except httpx.TimeoutException as error:
raise DeviceTimeout(error=error, device=self.device) from error raise DeviceTimeout(error=error, device=self.device) from error
except (httpx.HTTPStatusError) as error: except httpx.HTTPStatusError as error:
if error.response.status_code == 401: if error.response.status_code == 401:
raise AuthError(error=error, device=self.device) from error raise AuthError(error=error, device=self.device) from error
raise RestError(error=error, device=self.device) from error raise RestError(error=error, device=self.device) from error

View file

@ -338,6 +338,7 @@ async def build_frontend( # noqa: C901
} }
build_json = json.dumps(build_data, default=str) build_json = json.dumps(build_data, default=str)
log.debug("UI Build Data:\n{}", build_json)
# Create SHA256 hash from all parameters passed to UI, use as # Create SHA256 hash from all parameters passed to UI, use as
# build identifier. # build identifier.

View file

@ -119,9 +119,9 @@ def on_exit(_: t.Any) -> None:
"""Gunicorn shutdown tasks.""" """Gunicorn shutdown tasks."""
state = use_state() state = use_state()
state.clear() if not Settings.dev_mode:
state.clear()
log.info("Cleared hyperglass state") log.info("Cleared hyperglass state")
unregister_all_plugins() unregister_all_plugins()
@ -195,6 +195,7 @@ def run(_workers: int = None):
start(log_level=log_level, workers=workers) start(log_level=log_level, workers=workers)
except Exception as error: except Exception as error:
log.critical(error)
# Handle app exceptions. # Handle app exceptions.
if not Settings.dev_mode: if not Settings.dev_mode:
state = use_state() state = use_state()

View file

@ -12,6 +12,7 @@ from .response import (
from .cert_import import EncodedRequest from .cert_import import EncodedRequest
__all__ = ( __all__ = (
"Query",
"QueryError", "QueryError",
"InfoResponse", "InfoResponse",
"QueryResponse", "QueryResponse",

View file

@ -1,4 +1,5 @@
"""hyperglass-agent certificate import models.""" """hyperglass-agent certificate import models."""
# Standard Library # Standard Library
from typing import Union from typing import Union

View file

@ -7,7 +7,7 @@ import secrets
from datetime import datetime from datetime import datetime
# Third Party # Third Party
from pydantic import BaseModel, constr, validator from pydantic import BaseModel, constr, field_validator, ConfigDict, Field
# Project # Project
from hyperglass.log import log from hyperglass.log import log
@ -29,17 +29,12 @@ QueryType = constr(strip_whitespace=True, strict=True, min_length=1)
class Query(BaseModel): class Query(BaseModel):
"""Validation model for input query parameters.""" """Validation model for input query parameters."""
model_config = ConfigDict(extra="allow", alias_generator=snake_to_camel, populate_by_name=True)
query_location: QueryLocation # Device `name` field query_location: QueryLocation # Device `name` field
query_target: t.Union[t.List[QueryTarget], QueryTarget] query_target: t.Union[t.List[QueryTarget], QueryTarget]
query_type: QueryType # Directive `id` field query_type: QueryType # Directive `id` field
class Config:
"""Pydantic model configuration."""
extra = "allow"
alias_generator = snake_to_camel
allow_population_by_field_name = True
def __init__(self, **data) -> None: def __init__(self, **data) -> None:
"""Initialize the query with a UTC timestamp at initialization time.""" """Initialize the query with a UTC timestamp at initialization time."""
super().__init__(**data) super().__init__(**data)
@ -96,14 +91,14 @@ class Query(BaseModel):
def dict(self) -> t.Dict[str, t.Union[t.List[str], str]]: def dict(self) -> t.Dict[str, t.Union[t.List[str], str]]:
"""Include only public fields.""" """Include only public fields."""
return super().dict(include={"query_location", "query_target", "query_type"}) return super().model_dump(include={"query_location", "query_target", "query_type"})
@property @property
def device(self) -> Device: def device(self) -> Device:
"""Get this query's device object by query_location.""" """Get this query's device object by query_location."""
return self._state.devices[self.query_location] return self._state.devices[self.query_location]
@validator("query_location") @field_validator("query_location")
def validate_query_location(cls, value): def validate_query_location(cls, value):
"""Ensure query_location is defined.""" """Ensure query_location is defined."""
@ -114,7 +109,7 @@ class Query(BaseModel):
return value return value
@validator("query_type") @field_validator("query_type")
def validate_query_type(cls, value: t.Any): def validate_query_type(cls, value: t.Any):
"""Ensure a requested query type exists.""" """Ensure a requested query type exists."""
devices = use_state("devices") devices = use_state("devices")

View file

@ -4,25 +4,132 @@
import typing as t import typing as t
# Third Party # Third Party
from pydantic import BaseModel, StrictInt, StrictStr, StrictBool, constr, validator from pydantic import (
BaseModel,
StrictInt,
StrictStr,
StrictBool,
field_validator,
Field,
ConfigDict,
)
# Project # Project
from hyperglass.state import use_state from hyperglass.state import use_state
ErrorName = constr(regex=r"(success|warning|error|danger)") ErrorName = t.Literal["success", "warning", "error", "danger"]
ResponseLevel = constr(regex=r"success") ResponseLevel = t.Literal["success"]
ResponseFormat = constr(regex=r"(application\/json|text\/plain)") ResponseFormat = t.Literal[r"text/plain", r"application/json"]
schema_query_output = {
"title": "Output",
"description": "Looking Glass Response",
"example": """
BGP routing table entry for 1.1.1.0/24, version 224184946
BGP Bestpath: deterministic-med
Paths: (12 available, best #1, table default)
Advertised to update-groups:
1 40
13335, (aggregated by 13335 172.68.129.1), (received & used)
192.0.2.1 (metric 51) from 192.0.2.1 (192.0.2.1)
Origin IGP, metric 0, localpref 250, valid, internal
Community: 65000:1 65000:2
""",
}
schema_query_level = {"title": "Level", "description": "Severity"}
schema_query_random = {
"title": "Random",
"description": "Random string to prevent client or intermediate caching.",
"example": "504cbdb47eb8310ca237bf512c3e10b44b0a3d85868c4b64a20037dc1c3ef857",
}
schema_query_cached = {
"title": "Cached",
"description": "`true` if the response is from a previously cached query.",
}
schema_query_runtime = {
"title": "Runtime",
"description": "Time it took to run the query in seconds.",
"example": 6,
}
schema_query_keywords = {
"title": "Keywords",
"description": "Relevant keyword values contained in the `output` field, which can be used for formatting.",
"example": ["1.1.1.0/24", "best #1"],
}
schema_query_timestamp = {
"title": "Timestamp",
"description": "UTC Time at which the backend application received the query.",
"example": "2020-04-18 14:45:37",
}
schema_query_format = {
"title": "Format",
"description": "Response [MIME Type](http://www.iana.org/assignments/media-types/media-types.xhtml). Supported values: `text/plain` and `application/json`.",
"example": "text/plain",
}
schema_query_examples = [
{
"output": """
BGP routing table entry for 1.1.1.0/24, version 224184946
BGP Bestpath: deterministic-med
Paths: (12 available, best #1, table default)
Advertised to update-groups:
1 40
13335, (aggregated by 13335 172.68.129.1), (received & used)
192.0.2.1 (metric 51) from 192.0.2.1 (192.0.2.1)
Origin IGP, metric 0, localpref 250, valid, internal
Community: 65000:1 65000:2
""",
"level": "success",
"keywords": ["1.1.1.0/24", "best #1"],
}
]
schema_query_error_output = {
"title": "Output",
"description": "Error Details",
"example": "192.0.2.1/32 is not allowed.",
}
schema_query_error_level = {"title": "Level", "description": "Error Severity", "example": "danger"}
schema_query_error_keywords = {
"title": "Keywords",
"description": "Relevant keyword values contained in the `output` field, which can be used for formatting.",
"example": ["192.0.2.1/32"],
}
class QueryError(BaseModel): class QueryError(BaseModel):
"""Query response model.""" """Query response model."""
output: StrictStr model_config = ConfigDict(
level: ErrorName = "danger" json_schema_extra={
id: t.Optional[StrictStr] "title": "Query Error",
keywords: t.List[StrictStr] = [] "description": "Response received when there is an error executing the requested query.",
"examples": [
{
"output": "192.0.2.1/32 is not allowed.",
"level": "danger",
"keywords": ["192.0.2.1/32"],
}
],
}
)
@validator("output") output: str = Field(json_schema_extra=schema_query_error_output)
level: ErrorName = Field("danger", json_schema_extra=schema_query_error_level)
# id: t.Optional[StrictStr]
keywords: t.List[StrictStr] = Field([], json_schema_extra=schema_query_error_keywords)
@field_validator("output")
def validate_output(cls: "QueryError", value): def validate_output(cls: "QueryError", value):
"""If no output is specified, use a customizable generic message.""" """If no output is specified, use a customizable generic message."""
if value is None: if value is None:
@ -30,138 +137,45 @@ class QueryError(BaseModel):
return messages.general return messages.general
return value return value
class Config:
"""Pydantic model configuration."""
title = "Query Error"
description = "Response received when there is an error executing the requested query."
fields = {
"output": {
"title": "Output",
"description": "Error Details",
"example": "192.0.2.1/32 is not allowed.",
},
"level": {"title": "Level", "description": "Error Severity", "example": "danger"},
"keywords": {
"title": "Keywords",
"description": "Relevant keyword values contained in the `output` field, which can be used for formatting.",
"example": ["192.0.2.1/32"],
},
}
schema_extra = {
"examples": [
{
"output": "192.0.2.1/32 is not allowed.",
"level": "danger",
"keywords": ["192.0.2.1/32"],
}
]
}
class QueryResponse(BaseModel): class QueryResponse(BaseModel):
"""Query response model.""" """Query response model."""
output: t.Union[t.Dict, StrictStr] model_config = ConfigDict(
level: ResponseLevel = "success" json_schema_extra={
random: StrictStr "title": "Query Response",
cached: StrictBool "description": "Looking glass response",
runtime: StrictInt "examples": schema_query_examples,
keywords: t.List[StrictStr] = []
timestamp: StrictStr
format: ResponseFormat = "text/plain"
class Config:
"""Pydantic model configuration."""
title = "Query Response"
description = "Looking glass response"
fields = {
"level": {"title": "Level", "description": "Severity"},
"cached": {
"title": "Cached",
"description": "`true` if the response is from a previously cached query.",
},
"random": {
"title": "Random",
"description": "Random string to prevent client or intermediate caching.",
"example": "504cbdb47eb8310ca237bf512c3e10b44b0a3d85868c4b64a20037dc1c3ef857",
},
"runtime": {
"title": "Runtime",
"description": "Time it took to run the query in seconds.",
"example": 6,
},
"timestamp": {
"title": "Timestamp",
"description": "UTC Time at which the backend application received the query.",
"example": "2020-04-18 14:45:37",
},
"format": {
"title": "Format",
"description": "Response [MIME Type](http://www.iana.org/assignments/media-types/media-types.xhtml). Supported values: `text/plain` and `application/json`.",
"example": "text/plain",
},
"keywords": {
"title": "Keywords",
"description": "Relevant keyword values contained in the `output` field, which can be used for formatting.",
"example": ["1.1.1.0/24", "best #1"],
},
"output": {
"title": "Output",
"description": "Looking Glass Response",
"example": """
BGP routing table entry for 1.1.1.0/24, version 224184946
BGP Bestpath: deterministic-med
Paths: (12 available, best #1, table default)
Advertised to update-groups:
1 40
13335, (aggregated by 13335 172.68.129.1), (received & used)
192.0.2.1 (metric 51) from 192.0.2.1 (192.0.2.1)
Origin IGP, metric 0, localpref 250, valid, internal
Community: 65000:1 65000:2
""",
},
}
schema_extra = {
"examples": [
{
"output": """
BGP routing table entry for 1.1.1.0/24, version 224184946
BGP Bestpath: deterministic-med
Paths: (12 available, best #1, table default)
Advertised to update-groups:
1 40
13335, (aggregated by 13335 172.68.129.1), (received & used)
192.0.2.1 (metric 51) from 192.0.2.1 (192.0.2.1)
Origin IGP, metric 0, localpref 250, valid, internal
Community: 65000:1 65000:2
""",
"level": "success",
"keywords": ["1.1.1.0/24", "best #1"],
}
]
} }
)
output: t.Union[t.Dict, StrictStr] = Field(json_schema_extra=schema_query_output)
level: ResponseLevel = Field("success", json_schema_extra=schema_query_level)
random: str = Field(json_schema_extra=schema_query_random)
cached: bool = Field(json_schema_extra=schema_query_cached)
runtime: int = Field(json_schema_extra=schema_query_runtime)
keywords: t.List[str] = Field([], json_schema_extra=schema_query_keywords)
timestamp: str = Field(json_schema_extra=schema_query_timestamp)
format: ResponseFormat = Field("text/plain", json_schema_extra=schema_query_format)
class RoutersResponse(BaseModel): class RoutersResponse(BaseModel):
"""Response model for /api/devices list items.""" """Response model for /api/devices list items."""
model_config = ConfigDict(
json_schema_extra={
"title": "Device",
"description": "Device attributes",
"examples": [
{"id": "nyc_router_1", "name": "NYC Router 1", "group": "New York City, NY"}
],
}
)
id: StrictStr id: StrictStr
name: StrictStr name: StrictStr
group: t.Union[StrictStr, None] group: t.Union[StrictStr, None]
class Config:
"""Pydantic model configuration."""
title = "Device"
description = "Device attributes"
schema_extra = {
"examples": [
{"id": "nyc_router_1", "name": "NYC Router 1", "group": "New York City, NY"}
]
}
class CommunityResponse(BaseModel): class CommunityResponse(BaseModel):
"""Response model for /api/communities.""" """Response model for /api/communities."""
@ -174,36 +188,26 @@ class CommunityResponse(BaseModel):
class SupportedQueryResponse(BaseModel): class SupportedQueryResponse(BaseModel):
"""Response model for /api/queries list items.""" """Response model for /api/queries list items."""
model_config = ConfigDict(
json_schema_extra={
"title": "Query Type",
"description": "If enabled is `true`, the `name` field may be used to specify the query type.",
"examples": [{"name": "bgp_route", "display_name": "BGP Route", "enable": True}],
}
)
name: StrictStr name: StrictStr
display_name: StrictStr display_name: StrictStr
enable: StrictBool enable: StrictBool
class Config:
"""Pydantic model configuration."""
title = "Query Type"
description = (
"If enabled is `true`, the `name` field may be used to specify the query type."
)
schema_extra = {
"examples": [{"name": "bgp_route", "display_name": "BGP Route", "enable": True}]
}
class InfoResponse(BaseModel): class InfoResponse(BaseModel):
"""Response model for /api/info endpoint.""" """Response model for /api/info endpoint."""
name: StrictStr model_config = ConfigDict(
organization: StrictStr json_schema_extra={
primary_asn: StrictInt "title": "System Information",
version: StrictStr "description": "General information about this looking glass.",
class Config:
"""Pydantic model configuration."""
title = "System Information"
description = "General information about this looking glass."
schema_extra = {
"examples": [ "examples": [
{ {
"name": "hyperglass", "name": "hyperglass",
@ -211,5 +215,11 @@ class InfoResponse(BaseModel):
"primary_asn": 65000, "primary_asn": 65000,
"version": "hyperglass 1.0.0-beta.52", "version": "hyperglass 1.0.0-beta.52",
} }
] ],
} }
)
name: StrictStr
organization: StrictStr
primary_asn: StrictInt
version: StrictStr

View file

@ -4,11 +4,11 @@
# flake8: noqa # flake8: noqa
import math import math
import secrets import secrets
from typing import List, Union, Optional import typing as t
from datetime import datetime from datetime import datetime
# Third Party # Third Party
from pydantic import BaseModel, StrictInt, StrictStr, StrictFloat, constr, validator from pydantic import BaseModel, field_validator, ConfigDict, Field
"""Patterns: """Patterns:
GET /.well-known/looking-glass/v1/ping/2001:DB8::35?protocol=2,1 GET /.well-known/looking-glass/v1/ping/2001:DB8::35?protocol=2,1
@ -22,55 +22,49 @@ GET /.well-known/looking-glass/v1/routers/1
GET /.well-known/looking-glass/v1/cmd GET /.well-known/looking-glass/v1/cmd
""" """
QueryFormat = t.Literal[r"text/plain", r"application/json"]
class _HyperglassQuery(BaseModel): class _HyperglassQuery(BaseModel):
class Config: model_config = ConfigDict(validate_assignment=True, validate_default=True)
validate_all = True
validate_assignment = True
class BaseQuery(_HyperglassQuery): class BaseQuery(_HyperglassQuery):
protocol: StrictStr = "1,1" protocol: str = "1,1"
router: StrictStr router: str
routerindex: StrictInt routerindex: int
random: StrictStr = secrets.token_urlsafe(16) random: str = secrets.token_urlsafe(16)
vrf: Optional[StrictStr] runtime: int = 30
runtime: StrictInt = 30 query_format: QueryFormat = Field("text/plain", alias="format")
query_format: constr(regex=r"(text\/plain|application\/json)") = "text/plain"
@validator("runtime") @field_validator("runtime")
def validate_runtime(cls, value): def validate_runtime(cls, value):
if isinstance(value, float) and math.modf(value)[0] == 0: if isinstance(value, float) and math.modf(value)[0] == 0:
value = math.ceil(value) value = math.ceil(value)
return value return value
class Config:
fields = {"query_format": "format"}
class BaseData(_HyperglassQuery): class BaseData(_HyperglassQuery):
router: StrictStr model_config = ConfigDict(extra="allow")
performed_at: datetime
runtime: Union[StrictFloat, StrictInt]
output: List[StrictStr]
data_format: StrictStr
@validator("runtime") router: str
performed_at: datetime
runtime: t.Union[float, int]
output: t.List[str]
data_format: str = Field(alias="format")
@field_validator("runtime")
def validate_runtime(cls, value): def validate_runtime(cls, value):
if isinstance(value, float) and math.modf(value)[0] == 0: if isinstance(value, float) and math.modf(value)[0] == 0:
value = math.ceil(value) value = math.ceil(value)
return value return value
class Config:
fields = {"data_format": "format"}
extra = "allow"
class QueryError(_HyperglassQuery): class QueryError(_HyperglassQuery):
status: constr(regex=r"error") status: t.Literal["error"]
message: StrictStr message: str
class QueryResponse(_HyperglassQuery): class QueryResponse(_HyperglassQuery):
status: constr(regex=r"success|fail") status: t.Literal["success", "fail"]
data: BaseData data: BaseData

View file

@ -1,26 +1,18 @@
"""Custom validation types.""" """Custom validation types."""
import typing as t
from pydantic import AfterValidator
# Project # Project
from hyperglass.constants import SUPPORTED_QUERY_TYPES from hyperglass.constants import SUPPORTED_QUERY_TYPES
class SupportedQuery(str): def validate_query_type(value: str) -> str:
"""Query Type Validation Model.""" """Ensure query type is supported by hyperglass."""
if value not in SUPPORTED_QUERY_TYPES:
raise ValueError("'{}' is not a supported query type".format(value))
return value
@classmethod
def __get_validators__(cls):
"""Pydantic custom type method."""
yield cls.validate
@classmethod SupportedQuery = t.Annotated[str, AfterValidator(validate_query_type)]
def validate(cls, value):
"""Ensure query type is supported by hyperglass."""
if not isinstance(value, str):
raise TypeError("query_type must be a string")
if value not in SUPPORTED_QUERY_TYPES:
raise ValueError(f"'{value}' is not a supported query type")
return value
def __repr__(self):
"""Stringify custom field representation."""
return f"SupportedQuery({super().__repr__()})"

View file

@ -1,10 +1,10 @@
"""Validation model for Redis cache config.""" """Validation model for Redis cache config."""
# Standard Library # Standard Library
from typing import Union, Optional import typing as t
# Third Party # Third Party
from pydantic import SecretStr, StrictInt, StrictStr, StrictBool, IPvAnyAddress from pydantic import SecretStr, IPvAnyAddress
# Local # Local
from ..main import HyperglassModel from ..main import HyperglassModel
@ -13,30 +13,14 @@ from ..main import HyperglassModel
class CachePublic(HyperglassModel): class CachePublic(HyperglassModel):
"""Public cache parameters.""" """Public cache parameters."""
timeout: StrictInt = 120 timeout: int = 120
show_text: StrictBool = True show_text: bool = True
class Cache(CachePublic): class Cache(CachePublic):
"""Validation model for params.cache.""" """Validation model for params.cache."""
host: Union[IPvAnyAddress, StrictStr] = "localhost" host: t.Union[IPvAnyAddress, str] = "localhost"
port: StrictInt = 6379 port: int = 6379
database: StrictInt = 1 database: int = 1
password: Optional[SecretStr] password: t.Optional[SecretStr] = None
class Config:
"""Pydantic model configuration."""
title = "Cache"
description = "Redis server & cache timeout configuration."
fields = {
"host": {"description": "Redis server IP address or hostname."},
"port": {"description": "Redis server TCP port."},
"database": {"description": "Redis server database ID."},
"password": {"description": "Redis authentication password."},
"timeout": {
"description": "Time in seconds query output will be kept in the Redis cache."
},
"show_test": {description: "Show the cache text in the hyperglass UI."},
}

View file

@ -1,34 +1,35 @@
"""Validate credential configuration variables.""" """Validate credential configuration variables."""
# Standard Library # Standard Library
from typing import Optional import typing as t
# Third Party # Third Party
from pydantic import FilePath, SecretStr, StrictStr, constr, root_validator from pydantic import FilePath, SecretStr, model_validator
# Local # Local
from ..main import HyperglassModel from ..main import HyperglassModel
Methods = constr(regex=r"(password|unencrypted_key|encrypted_key)") AuthMethod = t.Literal["password", "unencrypted_key", "encrypted_key"]
class Credential(HyperglassModel, extra="allow"): class Credential(HyperglassModel, extra="allow"):
"""Model for per-credential config in devices.yaml.""" """Model for per-credential config in devices.yaml."""
username: StrictStr username: str
password: Optional[SecretStr] password: t.Optional[SecretStr] = None
key: Optional[FilePath] key: t.Optional[FilePath] = None
_method: t.Optional[AuthMethod] = None
@root_validator @model_validator(mode="after")
def validate_credential(cls, values): def validate_credential(cls, data: "Credential"):
"""Ensure either a password or an SSH key is set.""" """Ensure either a password or an SSH key is set."""
if values.get("key") is None and values.get("password") is None: if data.key is None and data.password is None:
raise ValueError( raise ValueError(
"Either a password or an SSH key must be specified for user '{}'".format( "Either a password or an SSH key must be specified for user '{}'".format(
values["username"] data.username
) )
) )
return values return data
def __init__(self, **kwargs): def __init__(self, **kwargs):
"""Set private attribute _method based on validated model.""" """Set private attribute _method based on validated model."""

View file

@ -7,7 +7,7 @@ from pathlib import Path
from ipaddress import IPv4Address, IPv6Address from ipaddress import IPv4Address, IPv6Address
# Third Party # Third Party
from pydantic import FilePath, StrictInt, StrictStr, StrictBool, validator from pydantic import FilePath, field_validator, ValidationInfo
from netmiko.ssh_dispatcher import CLASS_MAPPER # type: ignore from netmiko.ssh_dispatcher import CLASS_MAPPER # type: ignore
# Project # Project
@ -38,27 +38,27 @@ ALL_DEVICE_TYPES = {*DRIVER_MAP.keys(), *CLASS_MAPPER.keys()}
class DirectiveOptions(HyperglassModel, extra="ignore"): class DirectiveOptions(HyperglassModel, extra="ignore"):
"""Per-device directive options.""" """Per-device directive options."""
builtins: t.Union[StrictBool, t.List[StrictStr]] = True builtins: t.Union[bool, t.List[str]] = True
class Device(HyperglassModelWithId, extra="allow"): class Device(HyperglassModelWithId, extra="allow"):
"""Validation model for per-router config in devices.yaml.""" """Validation model for per-router config in devices.yaml."""
id: StrictStr id: str
name: StrictStr name: str
description: t.Optional[StrictStr] description: t.Optional[str] = None
avatar: t.Optional[FilePath] avatar: t.Optional[FilePath] = None
address: t.Union[IPv4Address, IPv6Address, StrictStr] address: t.Union[IPv4Address, IPv6Address, str]
group: t.Optional[StrictStr] group: t.Optional[str] = None
credential: Credential credential: Credential
proxy: t.Optional[Proxy] proxy: t.Optional[Proxy] = None
display_name: t.Optional[StrictStr] display_name: t.Optional[str] = None
port: StrictInt = 22 port: int = 22
http: HttpConfiguration = HttpConfiguration() http: HttpConfiguration = HttpConfiguration()
platform: StrictStr platform: str
structured_output: t.Optional[StrictBool] structured_output: t.Optional[bool] = None
directives: Directives = Directives() directives: Directives = Directives()
driver: t.Optional[SupportedDriver] driver: t.Optional[SupportedDriver] = None
driver_config: t.Dict[str, t.Any] = {} driver_config: t.Dict[str, t.Any] = {}
attrs: t.Dict[str, str] = {} attrs: t.Dict[str, str] = {}
@ -162,7 +162,7 @@ class Device(HyperglassModelWithId, extra="allow"):
a=key, a=key,
) )
@validator("address") @field_validator("address")
def validate_address( def validate_address(
cls, value: t.Union[IPv4Address, IPv6Address, str], values: t.Dict[str, t.Any] cls, value: t.Union[IPv4Address, IPv6Address, str], values: t.Dict[str, t.Any]
) -> t.Union[IPv4Address, IPv6Address, str]: ) -> t.Union[IPv4Address, IPv6Address, str]:
@ -177,7 +177,7 @@ class Device(HyperglassModelWithId, extra="allow"):
) )
return value return value
@validator("avatar") @field_validator("avatar")
def validate_avatar( def validate_avatar(
cls, value: t.Union[FilePath, None], values: t.Dict[str, t.Any] cls, value: t.Union[FilePath, None], values: t.Dict[str, t.Any]
) -> t.Union[FilePath, None]: ) -> t.Union[FilePath, None]:
@ -199,7 +199,7 @@ class Device(HyperglassModelWithId, extra="allow"):
src.save(target) src.save(target)
return value return value
@validator("platform", pre=True, always=True) @field_validator("platform", mode="before")
def validate_platform(cls: "Device", value: t.Any, values: t.Dict[str, t.Any]) -> str: def validate_platform(cls: "Device", value: t.Any, values: t.Dict[str, t.Any]) -> str:
"""Validate & rewrite device platform, set default `directives`.""" """Validate & rewrite device platform, set default `directives`."""
@ -220,35 +220,35 @@ class Device(HyperglassModelWithId, extra="allow"):
return value return value
@validator("structured_output", pre=True, always=True) @field_validator("structured_output", mode="before")
def validate_structured_output(cls, value: bool, values: t.Dict[str, t.Any]) -> bool: def validate_structured_output(cls, value: bool, info: ValidationInfo) -> bool:
"""Validate structured output is supported on the device & set a default.""" """Validate structured output is supported on the device & set a default."""
if value is True: if value is True:
if values["platform"] not in SUPPORTED_STRUCTURED_OUTPUT: if info.data.get("platform") not in SUPPORTED_STRUCTURED_OUTPUT:
raise ConfigError( raise ConfigError(
"The 'structured_output' field is set to 'true' on device '{d}' with " "The 'structured_output' field is set to 'true' on device '{}' with "
+ "platform '{p}', which does not support structured output", + "platform '{}', which does not support structured output",
d=values["name"], info.data.get("name"),
p=values["platform"], info.data.get("platform"),
) )
return value return value
if value is None and values["platform"] in SUPPORTED_STRUCTURED_OUTPUT: if value is None and info.data.get("platform") in SUPPORTED_STRUCTURED_OUTPUT:
value = True value = True
else: else:
value = False value = False
return value return value
@validator("directives", pre=True, always=True) @field_validator("directives", mode="before")
def validate_directives( def validate_directives(
cls: "Device", value: t.Optional[t.List[str]], values: t.Dict[str, t.Any] cls: "Device", value: t.Optional[t.List[str]], info: ValidationInfo
) -> "Directives": ) -> "Directives":
"""Associate directive IDs to loaded directive objects.""" """Associate directive IDs to loaded directive objects."""
directives = use_state("directives") directives = use_state("directives")
directive_ids = value or [] directive_ids = value or []
structured_output = values.get("structured_output", False) structured_output = info.data.get("structured_output", False)
platform = values.get("platform") platform = info.data.get("platform")
# Directive options # Directive options
directive_options = DirectiveOptions( directive_options = DirectiveOptions(
@ -280,10 +280,10 @@ class Device(HyperglassModelWithId, extra="allow"):
return device_directives return device_directives
@validator("driver") @field_validator("driver")
def validate_driver(cls: "Device", value: t.Optional[str], values: t.Dict[str, t.Any]) -> str: def validate_driver(cls: "Device", value: t.Optional[str], info: ValidationInfo) -> str:
"""Set the correct driver and override if supported.""" """Set the correct driver and override if supported."""
return get_driver(values["platform"], value) return get_driver(info.data.get("platform"), value)
class Devices(MultiModel, model=Device, unique_by="id"): class Devices(MultiModel, model=Device, unique_by="id"):
@ -305,9 +305,9 @@ class Devices(MultiModel, model=Device, unique_by="id"):
return True return True
return False return False
def directive_plugins(self: "Devices") -> t.Dict[Path, t.Tuple[StrictStr]]: def directive_plugins(self: "Devices") -> t.Dict[Path, t.Tuple[str]]:
"""Get a mapping of plugin paths to associated directive IDs.""" """Get a mapping of plugin paths to associated directive IDs."""
result: t.Dict[Path, t.Set[StrictStr]] = {} result: t.Dict[Path, t.Set[str]] = {}
# Unique set of all directives. # Unique set of all directives.
directives = {directive for device in self for directive in device.directives} directives = {directive for device in self for directive in device.directives}
# Unique set of all plugin file names. # Unique set of all plugin file names.

View file

@ -1,28 +1,31 @@
"""Configuration for API docs feature.""" """Configuration for API docs feature."""
import typing as t
# Third Party # Third Party
from pydantic import Field, HttpUrl, StrictStr, StrictBool, constr from pydantic import Field, HttpUrl
# Local # Local
from ..main import HyperglassModel from ..main import HyperglassModel
from ..fields import AnyUri from ..fields import AnyUri
DocsMode = constr(regex=r"(swagger|redoc)") DocsMode = t.Literal["swagger", "redoc"]
class EndpointConfig(HyperglassModel): class EndpointConfig(HyperglassModel):
"""Validation model for per API endpoint documentation.""" """Validation model for per API endpoint documentation."""
title: StrictStr = Field( title: str = Field(
..., ...,
title="Endpoint Title", title="Endpoint Title",
description="Displayed as the header text above the API endpoint section.", description="Displayed as the header text above the API endpoint section.",
) )
description: StrictStr = Field( description: str = Field(
..., ...,
title="Endpoint Description", title="Endpoint Description",
description="Displayed inside each API endpoint section.", description="Displayed inside each API endpoint section.",
) )
summary: StrictStr = Field( summary: str = Field(
..., ...,
title="Endpoint Summary", title="Endpoint Summary",
description="Displayed beside the API endpoint URI.", description="Displayed beside the API endpoint URI.",
@ -32,9 +35,7 @@ class EndpointConfig(HyperglassModel):
class Docs(HyperglassModel): class Docs(HyperglassModel):
"""Validation model for params.docs.""" """Validation model for params.docs."""
enable: StrictBool = Field( enable: bool = Field(True, title="Enable", description="Enable or disable API documentation.")
True, title="Enable", description="Enable or disable API documentation."
)
mode: DocsMode = Field( mode: DocsMode = Field(
"redoc", "redoc",
title="Docs Mode", title="Docs Mode",
@ -50,12 +51,12 @@ class Docs(HyperglassModel):
title="URI", title="URI",
description="HTTP URI/path where API documentation can be accessed.", description="HTTP URI/path where API documentation can be accessed.",
) )
title: StrictStr = Field( title: str = Field(
"{site_title} API Documentation", "{site_title} API Documentation",
title="Title", title="Title",
description="API documentation title. `{site_title}` may be used to display the `site_title` parameter.", description="API documentation title. `{site_title}` may be used to display the `site_title` parameter.",
) )
description: StrictStr = Field( description: str = Field(
"", "",
title="Description", title="Description",
description="API documentation description appearing below the title.", description="API documentation description appearing below the title.",
@ -80,27 +81,3 @@ class Docs(HyperglassModel):
description="General information about this looking glass.", description="General information about this looking glass.",
summary="System Information", summary="System Information",
) )
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.",
},
"communities": {
"title": "BGP Communities API Endpoint",
"description": "`/api/communities` API documentation options.",
},
}

View file

@ -8,9 +8,6 @@ import httpx
from pydantic import ( from pydantic import (
FilePath, FilePath,
SecretStr, SecretStr,
StrictInt,
StrictStr,
StrictBool,
PrivateAttr, PrivateAttr,
IPvAnyAddress, IPvAnyAddress,
) )
@ -39,23 +36,23 @@ Scheme = t.Literal["http", "https"]
class AttributeMapConfig(HyperglassModel): class AttributeMapConfig(HyperglassModel):
"""Allow the user to 'rewrite' hyperglass field names to their own values.""" """Allow the user to 'rewrite' hyperglass field names to their own values."""
query_target: t.Optional[StrictStr] query_target: t.Optional[str] = None
query_type: t.Optional[StrictStr] query_type: t.Optional[str] = None
query_location: t.Optional[StrictStr] query_location: t.Optional[str] = None
class AttributeMap(HyperglassModel): class AttributeMap(HyperglassModel):
"""Merged implementation of attribute map configuration.""" """Merged implementation of attribute map configuration."""
query_target: StrictStr query_target: str
query_type: StrictStr query_type: str
query_location: StrictStr query_location: str
class HttpBasicAuth(HyperglassModel): class HttpBasicAuth(HyperglassModel):
"""Configuration model for HTTP basic authentication.""" """Configuration model for HTTP basic authentication."""
username: StrictStr username: str
password: SecretStr password: SecretStr
@ -63,21 +60,21 @@ class HttpConfiguration(HyperglassModel):
"""HTTP client configuration.""" """HTTP client configuration."""
_attribute_map: AttributeMap = PrivateAttr() _attribute_map: AttributeMap = PrivateAttr()
path: StrictStr = "/" path: str = "/"
method: HttpMethod = "GET" method: HttpMethod = "GET"
scheme: Scheme = "https" scheme: Scheme = "https"
query: t.Optional[t.Union[t.Literal[False], t.Dict[str, Primitives]]] query: t.Optional[t.Union[t.Literal[False], t.Dict[str, Primitives]]] = None
verify_ssl: StrictBool = True verify_ssl: bool = True
ssl_ca: t.Optional[FilePath] ssl_ca: t.Optional[FilePath] = None
ssl_client: t.Optional[FilePath] ssl_client: t.Optional[FilePath] = None
source: t.Optional[IPvAnyAddress] source: t.Optional[IPvAnyAddress] = None
timeout: IntFloat = 5 timeout: IntFloat = 5
headers: t.Dict[str, str] = {} headers: t.Dict[str, str] = {}
follow_redirects: StrictBool = False follow_redirects: bool = False
basic_auth: t.Optional[HttpBasicAuth] basic_auth: t.Optional[HttpBasicAuth] = None
attribute_map: AttributeMapConfig = AttributeMapConfig() attribute_map: AttributeMapConfig = AttributeMapConfig()
body_format: BodyFormat = "json" body_format: BodyFormat = "json"
retries: StrictInt = 0 retries: int = 0
def __init__(self, **data: t.Any) -> None: def __init__(self, **data: t.Any) -> None:
"""Create HTTP Client Configuration Definition.""" """Create HTTP Client Configuration Definition."""

View file

@ -1,20 +1,16 @@
"""Validate logging configuration.""" """Validate logging configuration."""
# Standard Library # Standard Library
from typing import Dict, Union, Optional import typing as t
from pathlib import Path from pathlib import Path
# Third Party # Third Party
from pydantic import ( from pydantic import (
ByteSize, ByteSize,
SecretStr, SecretStr,
StrictInt,
StrictStr,
AnyHttpUrl, AnyHttpUrl,
StrictBool,
StrictFloat,
DirectoryPath, DirectoryPath,
validator, field_validator,
) )
# Project # Project
@ -28,18 +24,18 @@ from ..fields import LogFormat, HttpAuthMode, HttpProvider
class Syslog(HyperglassModel): class Syslog(HyperglassModel):
"""Validation model for syslog configuration.""" """Validation model for syslog configuration."""
enable: StrictBool = True enable: bool = True
host: StrictStr host: str
port: StrictInt = 514 port: int = 514
class HttpAuth(HyperglassModel): class HttpAuth(HyperglassModel):
"""HTTP hook authentication parameters.""" """HTTP hook authentication parameters."""
mode: HttpAuthMode = "basic" mode: HttpAuthMode = "basic"
username: Optional[StrictStr] username: t.Optional[str] = None
password: SecretStr password: SecretStr
header: StrictStr = "x-api-key" header: str = "x-api-key"
def api_key(self): def api_key(self):
"""Represent authentication as an API key header.""" """Represent authentication as an API key header."""
@ -53,16 +49,16 @@ class HttpAuth(HyperglassModel):
class Http(HyperglassModel, extra="allow"): class Http(HyperglassModel, extra="allow"):
"""HTTP logging parameters.""" """HTTP logging parameters."""
enable: StrictBool = True enable: bool = True
provider: HttpProvider = "generic" provider: HttpProvider = "generic"
host: AnyHttpUrl host: AnyHttpUrl
authentication: Optional[HttpAuth] authentication: t.Optional[HttpAuth] = None
headers: Dict[StrictStr, Union[StrictStr, StrictInt, StrictBool, None]] = {} headers: t.Dict[str, t.Union[str, int, bool, None]] = {}
params: Dict[StrictStr, Union[StrictStr, StrictInt, StrictBool, None]] = {} params: t.Dict[str, t.Union[str, int, bool, None]] = {}
verify_ssl: StrictBool = True verify_ssl: bool = True
timeout: Union[StrictFloat, StrictInt] = 5.0 timeout: t.Union[float, int] = 5.0
@validator("headers", "params") @field_validator("headers", "params")
def stringify_headers_params(cls, value): def stringify_headers_params(cls, value):
"""Ensure headers and URL parameters are strings.""" """Ensure headers and URL parameters are strings."""
for k, v in value.items(): for k, v in value.items():
@ -94,5 +90,5 @@ class Logging(HyperglassModel):
directory: DirectoryPath = Path("/tmp") # noqa: S108 directory: DirectoryPath = Path("/tmp") # noqa: S108
format: LogFormat = "text" format: LogFormat = "text"
max_size: ByteSize = "50MB" max_size: ByteSize = "50MB"
syslog: Optional[Syslog] syslog: t.Optional[Syslog] = None
http: Optional[Http] http: t.Optional[Http] = None

View file

@ -1,7 +1,7 @@
"""Validate error message configuration variables.""" """Validate error message configuration variables."""
# Third Party # Third Party
from pydantic import Field, StrictStr from pydantic import Field, ConfigDict
# Local # Local
from ..main import HyperglassModel from ..main import HyperglassModel
@ -10,66 +10,66 @@ from ..main import HyperglassModel
class Messages(HyperglassModel): class Messages(HyperglassModel):
"""Validation model for params.messages.""" """Validation model for params.messages."""
no_input: StrictStr = Field( no_input: str = Field(
"{field} must be specified.", "{field} must be specified.",
title="No Input", 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.", 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.",
) )
target_not_allowed: StrictStr = Field( target_not_allowed: str = Field(
"{target} is not allowed.", "{target} is not allowed.",
title="Target Not Allowed", title="Target Not Allowed",
description="Displayed when a query target is implicitly denied by a configured rule. `{target}` will be used to display the denied query target.", description="Displayed when a query target is implicitly denied by a configured rule. `{target}` will be used to display the denied query target.",
) )
feature_not_enabled: StrictStr = Field( feature_not_enabled: str = Field(
"{feature} is not enabled.", "{feature} is not enabled.",
title="Feature Not Enabled", 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}` may be used to display the disabled feature.", 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}` may be used to display the disabled feature.",
) )
invalid_input: StrictStr = Field( invalid_input: str = Field(
"{target} is not valid.", "{target} is not valid.",
title="Invalid Input", title="Invalid Input",
description="Displayed when a query target's value is invalid in relation to the corresponding query type. `{target}` may be used to display the invalid target.", description="Displayed when a query target's value is invalid in relation to the corresponding query type. `{target}` may be used to display the invalid target.",
) )
invalid_query: StrictStr = Field( invalid_query: str = Field(
"{target} is not a valid {query_type} target.", "{target} is not a valid {query_type} target.",
title="Invalid Query", title="Invalid Query",
description="Displayed when a query target's value is invalid in relation to the corresponding query type. `{target}` and `{query_type}` may be used to display the invalid target and corresponding query type.", description="Displayed when a query target's value is invalid in relation to the corresponding query type. `{target}` and `{query_type}` may be used to display the invalid target and corresponding query type.",
) )
invalid_field: StrictStr = Field( invalid_field: str = Field(
"{input} is an invalid {field}.", "{input} is an invalid {field}.",
title="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.", 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( general: str = Field(
"Something went wrong.", "Something went wrong.",
title="General Error", 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.", 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.",
) )
not_found: StrictStr = Field( not_found: str = Field(
"{type} '{name}' not found.", "{type} '{name}' not found.",
title="Not Found", title="Not Found",
description="Displayed when an object property does not exist in the configuration. `{type}` corresponds to a user-friendly name of the object type (for example, 'Device'), `{name}` corresponds to the object name that was not found.", description="Displayed when an object property does not exist in the configuration. `{type}` corresponds to a user-friendly name of the object type (for example, 'Device'), `{name}` corresponds to the object name that was not found.",
) )
request_timeout: StrictStr = Field( request_timeout: str = Field(
"Request timed out.", "Request timed out.",
title="Request Timeout", title="Request Timeout",
description="Displayed when the [request_timeout](/fixme) time expires.", description="Displayed when the [request_timeout](/fixme) time expires.",
) )
connection_error: StrictStr = Field( connection_error: str = Field(
"Error connecting to {device_name}: {error}", "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.", 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: str = Field(
"Authentication error occurred.", "Authentication error occurred.",
title="Authentication Error", title="Authentication Error",
description="Displayed when hyperglass is unable to authenticate to a configured device. Usually, this indicates a configuration 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: str = Field(
"No response.", "No response.",
title="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.", 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.",
) )
no_output: StrictStr = Field( no_output: str = Field(
"The query completed, but no matching results were found.", "The query completed, but no matching results were found.",
title="No Output", title="No Output",
description="Displayed when hyperglass can connect to a device and execute a query, but the response is empty.", description="Displayed when hyperglass can connect to a device and execute a query, but the response is empty.",
@ -77,18 +77,17 @@ class Messages(HyperglassModel):
def has(self, attr: str) -> bool: def has(self, attr: str) -> bool:
"""Determine if message type exists in Messages model.""" """Determine if message type exists in Messages model."""
return attr in self.dict().keys() return attr in self.model_dump().keys()
def __getitem__(self, attr: str) -> StrictStr: def __getitem__(self, attr: str) -> str:
"""Make messages subscriptable.""" """Make messages subscriptable."""
if not self.has(attr): if not self.has(attr):
raise KeyError(f"'{attr}' does not exist on Messages model") raise KeyError(f"'{attr}' does not exist on Messages model")
return getattr(self, attr) return getattr(self, attr)
class Config: model_config = ConfigDict(
"""Pydantic model configuration.""" title="Messages",
description="Customize almost all user-facing UI & API messages.",
title = "Messages" json_schema_extra={"level": 2},
description = "Customize almost all user-facing UI & API messages." )
schema_extra = {"level": 2}

View file

@ -4,7 +4,7 @@
from pathlib import Path from pathlib import Path
# Third Party # Third Party
from pydantic import FilePath, validator from pydantic import FilePath, field_validator
# Local # Local
from ..main import HyperglassModel from ..main import HyperglassModel
@ -17,7 +17,7 @@ class OpenGraph(HyperglassModel):
image: FilePath = DEFAULT_IMAGES / "hyperglass-opengraph.jpg" image: FilePath = DEFAULT_IMAGES / "hyperglass-opengraph.jpg"
@validator("image") @field_validator("image")
def validate_opengraph(cls, value): def validate_opengraph(cls, value):
"""Ensure the opengraph image is a supported format.""" """Ensure the opengraph image is a supported format."""
supported_extensions = (".jpg", ".jpeg", ".png") supported_extensions = (".jpg", ".jpeg", ".png")

View file

@ -1,11 +1,11 @@
"""Configuration validation entry point.""" """Configuration validation entry point."""
# Standard Library # Standard Library
from typing import Any, Dict, List, Tuple, Union, Literal import typing as t
from pathlib import Path from pathlib import Path
# Third Party # Third Party
from pydantic import Field, StrictInt, StrictStr, StrictBool, validator from pydantic import Field, field_validator, ValidationInfo, ConfigDict
# Project # Project
from hyperglass.settings import Settings from hyperglass.settings import Settings
@ -19,33 +19,33 @@ from .logging import Logging
from .messages import Messages from .messages import Messages
from .structured import Structured from .structured import Structured
Localhost = Literal["localhost"] Localhost = t.Literal["localhost"]
class ParamsPublic(HyperglassModel): class ParamsPublic(HyperglassModel):
"""Public configuration parameters.""" """Public configuration parameters."""
request_timeout: StrictInt = Field( request_timeout: int = Field(
90, 90,
title="Request Timeout", 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.", 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( primary_asn: t.Union[int, str] = Field(
"65001", "65001",
title="Primary ASN", title="Primary ASN",
description="Your network's primary ASN. This field is used to set some useful defaults such as the subtitle and PeeringDB URL.", description="Your network's primary ASN. This field is used to set some useful defaults such as the subtitle and PeeringDB URL.",
) )
org_name: StrictStr = Field( org_name: str = Field(
"Beloved Hyperglass User", "Beloved Hyperglass User",
title="Organization Name", title="Organization Name",
description="Your organization's name. This field is used in the UI & API documentation to set fields such as `<meta/>` HTML tags for SEO and the terms & conditions footer component.", description="Your organization's name. This field is used in the UI & API documentation to set fields such as `<meta/>` HTML tags for SEO and the terms & conditions footer component.",
) )
site_title: StrictStr = Field( site_title: str = Field(
"hyperglass", "hyperglass",
title="Site Title", title="Site Title",
description="The name of your hyperglass site. This field is used in the UI & API documentation to set fields such as the `<title/>` HTML tag, and the terms & conditions footer component.", description="The name of your hyperglass site. This field is used in the UI & API documentation to set fields such as the `<title/>` HTML tag, and the terms & conditions footer component.",
) )
site_description: StrictStr = Field( site_description: str = Field(
"{org_name} Network Looking Glass", "{org_name} Network Looking Glass",
title="Site Description", title="Site Description",
description='A short description of your hyperglass site. This field is used in th UI & API documentation to set the `<meta name="description"/>` tag. `{org_name}` may be used to insert the value of the `org_name` field.', description='A short description of your hyperglass site. This field is used in th UI & API documentation to set the `<meta name="description"/>` tag. `{org_name}` may be used to insert the value of the `org_name` field.',
@ -55,19 +55,21 @@ class ParamsPublic(HyperglassModel):
class Params(ParamsPublic, HyperglassModel): class Params(ParamsPublic, HyperglassModel):
"""Validation model for all configuration variables.""" """Validation model for all configuration variables."""
model_config = ConfigDict(json_schema_extra={"level": 1})
# Top Level Params # Top Level Params
fake_output: StrictBool = Field( fake_output: bool = Field(
False, False,
title="Fake Output", title="Fake Output",
description="If enabled, the hyperglass backend will return static fake output for development/testing purposes.", description="If enabled, the hyperglass backend will return static fake output for development/testing purposes.",
) )
cors_origins: List[StrictStr] = Field( cors_origins: t.List[str] = Field(
[], [],
title="Cross-Origin Resource Sharing", title="Cross-Origin Resource Sharing",
description="Allowed CORS hosts. By default, no CORS hosts are allowed.", description="Allowed CORS hosts. By default, no CORS hosts are allowed.",
) )
plugins: List[StrictStr] = [] plugins: t.List[str] = []
# Sub Level Params # Sub Level Params
cache: Cache = Cache() cache: Cache = Cache()
@ -77,26 +79,21 @@ class Params(ParamsPublic, HyperglassModel):
structured: Structured = Structured() structured: Structured = Structured()
web: Web = Web() web: Web = Web()
class Config: def __init__(self, **kw: t.Any) -> None:
"""Pydantic model configuration."""
schema_extra = {"level": 1}
def __init__(self, **kw: Any) -> None:
return super().__init__(**self.convert_paths(kw)) return super().__init__(**self.convert_paths(kw))
@validator("site_description") @field_validator("site_description")
def validate_site_description(cls: "Params", value: str, values: Dict[str, Any]) -> str: def validate_site_description(cls: "Params", value: str, info: ValidationInfo) -> str:
"""Format the site descripion with the org_name field.""" """Format the site description with the org_name field."""
return value.format(org_name=values["org_name"]) return value.format(org_name=info.data.get("org_name"))
@validator("primary_asn") @field_validator("primary_asn")
def validate_primary_asn(cls: "Params", value: Union[int, str]) -> str: def validate_primary_asn(cls: "Params", value: t.Union[int, str]) -> str:
"""Stringify primary_asn if passed as an integer.""" """Stringify primary_asn if passed as an integer."""
return str(value) return str(value)
@validator("plugins") @field_validator("plugins")
def validate_plugins(cls: "Params", value: List[str]) -> List[str]: def validate_plugins(cls: "Params", value: t.List[str]) -> t.List[str]:
"""Validate and register configured plugins.""" """Validate and register configured plugins."""
plugin_dir = Settings.app_path / "plugins" plugin_dir = Settings.app_path / "plugins"
@ -111,11 +108,11 @@ class Params(ParamsPublic, HyperglassModel):
return [str(f) for f in matching_plugins] return [str(f) for f in matching_plugins]
return [] return []
def common_plugins(self) -> Tuple[Path, ...]: def common_plugins(self) -> t.Tuple[Path, ...]:
"""Get all validated external common plugins as Path objects.""" """Get all validated external common plugins as Path objects."""
return tuple(Path(p) for p in self.plugins) return tuple(Path(p) for p in self.plugins)
def frontend(self) -> Dict[str, Any]: def frontend(self) -> t.Dict[str, t.Any]:
"""Export UI-specific parameters.""" """Export UI-specific parameters."""
return self.export_dict( return self.export_dict(

View file

@ -1,11 +1,11 @@
"""Validate SSH proxy configuration variables.""" """Validate SSH proxy configuration variables."""
# Standard Library # Standard Library
from typing import Any, Dict, Union import typing as t
from ipaddress import IPv4Address, IPv6Address from ipaddress import IPv4Address, IPv6Address
# Third Party # Third Party
from pydantic import StrictInt, StrictStr, validator from pydantic import field_validator, ValidationInfo
# Project # Project
from hyperglass.util import resolve_hostname from hyperglass.util import resolve_hostname
@ -20,12 +20,12 @@ from .credential import Credential
class Proxy(HyperglassModel): class Proxy(HyperglassModel):
"""Validation model for per-proxy config in devices.yaml.""" """Validation model for per-proxy config in devices.yaml."""
address: Union[IPv4Address, IPv6Address, StrictStr] address: t.Union[IPv4Address, IPv6Address, str]
port: StrictInt = 22 port: int = 22
credential: Credential credential: Credential
platform: StrictStr = "linux_ssh" platform: str = "linux_ssh"
def __init__(self: "Proxy", **kwargs: Any) -> None: def __init__(self: "Proxy", **kwargs: t.Any) -> None:
"""Check for legacy fields.""" """Check for legacy fields."""
kwargs = check_legacy_fields(model="Proxy", data=kwargs) kwargs = check_legacy_fields(model="Proxy", data=kwargs)
super().__init__(**kwargs) super().__init__(**kwargs)
@ -34,7 +34,7 @@ class Proxy(HyperglassModel):
def _target(self): def _target(self):
return str(self.address) return str(self.address)
@validator("address") @field_validator("address")
def validate_address(cls, value): def validate_address(cls, value):
"""Ensure a hostname is resolvable.""" """Ensure a hostname is resolvable."""
@ -46,14 +46,14 @@ class Proxy(HyperglassModel):
) )
return value return value
@validator("platform", pre=True, always=True) @field_validator("platform", mode="before")
def validate_type(cls: "Proxy", value: Any, values: Dict[str, Any]) -> str: def validate_type(cls: "Proxy", value: t.Any, info: ValidationInfo) -> str:
"""Validate device type.""" """Validate device type."""
if value != "linux_ssh": if value != "linux_ssh":
raise UnsupportedDevice( raise UnsupportedDevice(
"Proxy '{p}' uses platform '{t}', which is currently unsupported.", "Proxy '{}' uses platform '{}', which is currently unsupported.",
p=values["address"], info.data.get("address"),
t=value, value,
) )
return value return value

View file

@ -1,23 +1,21 @@
"""Structured data configuration variables.""" """Structured data configuration variables."""
# Standard Library # Standard Library
from typing import List import typing as t
# Third Party
from pydantic import StrictStr, constr
# Local # Local
from ..main import HyperglassModel from ..main import HyperglassModel
StructuredCommunityMode = constr(regex=r"(permit|deny)") StructuredCommunityMode = t.Literal["permit", "deny"]
StructuredRPKIMode = constr(regex=r"(router|external)") StructuredRPKIMode = t.Literal["router", "external"]
class StructuredCommunities(HyperglassModel): class StructuredCommunities(HyperglassModel):
"""Control structured data response for BGP communities.""" """Control structured data response for BGP communities."""
mode: StructuredCommunityMode = "deny" mode: StructuredCommunityMode = "deny"
items: List[StrictStr] = [] items: t.List[str] = []
class StructuredRpki(HyperglassModel): class StructuredRpki(HyperglassModel):

View file

@ -1,298 +0,0 @@
"""Validate VRF configuration variables."""
# Standard Library
import re
from typing import Dict, List, Union, Literal, Optional
from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network
# Third Party
from pydantic import (
Field,
FilePath,
StrictStr,
StrictBool,
PrivateAttr,
conint,
constr,
validator,
root_validator,
)
# Project
from hyperglass.log import log
# Local
from ..main import HyperglassModel
ACLAction = constr(regex=r"permit|deny")
AddressFamily = Union[Literal[4], Literal[6]]
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.")
return generate_id(display_name)
class AccessList4(HyperglassModel):
"""Validation model for IPv4 access-lists."""
network: IPv4Network = Field(
"0.0.0.0/0",
title="Network",
description="IPv4 Network/Prefix that should be permitted or denied. ",
)
action: ACLAction = Field(
"permit",
title="Action",
description="Permit or deny any networks contained within the prefix.",
)
ge: conint(ge=0, le=32) = Field(
0,
title="Greater Than or Equal To",
description="Similar to `ge` in a Cisco prefix-list, the `ge` field defines the **bottom** threshold for prefix size. For example, a value of `24` would result in a query for `192.0.2.0/23` being denied, but a query for `192.0.2.0/32` would be permitted. If this field is set to a value smaller than the `network` field's prefix length, this field's value will be overwritten to the prefix length of the prefix in the `network` field.",
)
le: conint(ge=0, le=32) = Field(
32,
title="Less Than or Equal To",
description="Similar to `le` in a Cisco prefix-list, the `le` field defines the **top** threshold for prefix size. For example, a value of `24` would result in a query for `192.0.2.0/23` being permitted, but a query for `192.0.2.0/32` would be denied.",
)
@validator("ge")
def validate_model(cls, value, values):
"""Ensure ge is at least the size of the input prefix.
Arguments:
value {int} -- Initial ge value
values {dict} -- Other post-validation fields
Returns:
{int} -- Validated ge value
"""
net_len = values["network"].prefixlen
if net_len > value:
value = net_len
return value
class AccessList6(HyperglassModel):
"""Validation model for IPv6 access-lists."""
network: IPv6Network = Field(
"::/0",
title="Network",
description="IPv6 Network/Prefix that should be permitted or denied. ",
)
action: ACLAction = Field(
"permit",
title="Action",
description="Permit or deny any networks contained within the prefix.",
)
ge: conint(ge=0, le=128) = Field(
0,
title="Greater Than or Equal To",
description="Similar to `ge` in a Cisco prefix-list, the `ge` field defines the **bottom** threshold for prefix size. For example, a value of `64` would result in a query for `2001:db8::/48` being denied, but a query for `2001:db8::1/128` would be permitted. If this field is set to a value smaller than the `network` field's prefix length, this field's value will be overwritten to the prefix length of the prefix in the `network` field.",
)
le: conint(ge=0, le=128) = Field(
128,
title="Less Than or Equal To",
description="Similar to `le` in a Cisco prefix-list, the `le` field defines the **top** threshold for prefix size. For example, a value of `64` would result in a query for `2001:db8::/48` being permitted, but a query for `2001:db8::1/128` would be denied.",
)
@validator("ge")
def validate_model(cls, value, values):
"""Ensure ge is at least the size of the input prefix.
Arguments:
value {int} -- Initial ge value
values {dict} -- Other post-validation fields
Returns:
{int} -- Validated ge value
"""
net_len = values["network"].prefixlen
if net_len > value:
value = net_len
return value
class InfoConfigParams(HyperglassModel, extra="allow"):
"""Validation model for per-help params."""
title: Optional[StrictStr]
class Config:
"""Pydantic model configuration."""
title = "Help Parameters"
description = "Set dynamic or reusable values which may be used in the help/information content. Params my be access by using Python string formatting syntax, e.g. `{param_name}`. Any arbitrary values may be added."
class InfoConfig(HyperglassModel):
"""Validation model for help configuration."""
enable: StrictBool = True
file: Optional[FilePath]
params: InfoConfigParams = InfoConfigParams()
class Config:
"""Pydantic model configuration."""
fields = {
"enable": {
"title": "Enable",
"description": "Enable or disable the display of help/information content for this query type.",
},
"file": {
"title": "File Name",
"description": "Path to a valid text or Markdown file containing custom content.",
},
}
class Info(HyperglassModel):
"""Validation model for per-VRF, per-Command help."""
bgp_aspath: InfoConfig = InfoConfig()
bgp_community: InfoConfig = InfoConfig()
bgp_route: InfoConfig = InfoConfig()
ping: InfoConfig = InfoConfig()
traceroute: InfoConfig = InfoConfig()
class Config:
"""Pydantic model configuration."""
title = "VRF Information"
description = "Per-VRF help & information content."
fields = {
"bgp_aspath": {
"title": "BGP AS Path",
"description": "Show information about bgp_aspath queries when selected.",
},
"bgp_community": {
"title": "BGP Community",
"description": "Show information about bgp_community queries when selected.",
},
"bgp_route": {
"title": "BGP Route",
"description": "Show information about bgp_route queries when selected.",
},
"ping": {
"title": "Ping",
"description": "Show information about ping queries when selected.",
},
"traceroute": {
"title": "Traceroute",
"description": "Show information about traceroute queries when selected.",
},
}
class DeviceVrf4(HyperglassModel, extra="allow"):
"""Validation model for IPv4 AFI definitions."""
source_address: IPv4Address
access_list: List[AccessList4] = [AccessList4()]
force_cidr: StrictBool = True
class DeviceVrf6(HyperglassModel, extra="allow"):
"""Validation model for IPv6 AFI definitions."""
source_address: IPv6Address
access_list: List[AccessList6] = [AccessList6()]
force_cidr: StrictBool = True
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: Dict) -> Dict:
"""Set dynamic attributes before VRF initialization."""
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"
else:
protocol4 = "ipv4_vpn"
protocol6 = "ipv6_vpn"
if values.get("ipv4") is not None:
values["ipv4"].protocol = protocol4
values["ipv4"].version = 4
if values.get("ipv6") is not None:
values["ipv6"].protocol = protocol6
values["ipv6"].version = 6
if values.get("default", False) and values.get("display_name") is None:
values["display_name"] = "Global"
return values
def __getitem__(self, i: AddressFamily) -> Union[DeviceVrf4, DeviceVrf6]:
"""Access the VRF's AFI by IP protocol number."""
if i not in (4, 6):
raise AttributeError(f"Must be 4 or 6, got '{i}'")
return getattr(self, f"ipv{i}")
def __hash__(self) -> int:
"""Make VRF object hashable so the object can be deduplicated with set()."""
return hash((self.name,))
def __eq__(self, other: object) -> bool:
"""Make VRF object comparable so the object can be deduplicated with set()."""
result = False
if isinstance(other, HyperglassModel):
result = self.name == other.name
return result
class Config:
"""Pydantic model configuration."""
title = "VRF"
description = "Per-VRF configuration."
fields = {
"name": {
"title": "Name",
"description": "VRF name as configured on the router/device.",
},
"display_name": {
"title": "Display Name",
"description": "Display name of VRF for use in the hyperglass UI. If none is specified, hyperglass will attempt to generate one.",
},
}

View file

@ -5,17 +5,8 @@ import typing as t
from pathlib import Path from pathlib import Path
# Third Party # Third Party
from pydantic import ( from pydantic import HttpUrl, FilePath, constr, field_validator, model_validator, ValidationInfo
HttpUrl, from pydantic_extra_types.color import Color
FilePath,
StrictInt,
StrictStr,
StrictBool,
constr,
validator,
root_validator,
)
from pydantic.color import Color
# Project # Project
from hyperglass.defaults import DEFAULT_HELP, DEFAULT_TERMS from hyperglass.defaults import DEFAULT_HELP, DEFAULT_TERMS
@ -27,40 +18,40 @@ from .opengraph import OpenGraph
DEFAULT_IMAGES = Path(__file__).parent.parent.parent / "images" DEFAULT_IMAGES = Path(__file__).parent.parent.parent / "images"
Percentage = constr(regex=r"^([1-9][0-9]?|100)\%$") Percentage = constr(pattern=r"^([1-9][0-9]?|100)\%$")
TitleMode = constr(regex=("logo_only|text_only|logo_subtitle|all")) TitleMode = t.Literal["logo_only", "text_only", "logo_subtitle", "all"]
ColorMode = constr(regex=r"light|dark") ColorMode = t.Literal["light", "dark"]
DOHProvider = constr(regex="|".join(DNS_OVER_HTTPS.keys())) DOHProvider = constr(pattern="|".join(DNS_OVER_HTTPS.keys()))
Title = constr(max_length=32) Title = constr(max_length=32)
Side = constr(regex=r"left|right") Side = t.Literal["left", "right"]
LocationDisplayMode = t.Literal["auto", "dropdown", "gallery"] LocationDisplayMode = t.Literal["auto", "dropdown", "gallery"]
class Credit(HyperglassModel): class Credit(HyperglassModel):
"""Validation model for developer credit.""" """Validation model for developer credit."""
enable: StrictBool = True enable: bool = True
class Link(HyperglassModel): class Link(HyperglassModel):
"""Validation model for generic link.""" """Validation model for generic link."""
title: StrictStr title: str
url: HttpUrl url: HttpUrl
show_icon: StrictBool = True show_icon: bool = True
side: Side = "left" side: Side = "left"
order: StrictInt = 0 order: int = 0
class Menu(HyperglassModel): class Menu(HyperglassModel):
"""Validation model for generic menu.""" """Validation model for generic menu."""
title: StrictStr title: str
content: StrictStr content: str
side: Side = "left" side: Side = "left"
order: StrictInt = 0 order: int = 0
@validator("content") @field_validator("content")
def validate_content(cls: "Menu", value: str) -> str: def validate_content(cls: "Menu", value: str) -> str:
"""Read content from file if a path is provided.""" """Read content from file if a path is provided."""
@ -75,16 +66,16 @@ class Menu(HyperglassModel):
class Greeting(HyperglassModel): class Greeting(HyperglassModel):
"""Validation model for greeting modal.""" """Validation model for greeting modal."""
enable: StrictBool = False enable: bool = False
file: t.Optional[FilePath] file: t.Optional[FilePath] = None
title: StrictStr = "Welcome" title: str = "Welcome"
button: StrictStr = "Continue" button: str = "Continue"
required: StrictBool = False required: bool = False
@validator("file") @field_validator("file")
def validate_file(cls, value, values): def validate_file(cls, value: str, info: ValidationInfo):
"""Ensure file is specified if greeting is enabled.""" """Ensure file is specified if greeting is enabled."""
if values["enable"] and value is None: if info.data.get("enable") and value is None:
raise ValueError("Greeting is enabled, but no file is specified.") raise ValueError("Greeting is enabled, but no file is specified.")
return value return value
@ -95,15 +86,15 @@ class Logo(HyperglassModel):
light: FilePath = DEFAULT_IMAGES / "hyperglass-light.svg" light: FilePath = DEFAULT_IMAGES / "hyperglass-light.svg"
dark: FilePath = DEFAULT_IMAGES / "hyperglass-dark.svg" dark: FilePath = DEFAULT_IMAGES / "hyperglass-dark.svg"
favicon: FilePath = DEFAULT_IMAGES / "hyperglass-icon.svg" favicon: FilePath = DEFAULT_IMAGES / "hyperglass-icon.svg"
width: t.Optional[t.Union[StrictInt, Percentage]] = "100%" width: t.Optional[t.Union[int, Percentage]] = "100%"
height: t.Optional[t.Union[StrictInt, Percentage]] height: t.Optional[t.Union[int, Percentage]] = None
class LogoPublic(Logo): class LogoPublic(Logo):
"""Public logo configuration.""" """Public logo configuration."""
light_format: StrictStr light_format: str
dark_format: StrictStr dark_format: str
class Text(HyperglassModel): class Text(HyperglassModel):
@ -112,27 +103,27 @@ class Text(HyperglassModel):
title_mode: TitleMode = "logo_only" title_mode: TitleMode = "logo_only"
title: Title = "hyperglass" title: Title = "hyperglass"
subtitle: Title = "Network Looking Glass" subtitle: Title = "Network Looking Glass"
query_location: StrictStr = "Location" query_location: str = "Location"
query_type: StrictStr = "Query Type" query_type: str = "Query Type"
query_target: StrictStr = "Target" query_target: str = "Target"
fqdn_tooltip: StrictStr = "Use {protocol}" # Formatted by Javascript fqdn_tooltip: str = "Use {protocol}" # Formatted by Javascript
fqdn_message: StrictStr = "Your browser has resolved {fqdn} to" # Formatted by Javascript fqdn_message: str = "Your browser has resolved {fqdn} to" # Formatted by Javascript
fqdn_error: StrictStr = "Unable to resolve {fqdn}" # Formatted by Javascript fqdn_error: str = "Unable to resolve {fqdn}" # Formatted by Javascript
fqdn_error_button: StrictStr = "Try Again" fqdn_error_button: str = "Try Again"
cache_prefix: StrictStr = "Results cached for " cache_prefix: str = "Results cached for "
cache_icon: StrictStr = "Cached from {time} UTC" # Formatted by Javascript cache_icon: str = "Cached from {time} UTC" # Formatted by Javascript
complete_time: StrictStr = "Completed in {seconds}" # Formatted by Javascript complete_time: str = "Completed in {seconds}" # Formatted by Javascript
rpki_invalid: StrictStr = "Invalid" rpki_invalid: str = "Invalid"
rpki_valid: StrictStr = "Valid" rpki_valid: str = "Valid"
rpki_unknown: StrictStr = "No ROAs Exist" rpki_unknown: str = "No ROAs Exist"
rpki_unverified: StrictStr = "Not Verified" rpki_unverified: str = "Not Verified"
no_communities: StrictStr = "No Communities" no_communities: str = "No Communities"
ip_error: StrictStr = "Unable to determine IP Address" ip_error: str = "Unable to determine IP Address"
no_ip: StrictStr = "No {protocol} Address" no_ip: str = "No {protocol} Address"
ip_select: StrictStr = "Select an IP Address" ip_select: str = "Select an IP Address"
ip_button: StrictStr = "My IP" ip_button: str = "My IP"
@validator("cache_prefix") @field_validator("cache_prefix")
def validate_cache_prefix(cls: "Text", value: str) -> str: def validate_cache_prefix(cls: "Text", value: str) -> str:
"""Ensure trailing whitespace.""" """Ensure trailing whitespace."""
return " ".join(value.split()) + " " return " ".join(value.split()) + " "
@ -155,21 +146,19 @@ class ThemeColors(HyperglassModel):
cyan: Color = "#118ab2" cyan: Color = "#118ab2"
pink: Color = "#f2607d" pink: Color = "#f2607d"
purple: Color = "#8d30b5" purple: Color = "#8d30b5"
primary: t.Optional[Color] primary: t.Optional[Color] = None
secondary: t.Optional[Color] secondary: t.Optional[Color] = None
success: t.Optional[Color] success: t.Optional[Color] = None
warning: t.Optional[Color] warning: t.Optional[Color] = None
error: t.Optional[Color] error: t.Optional[Color] = None
danger: t.Optional[Color] danger: t.Optional[Color] = None
@validator(*FUNC_COLOR_MAP.keys(), pre=True, always=True) @field_validator(*FUNC_COLOR_MAP.keys(), mode="before")
def validate_colors( def validate_colors(cls: "ThemeColors", value: str, info: ValidationInfo) -> str:
cls: "ThemeColors", value: str, values: t.Dict[str, t.Optional[str]], field
) -> str:
"""Set default functional color mapping.""" """Set default functional color mapping."""
if value is None: if value is None:
default_color = FUNC_COLOR_MAP[field.name] default_color = FUNC_COLOR_MAP[info.field_name]
value = str(values[default_color]) value = str(info.data[default_color])
return value return value
def dict(self, *args: t.Any, **kwargs: t.Any) -> t.Dict[str, str]: def dict(self, *args: t.Any, **kwargs: t.Any) -> t.Dict[str, str]:
@ -180,15 +169,15 @@ class ThemeColors(HyperglassModel):
class ThemeFonts(HyperglassModel): class ThemeFonts(HyperglassModel):
"""Validation model for theme fonts.""" """Validation model for theme fonts."""
body: StrictStr = "Nunito" body: str = "Nunito"
mono: StrictStr = "Fira Code" mono: str = "Fira Code"
class Theme(HyperglassModel): class Theme(HyperglassModel):
"""Validation model for theme variables.""" """Validation model for theme variables."""
colors: ThemeColors = ThemeColors() colors: ThemeColors = ThemeColors()
default_color_mode: t.Optional[ColorMode] default_color_mode: t.Optional[ColorMode] = None
fonts: ThemeFonts = ThemeFonts() fonts: ThemeFonts = ThemeFonts()
@ -196,27 +185,30 @@ class DnsOverHttps(HyperglassModel):
"""Validation model for DNS over HTTPS resolution.""" """Validation model for DNS over HTTPS resolution."""
name: DOHProvider = "cloudflare" name: DOHProvider = "cloudflare"
url: StrictStr = "" url: str = ""
@root_validator @model_validator(mode="before")
def validate_dns(cls: "DnsOverHttps", values: t.Dict[str, str]) -> t.Dict[str, str]: def validate_dns(cls, data: "DnsOverHttps") -> t.Dict[str, str]:
"""Assign url field to model based on selected provider.""" """Assign url field to model based on selected provider."""
provider = values["name"] name = data.get("name", "cloudflare")
values["url"] = DNS_OVER_HTTPS[provider] url = DNS_OVER_HTTPS[name]
return values return {
"name": name,
"url": url,
}
class HighlightPattern(HyperglassModel): class HighlightPattern(HyperglassModel):
"""Validation model for highlight pattern configuration.""" """Validation model for highlight pattern configuration."""
pattern: StrictStr pattern: str
label: t.Optional[StrictStr] = None label: t.Optional[str] = None
color: StrictStr = "primary" color: str = "primary"
@validator("color") @field_validator("color")
def validate_color(cls: "HighlightPattern", value: str) -> str: def validate_color(cls: "HighlightPattern", value: str) -> str:
"""Ensure highlight color is a valid theme color.""" """Ensure highlight color is a valid theme color."""
colors = list(ThemeColors.__fields__.keys()) colors = list(ThemeColors.model_fields.keys())
color_list = "\n - ".join(("", *colors)) color_list = "\n - ".join(("", *colors))
if value not in colors: if value not in colors:
raise ValueError( raise ValueError(
@ -243,8 +235,8 @@ class Web(HyperglassModel):
text: Text = Text() text: Text = Text()
theme: Theme = Theme() theme: Theme = Theme()
location_display_mode: LocationDisplayMode = "auto" location_display_mode: LocationDisplayMode = "auto"
custom_javascript: t.Optional[FilePath] custom_javascript: t.Optional[FilePath] = None
custom_html: t.Optional[FilePath] custom_html: t.Optional[FilePath] = None
highlight: t.List[HighlightPattern] = [] highlight: t.List[HighlightPattern] = []

View file

@ -2,11 +2,11 @@
# Standard Library # Standard Library
import re import re
from typing import List, Literal import typing as t
from ipaddress import ip_network from ipaddress import ip_network
# Third Party # Third Party
from pydantic import StrictInt, StrictStr, StrictBool, validator from pydantic import field_validator
# Project # Project
from hyperglass.state import use_state from hyperglass.state import use_state
@ -15,27 +15,27 @@ from hyperglass.external.rpki import rpki_state
# Local # Local
from ..main import HyperglassModel from ..main import HyperglassModel
WinningWeight = Literal["low", "high"] WinningWeight = t.Literal["low", "high"]
class BGPRoute(HyperglassModel): class BGPRoute(HyperglassModel):
"""Post-parsed BGP route.""" """Post-parsed BGP route."""
prefix: StrictStr prefix: str
active: StrictBool active: bool
age: StrictInt age: int
weight: StrictInt weight: int
med: StrictInt med: int
local_preference: StrictInt local_preference: int
as_path: List[StrictInt] as_path: t.List[int]
communities: List[StrictStr] communities: t.List[str]
next_hop: StrictStr next_hop: str
source_as: StrictInt source_as: int
source_rid: StrictStr source_rid: str
peer_rid: StrictStr peer_rid: str
rpki_state: StrictInt rpki_state: int
@validator("communities") @field_validator("communities")
def validate_communities(cls, value): def validate_communities(cls, value):
"""Filter returned communities against configured policy. """Filter returned communities against configured policy.
@ -69,7 +69,7 @@ class BGPRoute(HyperglassModel):
return [c for c in value if func(c)] return [c for c in value if func(c)]
@validator("rpki_state") @field_validator("rpki_state")
def validate_rpki_state(cls, value, values): def validate_rpki_state(cls, value, values):
"""If external RPKI validation is enabled, get validation state.""" """If external RPKI validation is enabled, get validation state."""
@ -106,9 +106,9 @@ class BGPRoute(HyperglassModel):
class BGPRouteTable(HyperglassModel): class BGPRouteTable(HyperglassModel):
"""Post-parsed BGP route table.""" """Post-parsed BGP route table."""
vrf: StrictStr vrf: str
count: StrictInt = 0 count: int = 0
routes: List[BGPRoute] routes: t.List[BGPRoute]
winning_weight: WinningWeight winning_weight: WinningWeight
def __init__(self, **kwargs): def __init__(self, **kwargs):

View file

@ -6,7 +6,15 @@ import typing as t
from ipaddress import IPv4Network, IPv6Network, ip_network from ipaddress import IPv4Network, IPv6Network, ip_network
# Third Party # Third Party
from pydantic import Field, FilePath, StrictStr, StrictBool, PrivateAttr, conint, validator from pydantic import (
Discriminator,
field_validator,
Field,
FilePath,
IPvAnyNetwork,
PrivateAttr,
Tag,
)
# Project # Project
from hyperglass.log import log from hyperglass.log import log
@ -18,24 +26,20 @@ from hyperglass.exceptions.private import InputValidationError
from .main import MultiModel, HyperglassModel, HyperglassUniqueModel from .main import MultiModel, HyperglassModel, HyperglassUniqueModel
from .fields import Action from .fields import Action
if t.TYPE_CHECKING:
# Project
from hyperglass.models.api.query import QueryTarget
IPv4PrefixLength = conint(ge=0, le=32) StringOrArray = t.Union[str, t.List[str]]
IPv6PrefixLength = conint(ge=0, le=128) Condition = t.Union[IPvAnyNetwork, str]
IPNetwork = t.Union[IPv4Network, IPv6Network]
StringOrArray = t.Union[StrictStr, t.List[StrictStr]]
Condition = t.Union[IPv4Network, IPv6Network, StrictStr]
RuleValidation = t.Union[t.Literal["ipv4", "ipv6", "pattern"], None] RuleValidation = t.Union[t.Literal["ipv4", "ipv6", "pattern"], None]
PassedValidation = t.Union[bool, None] PassedValidation = t.Union[bool, None]
IPFamily = t.Literal["ipv4", "ipv6"]
RuleTypeAttr = t.Literal["ipv4", "ipv6", "pattern", "none"]
class Input(HyperglassModel): class Input(HyperglassModel):
"""Base input field.""" """Base input field."""
_type: PrivateAttr _type: PrivateAttr
description: StrictStr description: str
@property @property
def is_select(self) -> bool: def is_select(self) -> bool:
@ -52,15 +56,15 @@ class Text(Input):
"""Text/input field model.""" """Text/input field model."""
_type: PrivateAttr = PrivateAttr("text") _type: PrivateAttr = PrivateAttr("text")
validation: t.Optional[StrictStr] validation: t.Optional[str] = None
class Option(HyperglassModel): class Option(HyperglassModel):
"""Select option model.""" """Select option model."""
name: t.Optional[StrictStr] name: t.Optional[str] = None
description: t.Optional[StrictStr] description: t.Optional[str] = None
value: StrictStr value: str
class Select(Input): class Select(Input):
@ -70,16 +74,16 @@ class Select(Input):
options: t.List[Option] options: t.List[Option]
class Rule(HyperglassModel, allow_population_by_field_name=True): class Rule(HyperglassModel):
"""Base rule.""" """Base rule."""
_validation: RuleValidation = PrivateAttr() _type: RuleTypeAttr = PrivateAttr(Field("none", discriminator="_type"))
_passed: PassedValidation = PrivateAttr(None) _passed: PassedValidation = PrivateAttr(None)
condition: Condition condition: Condition
action: Action = Action("permit") action: Action = "permit"
commands: t.List[str] = Field([], alias="command") commands: t.List[str] = Field([], alias="command")
@validator("commands", pre=True, allow_reuse=True) @field_validator("commands", mode="before")
def validate_commands(cls, value: t.Union[str, t.List[str]]) -> t.List[str]: def validate_commands(cls, value: t.Union[str, t.List[str]]) -> t.List[str]:
"""Ensure commands is a list.""" """Ensure commands is a list."""
if isinstance(value, str): if isinstance(value, str):
@ -89,22 +93,28 @@ class Rule(HyperglassModel, allow_population_by_field_name=True):
def validate_target(self, target: str, *, multiple: bool) -> bool: def validate_target(self, target: str, *, multiple: bool) -> bool:
"""Validate a query target (Placeholder signature).""" """Validate a query target (Placeholder signature)."""
raise NotImplementedError( raise NotImplementedError(
f"{self._validation} rule does not implement a 'validate_target()' method" f"{self._type} rule does not implement a 'validate_target()' method"
) )
class RuleWithIP(Rule): class RuleWithIP(Rule):
"""Base IP-based rule.""" """Base IP-based rule."""
_family: PrivateAttr condition: IPvAnyNetwork
condition: IPNetwork allow_reserved: bool = False
allow_reserved: StrictBool = False allow_unspecified: bool = False
allow_unspecified: StrictBool = False allow_loopback: bool = False
allow_loopback: StrictBool = False
ge: int ge: int
le: int le: int
def membership(self, target: IPNetwork, network: IPNetwork) -> bool: def __init__(self, **kw) -> None:
super().__init__(**kw)
if self.condition.network_address.version == 4:
self._type = "ipv4"
else:
self._type = "ipv6"
def membership(self, target: IPvAnyNetwork, network: IPvAnyNetwork) -> bool:
"""Check if IP address belongs to network.""" """Check if IP address belongs to network."""
log.debug("Checking membership of {} for {}", str(target), str(network)) log.debug("Checking membership of {} for {}", str(target), str(network))
if ( if (
@ -115,7 +125,7 @@ class RuleWithIP(Rule):
return True return True
return False return False
def in_range(self, target: IPNetwork) -> bool: def in_range(self, target: IPvAnyNetwork) -> bool:
"""Verify if target prefix length is within ge/le threshold.""" """Verify if target prefix length is within ge/le threshold."""
if target.prefixlen <= self.le and target.prefixlen >= self.ge: if target.prefixlen <= self.le and target.prefixlen >= self.ge:
log.debug("{} is in range {}-{}", target, self.ge, self.le) log.debug("{} is in range {}-{}", target, self.ge, self.le)
@ -123,7 +133,7 @@ class RuleWithIP(Rule):
return False return False
def validate_target(self, target: "QueryTarget", *, multiple: bool) -> bool: def validate_target(self, target: str, *, multiple: bool) -> bool:
"""Validate an IP address target against this rule's conditions.""" """Validate an IP address target against this rule's conditions."""
if isinstance(target, t.List): if isinstance(target, t.List):
@ -169,30 +179,28 @@ class RuleWithIP(Rule):
class RuleWithIPv4(RuleWithIP): class RuleWithIPv4(RuleWithIP):
"""A rule by which to evaluate an IPv4 target.""" """A rule by which to evaluate an IPv4 target."""
_family: PrivateAttr = PrivateAttr("ipv4") _type: RuleTypeAttr = "ipv4"
_validation: RuleValidation = PrivateAttr("ipv4")
condition: IPv4Network condition: IPv4Network
ge: IPv4PrefixLength = 0 ge: int = Field(0, ge=0, le=32)
le: IPv4PrefixLength = 32 le: int = Field(32, ge=0, le=32)
class RuleWithIPv6(RuleWithIP): class RuleWithIPv6(RuleWithIP):
"""A rule by which to evaluate an IPv6 target.""" """A rule by which to evaluate an IPv6 target."""
_family: PrivateAttr = PrivateAttr("ipv6") _type: RuleTypeAttr = "ipv6"
_validation: RuleValidation = PrivateAttr("ipv6")
condition: IPv6Network condition: IPv6Network
ge: IPv6PrefixLength = 0 ge: int = Field(0, ge=0, le=128)
le: IPv6PrefixLength = 128 le: int = Field(128, ge=0, le=128)
class RuleWithPattern(Rule): class RuleWithPattern(Rule):
"""A rule validated by a regular expression pattern.""" """A rule validated by a regular expression pattern."""
_validation: RuleValidation = PrivateAttr("pattern") _type: RuleTypeAttr = "pattern"
condition: StrictStr condition: str
def validate_target(self, target: "QueryTarget", *, multiple: bool) -> str: # noqa: C901 def validate_target(self, target: str, *, multiple: bool) -> str: # noqa: C901
"""Validate a string target against configured regex patterns.""" """Validate a string target against configured regex patterns."""
def validate_single_value(value: str) -> t.Union[bool, BaseException]: def validate_single_value(value: str) -> t.Union[bool, BaseException]:
@ -234,7 +242,7 @@ class RuleWithPattern(Rule):
class RuleWithoutValidation(Rule): class RuleWithoutValidation(Rule):
"""A rule with no validation.""" """A rule with no validation."""
_validation: RuleValidation = PrivateAttr(None) _type: RuleTypeAttr = "none"
condition: None condition: None
def validate_target(self, target: str, *, multiple: bool) -> t.Literal[True]: def validate_target(self, target: str, *, multiple: bool) -> t.Literal[True]:
@ -243,24 +251,44 @@ class RuleWithoutValidation(Rule):
return True return True
RuleType = t.Union[RuleWithIPv4, RuleWithIPv6, RuleWithPattern, RuleWithoutValidation] RuleWithIPv4Type = t.Annotated[RuleWithIPv4, Tag("ipv4")]
RuleWithIPv6Type = t.Annotated[RuleWithIPv6, Tag("ipv6")]
RuleWithPatternType = t.Annotated[RuleWithPattern, Tag("pattern")]
RuleWithoutValidationType = t.Annotated[RuleWithoutValidation, Tag("none")]
# RuleType = t.Union[RuleWithIPv4, RuleWithIPv6, RuleWithPattern, RuleWithoutValidation]
RuleType = t.Union[
RuleWithIPv4Type,
RuleWithIPv6Type,
RuleWithPatternType,
RuleWithoutValidationType,
]
def type_discriminator(value: t.Any) -> RuleTypeAttr:
"""Pydantic type discriminator."""
if isinstance(value, dict):
return value.get("_type")
return getattr(value, "_type", None)
class Directive(HyperglassUniqueModel, unique_by=("id", "table_output")): class Directive(HyperglassUniqueModel, unique_by=("id", "table_output")):
"""A directive contains commands that can be run on a device, as long as defined rules are met.""" """A directive contains commands that can be run on a device, as long as defined rules are met."""
__hyperglass_builtin__: t.ClassVar[bool] = False _hyperglass_builtin: bool = PrivateAttr(False)
id: StrictStr id: str
name: StrictStr name: str
rules: t.List[RuleType] = [RuleWithPattern(condition="*")] rules: t.List[RuleType] = [
Field(RuleWithPattern(condition="*"), discriminator=Discriminator(type_discriminator))
]
field: t.Union[Text, Select] field: t.Union[Text, Select]
info: t.Optional[FilePath] info: t.Optional[FilePath] = None
plugins: t.List[StrictStr] = [] plugins: t.List[str] = []
table_output: t.Optional[StrictStr] table_output: t.Optional[str] = None
groups: t.List[StrictStr] = [] groups: t.List[str] = []
multiple: StrictBool = False multiple: bool = False
multiple_separator: StrictStr = " " multiple_separator: str = " "
def validate_target(self, target: str) -> bool: def validate_target(self, target: str) -> bool:
"""Validate a target against all configured rules.""" """Validate a target against all configured rules."""
@ -281,7 +309,7 @@ class Directive(HyperglassUniqueModel, unique_by=("id", "table_output")):
return "text" return "text"
return None return None
@validator("plugins") @field_validator("plugins")
def validate_plugins(cls: "Directive", plugins: t.List[str]) -> t.List[str]: def validate_plugins(cls: "Directive", plugins: t.List[str]) -> t.List[str]:
"""Validate and register configured plugins.""" """Validate and register configured plugins."""
plugin_dir = Settings.app_path / "plugins" plugin_dir = Settings.app_path / "plugins"
@ -322,7 +350,7 @@ class Directive(HyperglassUniqueModel, unique_by=("id", "table_output")):
class BuiltinDirective(Directive, unique_by=("id", "table_output", "platforms")): class BuiltinDirective(Directive, unique_by=("id", "table_output", "platforms")):
"""Natively-supported directive.""" """Natively-supported directive."""
__hyperglass_builtin__: t.ClassVar[bool] = True _hyperglass_builtin: bool = PrivateAttr(True)
platforms: Series[str] = [] platforms: Series[str] = []
@ -339,7 +367,7 @@ class Directives(MultiModel[Directive], model=Directive, unique_by="id"):
*( *(
self.table_if_available(directive) if table_output else directive # noqa: IF100 GFY self.table_if_available(directive) if table_output else directive # noqa: IF100 GFY
for directive in self for directive in self
if directive.__hyperglass_builtin__ is True if directive._hyperglass_builtin is True
and platform in getattr(directive, "platforms", ()) and platform in getattr(directive, "platforms", ())
) )
) )

View file

@ -3,12 +3,11 @@
# Standard Library # Standard Library
import re import re
import typing as t import typing as t
from pathlib import Path
# Third Party # Third Party
from pydantic import StrictInt, StrictFloat from pydantic import AfterValidator, BeforeValidator
IntFloat = t.TypeVar("IntFloat", StrictInt, StrictFloat) IntFloat = t.TypeVar("IntFloat", int, float)
J = t.TypeVar("J") J = t.TypeVar("J")
SupportedDriver = t.Literal["netmiko", "hyperglass_agent"] SupportedDriver = t.Literal["netmiko", "hyperglass_agent"]
@ -17,133 +16,42 @@ HttpProvider = t.Literal["msteams", "slack", "generic"]
LogFormat = t.Literal["text", "json"] LogFormat = t.Literal["text", "json"]
Primitives = t.Union[None, float, int, bool, str] Primitives = t.Union[None, float, int, bool, str]
JsonValue = t.Union[J, t.Sequence[J], t.Dict[str, J]] JsonValue = t.Union[J, t.Sequence[J], t.Dict[str, J]]
ActionValue = t.Literal["permit", "deny"]
HttpMethodValue = t.Literal[
"CONNECT",
"DELETE",
"GET",
"HEAD",
"OPTIONS",
"PATCH",
"POST",
"PUT",
"TRACE",
]
class AnyUri(str): def validate_uri(value: str) -> str:
"""Custom field type for HTTP URI, e.g. /example.""" """Ensure URI string contains a leading forward-slash."""
uri_regex = re.compile(r"^(\/.*)$")
@classmethod match = uri_regex.fullmatch(value)
def __get_validators__(cls): if not match:
"""Pydantic custom field method.""" raise ValueError("Invalid format. A URI must begin with a forward slash, e.g. '/example'")
yield cls.validate return match.group()
@classmethod
def validate(cls, value):
"""Ensure URI string contains a leading forward-slash."""
uri_regex = re.compile(r"^(\/.*)$")
if not isinstance(value, str):
raise TypeError("AnyUri type must be a string")
match = uri_regex.fullmatch(value)
if not match:
raise ValueError(
"Invalid format. A URI must begin with a forward slash, e.g. '/example'"
)
return cls(match.group())
def __repr__(self):
"""Stringify custom field representation."""
return f"AnyUri({super().__repr__()})"
class Action(str): def validate_action(value: str) -> ActionValue:
"""Custom field type for policy actions.""" """Ensure action is an allowed value or acceptable alias."""
permits = ("permit", "allow", "accept") permits = ("permit", "allow", "accept")
denies = ("deny", "block", "reject") denies = ("deny", "block", "reject")
value = value.strip().lower()
if value in permits:
return "permit"
if value in denies:
return "deny"
@classmethod raise ValueError("Action must be one of '{}'".format(", ".join((*permits, *denies))))
def __get_validators__(cls):
"""Pydantic custom field method."""
yield cls.validate
@classmethod
def validate(cls, value: str):
"""Ensure action is an allowed value or acceptable alias."""
if not isinstance(value, str):
raise TypeError("Action type must be a string")
value = value.strip().lower()
if value in cls.permits:
return cls("permit")
if value in cls.denies:
return cls("deny")
raise ValueError(
"Action must be one of '{}'".format(", ".join((*cls.permits, *cls.denies)))
)
def __repr__(self):
"""Stringify custom field representation."""
return f"Action({super().__repr__()})"
class HttpMethod(str): AnyUri = t.Annotated[str, AfterValidator(validate_uri)]
"""Custom field type for HTTP methods.""" Action = t.Annotated[ActionValue, AfterValidator(validate_action)]
HttpMethod = t.Annotated[HttpMethodValue, BeforeValidator(str.upper)]
methods = (
"CONNECT",
"DELETE",
"GET",
"HEAD",
"OPTIONS",
"PATCH",
"POST",
"PUT",
"TRACE",
)
@classmethod
def __get_validators__(cls):
"""Pydantic custom field method."""
yield cls.validate
@classmethod
def validate(cls, value: str):
"""Ensure http method is valid."""
if not isinstance(value, str):
raise TypeError("HTTP Method must be a string")
value = value.strip().upper()
if value in cls.methods:
return cls(value)
raise ValueError("HTTP Method must be one of {!r}".format(", ".join(cls.methods)))
def __repr__(self):
"""Stringify custom field representation."""
return f"HttpMethod({super().__repr__()})"
class ConfigPathItem(Path):
"""Custom field type for files or directories contained within app_path."""
@classmethod
def __get_validators__(cls):
"""Pydantic custom field method."""
yield cls.validate
@classmethod
def validate(cls, value: Path) -> Path:
"""Ensure path is within app path."""
if isinstance(value, str):
value = Path(value)
if not isinstance(value, Path):
raise TypeError("Unable to convert type {} to ConfigPathItem".format(type(value)))
# Project
from hyperglass.settings import Settings
if not value.is_relative_to(Settings.app_path):
raise ValueError("{!s} must be relative to {!s}".format(value, Settings.app_path))
if Settings.container:
value = Settings.default_app_path.joinpath(
*(p for p in value.parts if p not in Settings.app_path.parts)
)
return value
def __repr__(self):
"""Stringify custom field representation."""
return f"ConfigPathItem({super().__repr__()})"

View file

@ -9,8 +9,7 @@ import typing as t
from pathlib import Path from pathlib import Path
# Third Party # Third Party
from pydantic import HttpUrl, BaseModel, BaseConfig, PrivateAttr from pydantic import HttpUrl, BaseModel, PrivateAttr, RootModel, ConfigDict
from pydantic.generics import GenericModel
# Project # Project
from hyperglass.log import log from hyperglass.log import log
@ -20,38 +19,30 @@ from hyperglass.types import Series
MultiModelT = t.TypeVar("MultiModelT", bound=BaseModel) MultiModelT = t.TypeVar("MultiModelT", bound=BaseModel)
def alias_generator(field: str) -> str:
"""Remove unsupported characters from field names.
Converts any "desirable" separators to underscore, then removes all
characters that are unsupported in Python class variable names.
Also removes leading numbers underscores.
"""
_replaced = re.sub(r"[\-|\.|\@|\~|\:\/|\s]", "_", field)
_scrubbed = "".join(re.findall(r"([a-zA-Z]\w+|\_+)", _replaced))
snake_field = _scrubbed.lower()
return snake_to_camel(snake_field)
class HyperglassModel(BaseModel): class HyperglassModel(BaseModel):
"""Base model for all hyperglass configuration models.""" """Base model for all hyperglass configuration models."""
class Config(BaseConfig): model_config = ConfigDict(
"""Pydantic model configuration.""" extra="forbid",
json_encoders={HttpUrl: lambda v: str(v), Path: str},
validate_all = True populate_by_name=True,
extra = "forbid" validate_assignment=True,
validate_assignment = True validate_default=True,
allow_population_by_field_name = True alias_generator=alias_generator,
json_encoders = {HttpUrl: lambda v: str(v), Path: str} )
@classmethod
def alias_generator(cls: "HyperglassModel", field: str) -> str:
"""Remove unsupported characters from field names.
Converts any "desirable" separators to underscore, then removes all
characters that are unsupported in Python class variable names.
Also removes leading numbers underscores.
"""
_replaced = re.sub(r"[\-|\.|\@|\~|\:\/|\s]", "_", field)
_scrubbed = "".join(re.findall(r"([a-zA-Z]\w+|\_+)", _replaced))
snake_field = _scrubbed.lower()
if snake_field != field:
log.debug(
"Model field '{}.{}' was converted from {} to {}",
cls.__module__,
snake_field,
repr(field),
repr(snake_field),
)
return snake_to_camel(snake_field)
def convert_paths(self, value: t.Any): def convert_paths(self, value: t.Any):
"""Change path to relative to app_path.""" """Change path to relative to app_path."""
@ -98,7 +89,7 @@ class HyperglassModel(BaseModel):
for key in kwargs.keys(): for key in kwargs.keys():
export_kwargs.pop(key, None) export_kwargs.pop(key, None)
return self.json(*args, **export_kwargs, **kwargs) return self.model_dump_json(*args, **export_kwargs, **kwargs)
def export_dict(self, *args, **kwargs): def export_dict(self, *args, **kwargs):
"""Return instance as dictionary.""" """Return instance as dictionary."""
@ -108,7 +99,7 @@ class HyperglassModel(BaseModel):
for key in kwargs.keys(): for key in kwargs.keys():
export_kwargs.pop(key, None) export_kwargs.pop(key, None)
return self.dict(*args, **export_kwargs, **kwargs) return self.model_dump(*args, **export_kwargs, **kwargs)
def export_yaml(self, *args, **kwargs): def export_yaml(self, *args, **kwargs):
"""Return instance as YAML.""" """Return instance as YAML."""
@ -177,47 +168,45 @@ class HyperglassModelWithId(HyperglassModel):
return hash(self.id) return hash(self.id)
class MultiModel(GenericModel, t.Generic[MultiModelT]): class MultiModel(RootModel[MultiModelT]):
"""Extension of HyperglassModel for managing multiple models as a list.""" """Extension of HyperglassModel for managing multiple models as a list."""
model_config = ConfigDict(
validate_default=True,
validate_assignment=True,
)
model: t.ClassVar[MultiModelT] model: t.ClassVar[MultiModelT]
unique_by: t.ClassVar[str] unique_by: t.ClassVar[str]
model_name: t.ClassVar[str] = "MultiModel" _model_name: t.ClassVar[str] = "MultiModel"
__root__: t.List[MultiModelT] = [] root: t.List[MultiModelT] = []
_count: int = PrivateAttr() _count: int = PrivateAttr()
class Config(BaseConfig):
"""Pydantic model configuration."""
validate_all = True
extra = "forbid"
validate_assignment = True
def __init__(self, *items: t.Union[MultiModelT, t.Dict[str, t.Any]]) -> None: def __init__(self, *items: t.Union[MultiModelT, t.Dict[str, t.Any]]) -> None:
"""Validate items.""" """Validate items."""
for cls_var in ("model", "unique_by"): for cls_var in ("model", "unique_by"):
if getattr(self, cls_var, None) is None: if getattr(self, cls_var, None) is None:
raise AttributeError(f"MultiModel is missing class variable '{cls_var}'") raise AttributeError(f"MultiModel is missing class variable '{cls_var}'")
valid = self._valid_items(*items) valid = self._valid_items(*items)
super().__init__(__root__=valid) super().__init__(root=valid)
self._count = len(self.__root__) self._count = len(self.root)
def __init_subclass__(cls, **kw: t.Any) -> None: def __init_subclass__(cls, **kw: t.Any) -> None:
"""Add class variables from keyword arguments.""" """Add class variables from keyword arguments."""
model = kw.pop("model", None) model = kw.pop("model", None)
cls.model = model cls.model = model
cls.unique_by = kw.pop("unique_by", None) cls.unique_by = kw.pop("unique_by", None)
cls.model_name = getattr(model, "__name__", "MultiModel") cls._model_name = getattr(model, "__name__", "MultiModel")
super().__init_subclass__() super().__init_subclass__()
def __repr__(self) -> str: def __repr__(self) -> str:
"""Represent model.""" """Represent model."""
return repr_from_attrs(self, ["_count", "unique_by", "model_name"], strip="_") return repr_from_attrs(self, ["_count", "unique_by", "_model_name"], strip="_")
def __iter__(self) -> t.Iterator[MultiModelT]: def __iter__(self) -> t.Iterator[MultiModelT]:
"""Iterate items.""" """Iterate items."""
return iter(self.__root__) return iter(self.root)
def __getitem__(self, value: t.Union[int, str]) -> MultiModelT: def __getitem__(self, value: t.Union[int, str]) -> MultiModelT:
"""Get an item by its `unique_by` property.""" """Get an item by its `unique_by` property."""
@ -228,7 +217,7 @@ class MultiModel(GenericModel, t.Generic[MultiModelT]):
) )
) )
if isinstance(value, int): if isinstance(value, int):
return self.__root__[value] return self.root[value]
for item in self: for item in self:
if hasattr(item, self.unique_by) and getattr(item, self.unique_by) == value: if hasattr(item, self.unique_by) and getattr(item, self.unique_by) == value:
@ -265,7 +254,7 @@ class MultiModel(GenericModel, t.Generic[MultiModelT]):
def __len__(self) -> int: def __len__(self) -> int:
"""Get number of items.""" """Get number of items."""
return len(self.__root__) return len(self.root)
@property @property
def ids(self) -> t.Tuple[t.Any, ...]: def ids(self) -> t.Tuple[t.Any, ...]:
@ -283,7 +272,7 @@ class MultiModel(GenericModel, t.Generic[MultiModelT]):
new = type(name, (cls,), cls.__dict__) new = type(name, (cls,), cls.__dict__)
new.model = model new.model = model
new.unique_by = unique_by new.unique_by = unique_by
new.model_name = getattr(model, "__name__", "MultiModel") new._model_name = getattr(model, "__name__", "MultiModel")
return new return new
def _valid_items( def _valid_items(
@ -317,7 +306,7 @@ class MultiModel(GenericModel, t.Generic[MultiModelT]):
if getattr(o, unique_by) == v if getattr(o, unique_by) == v
} }
return tuple(unique_by_objects.values()) return tuple(unique_by_objects.values())
return (*self.__root__, *to_add) return (*self.root, *to_add)
def filter(self, *properties: str) -> MultiModelT: def filter(self, *properties: str) -> MultiModelT:
"""Get only items with `unique_by` properties matching values in `properties`.""" """Get only items with `unique_by` properties matching values in `properties`."""
@ -345,8 +334,8 @@ class MultiModel(GenericModel, t.Generic[MultiModelT]):
def add(self, *items, unique_by: t.Optional[str] = None) -> None: def add(self, *items, unique_by: t.Optional[str] = None) -> None:
"""Add an item to the model.""" """Add an item to the model."""
new = self._merge_with(*items, unique_by=unique_by) new = self._merge_with(*items, unique_by=unique_by)
self.__root__ = new self.root = new
self._count = len(self.__root__) self._count = len(self.root)
for item in new: for item in new:
log.debug( log.debug(
"Added {} '{!s}' to {}", "Added {} '{!s}' to {}",

View file

@ -1,9 +1,11 @@
"""Data Models for Parsing Arista JSON Response.""" """Data Models for Parsing Arista JSON Response."""
# Standard Library # Standard Library
from typing import Dict, List, Optional import typing as t
from datetime import datetime from datetime import datetime
from pydantic import ConfigDict
# Project # Project
from hyperglass.log import log from hyperglass.log import log
from hyperglass.models.data import BGPRouteTable from hyperglass.models.data import BGPRouteTable
@ -29,16 +31,14 @@ def _alias_generator(field: str) -> str:
class _AristaBase(HyperglassModel): class _AristaBase(HyperglassModel):
"""Base Model for Arista validation.""" """Base Model for Arista validation."""
class Config: model_config = ConfigDict(extra="ignore", alias_generator=_alias_generator)
extra = "ignore"
alias_generator = _alias_generator
class AristaAsPathEntry(_AristaBase): class AristaAsPathEntry(_AristaBase):
"""Validation model for Arista asPathEntry.""" """Validation model for Arista asPathEntry."""
as_path_type: str = "External" as_path_type: str = "External"
as_path: Optional[str] = "" as_path: t.Optional[str] = ""
class AristaPeerEntry(_AristaBase): class AristaPeerEntry(_AristaBase):
@ -55,18 +55,18 @@ class AristaRouteType(_AristaBase):
suppressed: bool suppressed: bool
valid: bool valid: bool
active: bool active: bool
origin_validity: Optional[str] = "notVerified" origin_validity: t.Optional[str] = "notVerified"
class AristaRouteDetail(_AristaBase): class AristaRouteDetail(_AristaBase):
"""Validation for Arista routeDetail.""" """Validation for Arista routeDetail."""
origin: str origin: str
label_stack: List = [] label_stack: t.List = []
ext_community_list: List[str] = [] ext_community_list: t.List[str] = []
ext_community_list_raw: List[str] = [] ext_community_list_raw: t.List[str] = []
community_list: List[str] = [] community_list: t.List[str] = []
large_community_list: List[str] = [] large_community_list: t.List[str] = []
class AristaRoutePath(_AristaBase): class AristaRoutePath(_AristaBase):
@ -81,16 +81,16 @@ class AristaRoutePath(_AristaBase):
timestamp: int = int(datetime.utcnow().timestamp()) timestamp: int = int(datetime.utcnow().timestamp())
next_hop: str next_hop: str
route_type: AristaRouteType route_type: AristaRouteType
route_detail: Optional[AristaRouteDetail] route_detail: t.Optional[AristaRouteDetail]
class AristaRouteEntry(_AristaBase): class AristaRouteEntry(_AristaBase):
"""Validation model for Arista bgpRouteEntries.""" """Validation model for Arista bgpRouteEntries."""
total_paths: int = 0 total_paths: int = 0
bgp_advertised_peer_groups: Dict = {} bgp_advertised_peer_groups: t.Dict = {}
mask_length: int mask_length: int
bgp_route_paths: List[AristaRoutePath] = [] bgp_route_paths: t.List[AristaRoutePath] = []
class AristaBGPTable(_AristaBase): class AristaBGPTable(_AristaBase):
@ -98,7 +98,7 @@ class AristaBGPTable(_AristaBase):
router_id: str router_id: str
vrf: str vrf: str
bgp_route_entries: Dict[str, AristaRouteEntry] bgp_route_entries: t.Dict[str, AristaRouteEntry]
# The raw value is really a string, but `int` will convert it. # The raw value is really a string, but `int` will convert it.
asn: int asn: int
@ -109,7 +109,7 @@ class AristaBGPTable(_AristaBase):
return now_timestamp - timestamp return now_timestamp - timestamp
@staticmethod @staticmethod
def _get_as_path(as_path: str) -> List[str]: def _get_as_path(as_path: str) -> t.List[str]:
if as_path == "": if as_path == "":
return [] return []
return [int(p) for p in as_path.split() if p.isdecimal()] return [int(p) for p in as_path.split() if p.isdecimal()]
@ -119,11 +119,9 @@ class AristaBGPTable(_AristaBase):
routes = [] routes = []
count = 0 count = 0
for prefix, entries in self.bgp_route_entries.items(): for prefix, entries in self.bgp_route_entries.items():
count += entries.total_paths count += entries.total_paths
for route in entries.bgp_route_paths: for route in entries.bgp_route_paths:
as_path = self._get_as_path(route.as_path_entry.as_path) as_path = self._get_as_path(route.as_path_entry.as_path)
rpki_state = RPKI_STATE_MAP.get(route.route_type.origin_validity, 3) rpki_state = RPKI_STATE_MAP.get(route.route_type.origin_validity, 3)

View file

@ -1,11 +1,11 @@
"""Data Models for Parsing FRRouting JSON Response.""" """Data Models for Parsing FRRouting JSON Response."""
# Standard Library # Standard Library
from typing import List import typing as t
from datetime import datetime from datetime import datetime
# Third Party # Third Party
from pydantic import StrictInt, StrictStr, StrictBool, constr, root_validator from pydantic import model_validator, ConfigDict
# Project # Project
from hyperglass.log import log from hyperglass.log import log
@ -14,7 +14,7 @@ from hyperglass.models.data import BGPRouteTable
# Local # Local
from ..main import HyperglassModel from ..main import HyperglassModel
FRRPeerType = constr(regex=r"(internal|external)") FRRPeerType = t.Literal["internal", "external"]
def _alias_generator(field): def _alias_generator(field):
@ -23,46 +23,44 @@ def _alias_generator(field):
class _FRRBase(HyperglassModel): class _FRRBase(HyperglassModel):
class Config: model_config = ConfigDict(alias_generator=_alias_generator, extra="ignore")
alias_generator = _alias_generator
extra = "ignore"
class FRRNextHop(_FRRBase): class FRRNextHop(_FRRBase):
"""FRR Next Hop Model.""" """FRR Next Hop Model."""
ip: StrictStr ip: str
afi: StrictStr afi: str
metric: StrictInt metric: int
accessible: StrictBool accessible: bool
used: StrictBool used: bool
class FRRPeer(_FRRBase): class FRRPeer(_FRRBase):
"""FRR Peer Model.""" """FRR Peer Model."""
peer_id: StrictStr peer_id: str
router_id: StrictStr router_id: str
type: FRRPeerType type: FRRPeerType
class FRRPath(_FRRBase): class FRRPath(_FRRBase):
"""FRR Path Model.""" """FRR Path Model."""
aspath: List[StrictInt] aspath: t.List[int]
aggregator_as: StrictInt aggregator_as: int
aggregator_id: StrictStr aggregator_id: str
med: StrictInt = 0 med: int = 0
localpref: StrictInt localpref: int
weight: StrictInt weight: int
valid: StrictBool valid: bool
last_update: StrictInt last_update: int
bestpath: StrictBool bestpath: bool
community: List[StrictStr] community: t.List[str]
nexthops: List[FRRNextHop] nexthops: t.List[FRRNextHop]
peer: FRRPeer peer: FRRPeer
@root_validator(pre=True) @model_validator(pre=True)
def validate_path(cls, values): def validate_path(cls, values):
"""Extract meaningful data from FRR response.""" """Extract meaningful data from FRR response."""
new = values.copy() new = values.copy()
@ -77,8 +75,8 @@ class FRRPath(_FRRBase):
class FRRRoute(_FRRBase): class FRRRoute(_FRRBase):
"""FRR Route Model.""" """FRR Route Model."""
prefix: StrictStr prefix: str
paths: List[FRRPath] = [] paths: t.List[FRRPath] = []
def serialize(self): def serialize(self):
"""Convert the FRR-specific fields to standard parsed data model.""" """Convert the FRR-specific fields to standard parsed data model."""

View file

@ -1,11 +1,10 @@
"""Data Models for Parsing Juniper XML Response.""" """Data Models for Parsing Juniper XML Response."""
# Standard Library # Standard Library
from typing import Any, Dict, List import typing as t
# Third Party # Third Party
from pydantic import validator, root_validator from pydantic import field_validator, model_validator, ConfigDict
from pydantic.types import StrictInt, StrictStr, StrictBool
# Project # Project
from hyperglass.log import log from hyperglass.log import log
@ -26,7 +25,7 @@ RPKI_STATE_MAP = {
class JuniperBase(HyperglassModel, extra="ignore"): class JuniperBase(HyperglassModel, extra="ignore"):
"""Base Juniper model.""" """Base Juniper model."""
def __init__(self, **kwargs: Any) -> None: def __init__(self, **kwargs: t.Any) -> None:
"""Convert all `-` keys to `_`. """Convert all `-` keys to `_`.
Default camelCase alias generator will still be used. Default camelCase alias generator will still be used.
@ -38,22 +37,24 @@ class JuniperBase(HyperglassModel, extra="ignore"):
class JuniperRouteTableEntry(JuniperBase): class JuniperRouteTableEntry(JuniperBase):
"""Parse Juniper rt-entry data.""" """Parse Juniper rt-entry data."""
active_tag: StrictBool model_config = ConfigDict(validate_assignment=False)
active_tag: bool
preference: int preference: int
age: StrictInt age: int
local_preference: int local_preference: int
metric: int = 0 metric: int = 0
as_path: List[StrictInt] = [] as_path: t.List[int] = []
validation_state: StrictInt = 3 validation_state: int = 3
next_hop: StrictStr next_hop: str
peer_rid: StrictStr peer_rid: str
peer_as: int peer_as: int
source_as: int source_as: int
source_rid: StrictStr source_rid: str
communities: List[StrictStr] = None communities: t.List[str] = None
@root_validator(pre=True) @model_validator(mode="before")
def validate_optional_flags(cls, values): def validate_optional_flags(cls, values: t.Dict[str, t.Any]):
"""Flatten & rename keys prior to validation.""" """Flatten & rename keys prior to validation."""
next_hops = [] next_hops = []
nh = None nh = None
@ -67,7 +68,7 @@ class JuniperRouteTableEntry(JuniperBase):
nh = values.pop("protocol_nh") nh = values.pop("protocol_nh")
# Force the next hops to be a list # Force the next hops to be a list
if isinstance(nh, Dict): if isinstance(nh, t.Dict):
nh = [nh] nh = [nh]
if nh is not None: if nh is not None:
@ -94,12 +95,12 @@ class JuniperRouteTableEntry(JuniperBase):
return values return values
@validator("validation_state", pre=True, always=True) @field_validator("validation_state", mode="before")
def validate_rpki_state(cls, value): def validate_rpki_state(cls, value):
"""Convert string RPKI state to standard integer mapping.""" """Convert string RPKI state to standard integer mapping."""
return RPKI_STATE_MAP.get(value, 3) return RPKI_STATE_MAP.get(value, 3)
@validator("active_tag", pre=True, always=True) @field_validator("active_tag", mode="before")
def validate_active_tag(cls, value): def validate_active_tag(cls, value):
"""Convert active-tag from string/null to boolean.""" """Convert active-tag from string/null to boolean."""
if value == "*": if value == "*":
@ -108,7 +109,7 @@ class JuniperRouteTableEntry(JuniperBase):
value = False value = False
return value return value
@validator("age", pre=True, always=True) @field_validator("age", mode="before")
def validate_age(cls, value): def validate_age(cls, value):
"""Get age as seconds.""" """Get age as seconds."""
if not isinstance(value, dict): if not isinstance(value, dict):
@ -120,13 +121,13 @@ class JuniperRouteTableEntry(JuniperBase):
value = value.get("@junos:seconds", 0) value = value.get("@junos:seconds", 0)
return int(value) return int(value)
@validator("as_path", pre=True, always=True) @field_validator("as_path", mode="before")
def validate_as_path(cls, value): def validate_as_path(cls, value):
"""Remove origin flags from AS_PATH.""" """Remove origin flags from AS_PATH."""
disallowed = ("E", "I", "?") disallowed = ("E", "I", "?")
return [int(a) for a in value.split() if a not in disallowed] return [int(a) for a in value.split() if a not in disallowed]
@validator("communities", pre=True, always=True) @field_validator("communities", mode="before")
def validate_communities(cls, value): def validate_communities(cls, value):
"""Flatten community list.""" """Flatten community list."""
if value is not None: if value is not None:
@ -139,13 +140,13 @@ class JuniperRouteTableEntry(JuniperBase):
class JuniperRouteTable(JuniperBase): class JuniperRouteTable(JuniperBase):
"""Validation model for Juniper rt data.""" """Validation model for Juniper rt data."""
rt_destination: StrictStr rt_destination: str
rt_prefix_length: int rt_prefix_length: int
rt_entry_count: int rt_entry_count: int
rt_announced_count: int rt_announced_count: int
rt_entry: List[JuniperRouteTableEntry] rt_entry: t.List[JuniperRouteTableEntry]
@validator("rt_entry_count", pre=True, always=True) @field_validator("rt_entry_count", mode="before")
def validate_entry_count(cls, value): def validate_entry_count(cls, value):
"""Flatten & convert entry-count to integer.""" """Flatten & convert entry-count to integer."""
return int(value.get("#text")) return int(value.get("#text"))
@ -154,12 +155,12 @@ class JuniperRouteTable(JuniperBase):
class JuniperBGPTable(JuniperBase): class JuniperBGPTable(JuniperBase):
"""Validation model for route-table data.""" """Validation model for route-table data."""
table_name: StrictStr table_name: str
destination_count: int destination_count: int
total_route_count: int total_route_count: int
active_route_count: int active_route_count: int
hidden_route_count: int hidden_route_count: int
rt: List[JuniperRouteTable] rt: t.List[JuniperRouteTable]
def bgp_table(self: "JuniperBGPTable") -> "BGPRouteTable": def bgp_table(self: "JuniperBGPTable") -> "BGPRouteTable":
"""Convert the Juniper-specific fields to standard parsed data model.""" """Convert the Juniper-specific fields to standard parsed data model."""

View file

@ -10,12 +10,14 @@ from pydantic import (
FilePath, FilePath,
RedisDsn, RedisDsn,
SecretStr, SecretStr,
BaseSettings,
DirectoryPath, DirectoryPath,
IPvAnyAddress, IPvAnyAddress,
validator, field_validator,
ValidationInfo,
) )
from pydantic_settings import BaseSettings, SettingsConfigDict
# Project # Project
from hyperglass.util import at_least, cpu_count from hyperglass.util import at_least, cpu_count
@ -31,11 +33,7 @@ _default_app_path = Path("/etc/hyperglass")
class HyperglassSettings(BaseSettings): class HyperglassSettings(BaseSettings):
"""hyperglass system settings, required to start hyperglass.""" """hyperglass system settings, required to start hyperglass."""
class Config: model_config = SettingsConfigDict(env_prefix="hyperglass_")
"""hyperglass system settings configuration."""
env_prefix = "hyperglass_"
underscore_attrs_are_private = True
config_file_names: t.ClassVar[t.Tuple[str, ...]] = ("config", "devices", "directives") config_file_names: t.ClassVar[t.Tuple[str, ...]] = ("config", "devices", "directives")
default_app_path: t.ClassVar[Path] = _default_app_path default_app_path: t.ClassVar[Path] = _default_app_path
@ -46,12 +44,12 @@ class HyperglassSettings(BaseSettings):
disable_ui: bool = False disable_ui: bool = False
app_path: DirectoryPath = _default_app_path app_path: DirectoryPath = _default_app_path
redis_host: str = "localhost" redis_host: str = "localhost"
redis_password: t.Optional[SecretStr] redis_password: t.Optional[SecretStr] = None
redis_db: int = 1 redis_db: int = 1
redis_dsn: RedisDsn = None redis_dsn: RedisDsn = None
host: IPvAnyAddress = None host: IPvAnyAddress = None
port: int = 8001 port: int = 8001
ca_cert: t.Optional[FilePath] ca_cert: t.Optional[FilePath] = None
container: bool = False container: bool = False
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
@ -88,16 +86,16 @@ class HyperglassSettings(BaseSettings):
yield Panel.fit(table, title="hyperglass settings", border_style="subtle") yield Panel.fit(table, title="hyperglass settings", border_style="subtle")
@validator("host", pre=True, always=True) @field_validator("host", mode="before")
def validate_host( def validate_host(
cls: "HyperglassSettings", value: t.Any, values: t.Dict[str, t.Any] cls: "HyperglassSettings", value: t.Any, info: ValidationInfo
) -> IPvAnyAddress: ) -> IPvAnyAddress:
"""Set default host based on debug mode.""" """Set default host based on debug mode."""
if value is None: if value is None:
if values["debug"] is False: if info.data.get("debug") is False:
return ip_address("::1") return ip_address("::1")
if values["debug"] is True: if info.data.get("debug") is True:
return ip_address("::") return ip_address("::")
if isinstance(value, str): if isinstance(value, str):
@ -112,20 +110,16 @@ class HyperglassSettings(BaseSettings):
raise ValueError(str(value)) raise ValueError(str(value))
@validator("redis_dsn", always=True) @field_validator("redis_dsn", mode="before")
def validate_redis_dsn( def validate_redis_dsn(cls, value: t.Any, info: ValidationInfo) -> RedisDsn:
cls: "HyperglassSettings", value: t.Any, values: t.Dict[str, t.Any]
) -> RedisDsn:
"""Construct a Redis DSN if none is provided.""" """Construct a Redis DSN if none is provided."""
if value is None: if value is None:
dsn = "redis://{}/{!s}".format(values["redis_host"], values["redis_db"]) host = info.data.get("redis_host")
password = values.get("redis_password") db = info.data.get("redis_db")
dsn = "redis://{}/{!s}".format(host, db)
password = info.data.get("redis_password")
if password is not None: if password is not None:
dsn = "redis://:{}@{}/{!s}".format( dsn = "redis://:{}@{}/{!s}".format(password.get_secret_value(), host, db)
password.get_secret_value(),
values["redis_host"],
values["redis_db"],
)
return dsn return dsn
return value return value

View file

@ -1,10 +1,7 @@
"""UI Configuration models.""" """UI Configuration models."""
# Standard Library # Standard Library
from typing import Any, Dict, List, Tuple, Union, Literal, Optional import typing as t
# Third Party
from pydantic import StrictStr, StrictBool
# Local # Local
from .main import HyperglassModel from .main import HyperglassModel
@ -13,45 +10,45 @@ from .config.cache import CachePublic
from .config.params import ParamsPublic from .config.params import ParamsPublic
from .config.messages import Messages from .config.messages import Messages
Alignment = Union[Literal["left"], Literal["center"], Literal["right"], None] Alignment = t.Union[t.Literal["left"], t.Literal["center"], t.Literal["right"], None]
StructuredDataField = Tuple[str, str, Alignment] StructuredDataField = t.Tuple[str, str, Alignment]
class UIDirective(HyperglassModel): class UIDirective(HyperglassModel):
"""UI: Directive.""" """UI: Directive."""
id: StrictStr id: str
name: StrictStr name: str
field_type: StrictStr field_type: str
groups: List[StrictStr] groups: t.List[str]
description: StrictStr description: str
info: Optional[str] = None info: t.Optional[str] = None
options: Optional[List[Dict[str, Any]]] options: t.Optional[t.List[t.Dict[str, t.Any]]] = None
class UILocation(HyperglassModel): class UILocation(HyperglassModel):
"""UI: Location (Device).""" """UI: Location (Device)."""
id: StrictStr id: str
name: StrictStr name: str
group: Optional[StrictStr] group: t.Optional[str] = None
avatar: Optional[StrictStr] avatar: t.Optional[str] = None
description: Optional[StrictStr] description: t.Optional[str] = None
directives: List[UIDirective] = [] directives: t.List[UIDirective] = []
class UIDevices(HyperglassModel): class UIDevices(HyperglassModel):
"""UI: Devices.""" """UI: Devices."""
group: Optional[StrictStr] group: t.Optional[str] = None
locations: List[UILocation] = [] locations: t.List[UILocation] = []
class UIContent(HyperglassModel): class UIContent(HyperglassModel):
"""UI: Content.""" """UI: Content."""
credit: StrictStr credit: str
greeting: StrictStr greeting: str
class UIParameters(ParamsPublic, HyperglassModel): class UIParameters(ParamsPublic, HyperglassModel):
@ -60,8 +57,8 @@ class UIParameters(ParamsPublic, HyperglassModel):
cache: CachePublic cache: CachePublic
web: WebPublic web: WebPublic
messages: Messages messages: Messages
version: StrictStr version: str
devices: List[UIDevices] = [] devices: t.List[UIDevices] = []
parsed_data_fields: Tuple[StructuredDataField, ...] parsed_data_fields: t.Tuple[StructuredDataField, ...]
content: UIContent content: UIContent
developer_mode: StrictBool developer_mode: bool

View file

@ -1,7 +1,7 @@
"""Model utilities.""" """Model utilities."""
# Standard Library # Standard Library
from typing import Any, Dict, Tuple import typing as t
# Third Party # Third Party
from pydantic import BaseModel from pydantic import BaseModel
@ -34,7 +34,7 @@ class LegacyField(BaseModel):
required: bool = True required: bool = True
LEGACY_FIELDS: Dict[str, Tuple[LegacyField, ...]] = { LEGACY_FIELDS: t.Dict[str, t.Tuple[LegacyField, ...]] = {
"Device": ( "Device": (
LegacyField(old="nos", new="platform", overwrite=True), LegacyField(old="nos", new="platform", overwrite=True),
LegacyField(old="network", new="group", overwrite=False, required=False), LegacyField(old="network", new="group", overwrite=False, required=False),
@ -43,7 +43,7 @@ LEGACY_FIELDS: Dict[str, Tuple[LegacyField, ...]] = {
} }
def check_legacy_fields(*, model: str, data: Dict[str, Any]) -> Dict[str, Any]: def check_legacy_fields(*, model: str, data: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
"""Check for legacy fields prior to model initialization.""" """Check for legacy fields prior to model initialization."""
if model in LEGACY_FIELDS: if model in LEGACY_FIELDS:
for field in LEGACY_FIELDS[model]: for field in LEGACY_FIELDS[model]:

View file

@ -5,7 +5,7 @@ import typing as t
from datetime import datetime from datetime import datetime
# Third Party # Third Party
from pydantic import StrictStr, root_validator from pydantic import model_validator, ConfigDict
# Project # Project
from hyperglass.log import log from hyperglass.log import log
@ -17,35 +17,33 @@ _WEBHOOK_TITLE = "hyperglass received a valid query with the following data"
_ICON_URL = "https://res.cloudinary.com/hyperglass/image/upload/v1593192484/icon.png" _ICON_URL = "https://res.cloudinary.com/hyperglass/image/upload/v1593192484/icon.png"
def to_snake_case(value: str) -> str:
"""Convert string to snake case."""
return value.replace("_", "-")
class WebhookHeaders(HyperglassModel): class WebhookHeaders(HyperglassModel):
"""Webhook data model.""" """Webhook data model."""
user_agent: t.Optional[StrictStr] model_config = ConfigDict(alias_generator=to_snake_case)
referer: t.Optional[StrictStr]
accept_encoding: t.Optional[StrictStr]
accept_language: t.Optional[StrictStr]
x_real_ip: t.Optional[StrictStr]
x_forwarded_for: t.Optional[StrictStr]
class Config: user_agent: t.Optional[str] = None
"""Pydantic model config.""" referer: t.Optional[str] = None
accept_encoding: t.Optional[str] = None
fields = { accept_language: t.Optional[str] = None
"user_agent": "user-agent", x_real_ip: t.Optional[str] = None
"accept_encoding": "accept-encoding", x_forwarded_for: t.Optional[str] = None
"accept_language": "accept-language",
"x_real_ip": "x-real-ip",
"x_forwarded_for": "x-forwarded-for",
}
class WebhookNetwork(HyperglassModel, extra="allow"): class WebhookNetwork(HyperglassModel):
"""Webhook data model.""" """Webhook data model."""
prefix: StrictStr = "Unknown" model_config = ConfigDict(extra="allow")
asn: StrictStr = "Unknown"
org: StrictStr = "Unknown" prefix: str = "Unknown"
country: StrictStr = "Unknown" asn: str = "Unknown"
org: str = "Unknown"
country: str = "Unknown"
class Webhook(HyperglassModel): class Webhook(HyperglassModel):
@ -55,26 +53,26 @@ class Webhook(HyperglassModel):
query_type: str query_type: str
query_target: t.Union[t.List[str], str] query_target: t.Union[t.List[str], str]
headers: WebhookHeaders headers: WebhookHeaders
source: StrictStr = "Unknown" source: str = "Unknown"
network: WebhookNetwork network: WebhookNetwork
timestamp: datetime timestamp: datetime
@root_validator(pre=True) @model_validator(mode="before")
def validate_webhook(cls, values): def validate_webhook(cls, model: "Webhook") -> "Webhook":
"""Reset network attributes if the source is localhost.""" """Reset network attributes if the source is localhost."""
if values.get("source") in ("127.0.0.1", "::1"): if model.source in ("127.0.0.1", "::1"):
values["network"] = {} model.network = {}
return values return model
def msteams(self): def msteams(self) -> t.Dict[str, t.Any]:
"""Format the webhook data as a Microsoft Teams card.""" """Format the webhook data as a Microsoft Teams card."""
def code(value: t.Any): def code(value: t.Any) -> str:
"""Wrap argument in backticks for markdown inline code formatting.""" """Wrap argument in backticks for markdown inline code formatting."""
return f"`{str(value)}`" return f"`{str(value)}`"
header_data = [ header_data = [
{"name": k, "value": code(v)} for k, v in self.headers.dict(by_alias=True).items() {"name": k, "value": code(v)} for k, v in self.headers.model_dump(by_alias=True).items()
] ]
time_fmt = self.timestamp.strftime("%Y %m %d %H:%M:%S") time_fmt = self.timestamp.strftime("%Y %m %d %H:%M:%S")
payload = { payload = {
@ -114,7 +112,7 @@ class Webhook(HyperglassModel):
return payload return payload
def slack(self): def slack(self) -> t.Dict[str, t.Any]:
"""Format the webhook data as a Slack message.""" """Format the webhook data as a Slack message."""
def make_field(key, value, code=False): def make_field(key, value, code=False):
@ -123,7 +121,7 @@ class Webhook(HyperglassModel):
return f"*{key}*\n{value}" return f"*{key}*\n{value}"
header_data = [] header_data = []
for k, v in self.headers.dict(by_alias=True).items(): for k, v in self.headers.model_dump(by_alias=True).items():
field = make_field(k, v, code=True) field = make_field(k, v, code=True)
header_data.append(field) header_data.append(field)

View file

@ -22,7 +22,7 @@ SupportedMethod = t.TypeVar("SupportedMethod")
class HyperglassPlugin(BaseModel, ABC): class HyperglassPlugin(BaseModel, ABC):
"""Plugin to interact with device command output.""" """Plugin to interact with device command output."""
__hyperglass_builtin__: bool = PrivateAttr(False) _hyperglass_builtin: bool = PrivateAttr(False)
_type: t.ClassVar[str] _type: t.ClassVar[str]
name: str name: str
common: bool = False common: bool = False
@ -76,7 +76,7 @@ class HyperglassPlugin(BaseModel, ABC):
table = Table.grid(padding=(0, 1), expand=False) table = Table.grid(padding=(0, 1), expand=False)
table.add_column(justify="right") table.add_column(justify="right")
data = {"builtin": True if self.__hyperglass_builtin__ else False} data = {"builtin": True if self._hyperglass_builtin else False}
data.update( data.update(
{ {
attr: getattr(self, attr) attr: getattr(self, attr)

View file

@ -93,7 +93,7 @@ def validate_large_community(value: str) -> bool:
class ValidateBGPCommunity(InputPlugin): class ValidateBGPCommunity(InputPlugin):
"""Validate a BGP community string.""" """Validate a BGP community string."""
__hyperglass_builtin__: bool = PrivateAttr(True) _hyperglass_builtin: bool = PrivateAttr(True)
def validate(self, query: "Query") -> "InputPluginValidationReturn": def validate(self, query: "Query") -> "InputPluginValidationReturn":
"""Ensure an input query target is a valid BGP community.""" """Ensure an input query target is a valid BGP community."""

View file

@ -29,7 +29,6 @@ def parse_arista(output: t.Sequence[str]) -> "OutputDataModel":
result = None result = None
for response in output: for response in output:
try: try:
parsed: t.Dict = json.loads(response) parsed: t.Dict = json.loads(response)
@ -68,7 +67,7 @@ def parse_arista(output: t.Sequence[str]) -> "OutputDataModel":
class BGPRoutePluginArista(OutputPlugin): class BGPRoutePluginArista(OutputPlugin):
"""Coerce a Arista route table in JSON format to a standard BGP Table structure.""" """Coerce a Arista route table in JSON format to a standard BGP Table structure."""
__hyperglass_builtin__: bool = PrivateAttr(True) _hyperglass_builtin: bool = PrivateAttr(True)
platforms: t.Sequence[str] = ("arista_eos",) platforms: t.Sequence[str] = ("arista_eos",)
directives: t.Sequence[str] = ( directives: t.Sequence[str] = (
"__hyperglass_arista_eos_bgp_route_table__", "__hyperglass_arista_eos_bgp_route_table__",

View file

@ -119,7 +119,7 @@ def parse_juniper(output: Sequence[str]) -> "OutputDataModel": # noqa: C901
class BGPRoutePluginJuniper(OutputPlugin): class BGPRoutePluginJuniper(OutputPlugin):
"""Coerce a Juniper route table in XML format to a standard BGP Table structure.""" """Coerce a Juniper route table in XML format to a standard BGP Table structure."""
__hyperglass_builtin__: bool = PrivateAttr(True) _hyperglass_builtin: bool = PrivateAttr(True)
platforms: Sequence[str] = ("juniper",) platforms: Sequence[str] = ("juniper",)
directives: Sequence[str] = ( directives: Sequence[str] = (
"__hyperglass_juniper_bgp_route_table__", "__hyperglass_juniper_bgp_route_table__",

View file

@ -21,7 +21,7 @@ if t.TYPE_CHECKING:
class MikrotikGarbageOutput(OutputPlugin): class MikrotikGarbageOutput(OutputPlugin):
"""Parse Mikrotik output to remove garbage.""" """Parse Mikrotik output to remove garbage."""
__hyperglass_builtin__: bool = PrivateAttr(True) _hyperglass_builtin: bool = PrivateAttr(True)
platforms: t.Sequence[str] = ("mikrotik_routeros", "mikrotik_switchos") platforms: t.Sequence[str] = ("mikrotik_routeros", "mikrotik_switchos")
directives: t.Sequence[str] = ( directives: t.Sequence[str] = (
"__hyperglass_mikrotik_bgp_aspath__", "__hyperglass_mikrotik_bgp_aspath__",
@ -37,7 +37,6 @@ class MikrotikGarbageOutput(OutputPlugin):
result = () result = ()
for each_output in output: for each_output in output:
if each_output.split()[-1] in ("DISTANCE", "STATUS"): if each_output.split()[-1] in ("DISTANCE", "STATUS"):
# Mikrotik shows the columns with no rows if there is no data. # Mikrotik shows the columns with no rows if there is no data.
# Rather than send back an empty table, send back an empty # Rather than send back an empty table, send back an empty

View file

@ -20,7 +20,7 @@ if TYPE_CHECKING:
class RemoveCommand(OutputPlugin): class RemoveCommand(OutputPlugin):
"""Remove anything before the command if found in output.""" """Remove anything before the command if found in output."""
__hyperglass_builtin__: bool = PrivateAttr(True) _hyperglass_builtin: bool = PrivateAttr(True)
def process(self, *, output: OutputType, query: "Query") -> Sequence[str]: def process(self, *, output: OutputType, query: "Query") -> Sequence[str]:
"""Remove anything before the command if found in output.""" """Remove anything before the command if found in output."""

View file

@ -61,7 +61,7 @@ class PluginManager(t.Generic[PluginT]):
plugins = self._state.plugins(self._type) plugins = self._state.plugins(self._type)
if builtins is False: if builtins is False:
plugins = [p for p in plugins if p.__hyperglass_builtin__ is False] plugins = [p for p in plugins if p._hyperglass_builtin is False]
# Sort plugins by their name attribute, which is the name of the class by default. # Sort plugins by their name attribute, which is the name of the class by default.
sorted_by_name = sorted(plugins, key=lambda p: str(p)) sorted_by_name = sorted(plugins, key=lambda p: str(p))
@ -69,7 +69,7 @@ class PluginManager(t.Generic[PluginT]):
# Sort with built-in plugins last. # Sort with built-in plugins last.
return sorted( return sorted(
sorted_by_name, sorted_by_name,
key=lambda p: -1 if p.__hyperglass_builtin__ else 1, key=lambda p: -1 if p._hyperglass_builtin else 1,
reverse=True, reverse=True,
) )
@ -111,7 +111,7 @@ class PluginManager(t.Generic[PluginT]):
if issubclass(plugin, HyperglassPlugin): if issubclass(plugin, HyperglassPlugin):
instance = plugin(*args, **kwargs) instance = plugin(*args, **kwargs)
self._state.add_plugin(self._type, instance) self._state.add_plugin(self._type, instance)
if instance.__hyperglass_builtin__ is True: if instance._hyperglass_builtin is True:
log.debug("Registered {} built-in plugin {!r}", self._type, instance.name) log.debug("Registered {} built-in plugin {!r}", self._type, instance.name)
else: else:
log.success("Registered {} plugin {!r}", self._type, instance.name) log.success("Registered {} plugin {!r}", self._type, instance.name)
@ -182,7 +182,6 @@ class OutputPluginManager(PluginManager[OutputPlugin], type="output"):
) )
common = (plugin for plugin in self.plugins() if plugin.common is True) common = (plugin for plugin in self.plugins() if plugin.common is True)
for plugin in (*directives, *common): for plugin in (*directives, *common):
log.debug("Output Plugin {!r} starting with\n{!r}", plugin.name, result) log.debug("Output Plugin {!r} starting with\n{!r}", plugin.name, result)
result = plugin.process(output=result, query=query) result = plugin.process(output=result, query=query)
log.debug("Output Plugin {!r} completed with\n{!r}", plugin.name, result) log.debug("Output Plugin {!r} completed with\n{!r}", plugin.name, result)

View file

@ -0,0 +1,6 @@
from hyperglass.models.config.devices import Device
class MockDevice(Device):
def has_directives(self, *_: str) -> bool:
return True

View file

@ -13,6 +13,7 @@ from hyperglass.models.data.bgp_route import BGPRouteTable
# Local # Local
from .._builtin.bgp_route_arista import BGPRoutePluginArista from .._builtin.bgp_route_arista import BGPRoutePluginArista
from ._fixtures import MockDevice
DEPENDS_KWARGS = { DEPENDS_KWARGS = {
"depends": [ "depends": [
@ -28,20 +29,17 @@ SAMPLE = Path(__file__).parent.parent.parent.parent / ".samples" / "arista_route
def _tester(sample: str): def _tester(sample: str):
plugin = BGPRoutePluginArista() plugin = BGPRoutePluginArista()
device = Device( device = MockDevice(
name="Test Device", name="Test Device",
address="127.0.0.1", address="127.0.0.1",
group="Test Network", group="Test Network",
credential={"username": "", "password": ""}, credential={"username": "", "password": ""},
platform="arista", platform="arista",
structured_output=True, structured_output=True,
directives=[], directives=["__hyperglass_arista_eos_bgp_route_table__"],
attrs={"source4": "192.0.2.1", "source6": "2001:db8::1"}, attrs={"source4": "192.0.2.1", "source6": "2001:db8::1"},
) )
# Override has_directives method for testing.
device.has_directives = lambda *x: True
query = type("Query", (), {"device": device}) query = type("Query", (), {"device": device})
result = plugin.process(output=(sample,), query=query) result = plugin.process(output=(sample,), query=query)

View file

@ -10,11 +10,11 @@ import pytest
# Project # Project
from hyperglass.log import log from hyperglass.log import log
from hyperglass.models.api.query import Query from hyperglass.models.api.query import Query
from hyperglass.models.config.devices import Device
from hyperglass.models.data.bgp_route import BGPRouteTable from hyperglass.models.data.bgp_route import BGPRouteTable
# Local # Local
from .._builtin.bgp_route_juniper import BGPRoutePluginJuniper from .._builtin.bgp_route_juniper import BGPRoutePluginJuniper
from ._fixtures import MockDevice
DEPENDS_KWARGS = { DEPENDS_KWARGS = {
"depends": [ "depends": [
@ -32,7 +32,7 @@ AS_PATH = Path(__file__).parent.parent.parent.parent / ".samples" / "juniper_rou
def _tester(sample: str): def _tester(sample: str):
plugin = BGPRoutePluginJuniper() plugin = BGPRoutePluginJuniper()
device = Device( device = MockDevice(
name="Test Device", name="Test Device",
address="127.0.0.1", address="127.0.0.1",
group="Test Network", group="Test Network",
@ -43,9 +43,6 @@ def _tester(sample: str):
attrs={"source4": "192.0.2.1", "source6": "2001:db8::1"}, attrs={"source4": "192.0.2.1", "source6": "2001:db8::1"},
) )
# Override has_directives method for testing.
device.has_directives = lambda *x: True
query = type("Query", (), {"device": device}) query = type("Query", (), {"device": device})
result = plugin.process(output=(sample,), query=query) result = plugin.process(output=(sample,), query=query)

View file

@ -21,8 +21,6 @@ from .system_info import cpu_count, check_python, get_system_info, get_node_vers
__all__ = ( __all__ = (
"at_least", "at_least",
# "build_frontend",
# "build_ui",
"check_path", "check_path",
"check_python", "check_python",
"compare_dicts", "compare_dicts",

View file

@ -11,16 +11,16 @@ dependencies = [
"PyYAML>=6.0", "PyYAML>=6.0",
"aiofiles>=23.2.1", "aiofiles>=23.2.1",
"distro==1.8.0", "distro==1.8.0",
"fastapi==0.95.1", "fastapi>=0.110.0",
"favicons==0.2.2", "favicons==0.2.2",
"gunicorn==20.1.0", "gunicorn>=21.2.0",
"httpx==0.24.0", "httpx==0.24.0",
"loguru==0.7.0", "loguru==0.7.0",
"netmiko==4.1.2", "netmiko==4.1.2",
"paramiko==3.4.0", "paramiko==3.4.0",
"psutil==5.9.4", "psutil==5.9.4",
"py-cpuinfo==9.0.0", "py-cpuinfo==9.0.0",
"pydantic==1.10.14", "pydantic>=2.6.3",
"redis==4.5.4", "redis==4.5.4",
"rich>=13.7.0", "rich>=13.7.0",
"typer>=0.9.0", "typer>=0.9.0",
@ -28,6 +28,8 @@ dependencies = [
"uvloop>=0.17.0", "uvloop>=0.17.0",
"xmltodict==0.13.0", "xmltodict==0.13.0",
"toml>=0.10.2", "toml>=0.10.2",
"pydantic-settings>=2.2.1",
"pydantic-extra-types>=2.6.0",
] ]
readme = "README.md" readme = "README.md"
requires-python = ">= 3.11" requires-python = ">= 3.11"

View file

@ -10,6 +10,8 @@
-e file:. -e file:.
aiofiles==23.2.1 aiofiles==23.2.1
# via hyperglass # via hyperglass
annotated-types==0.6.0
# via pydantic
anyio==4.3.0 anyio==4.3.0
# via httpcore # via httpcore
# via starlette # via starlette
@ -41,7 +43,7 @@ distlib==0.3.8
# via virtualenv # via virtualenv
distro==1.8.0 distro==1.8.0
# via hyperglass # via hyperglass
fastapi==0.95.1 fastapi==0.110.0
# via hyperglass # via hyperglass
favicons==0.2.2 favicons==0.2.2
# via hyperglass # via hyperglass
@ -53,7 +55,7 @@ freetype-py==2.4.0
# via rlpycairo # via rlpycairo
future==0.18.3 future==0.18.3
# via textfsm # via textfsm
gunicorn==20.1.0 gunicorn==21.2.0
# via hyperglass # via hyperglass
h11==0.14.0 h11==0.14.0
# via httpcore # via httpcore
@ -90,6 +92,7 @@ ntc-templates==4.3.0
# via netmiko # via netmiko
packaging==23.2 packaging==23.2
# via black # via black
# via gunicorn
# via pytest # via pytest
paramiko==3.4.0 paramiko==3.4.0
# via hyperglass # via hyperglass
@ -121,9 +124,17 @@ pycodestyle==2.11.1
# via flake8 # via flake8
pycparser==2.21 pycparser==2.21
# via cffi # via cffi
pydantic==1.10.14 pydantic==2.6.3
# via fastapi # via fastapi
# via hyperglass # via hyperglass
# via pydantic-extra-types
# via pydantic-settings
pydantic-core==2.16.3
# via pydantic
pydantic-extra-types==2.6.0
# via hyperglass
pydantic-settings==2.2.1
# via hyperglass
pyflakes==3.2.0 pyflakes==3.2.0
# via flake8 # via flake8
pygments==2.17.2 pygments==2.17.2
@ -139,6 +150,8 @@ pytest==8.0.1
# via pytest-dependency # via pytest-dependency
pytest-asyncio==0.23.5 pytest-asyncio==0.23.5
pytest-dependency==0.6.0 pytest-dependency==0.6.0
python-dotenv==1.0.1
# via pydantic-settings
pyyaml==6.0.1 pyyaml==6.0.1
# via bandit # via bandit
# via hyperglass # via hyperglass
@ -159,7 +172,6 @@ ruff==0.2.2
scp==0.14.5 scp==0.14.5
# via netmiko # via netmiko
setuptools==69.1.0 setuptools==69.1.0
# via gunicorn
# via netmiko # via netmiko
# via nodeenv # via nodeenv
# via pytest-dependency # via pytest-dependency
@ -170,7 +182,7 @@ sniffio==1.3.0
# via httpcore # via httpcore
# via httpx # via httpx
stackprinter==0.2.11 stackprinter==0.2.11
starlette==0.26.1 starlette==0.36.3
# via fastapi # via fastapi
stevedore==5.1.0 stevedore==5.1.0
# via bandit # via bandit
@ -193,7 +205,9 @@ typer==0.9.0
# via favicons # via favicons
# via hyperglass # via hyperglass
typing-extensions==4.9.0 typing-extensions==4.9.0
# via fastapi
# via pydantic # via pydantic
# via pydantic-core
# via typer # via typer
uvicorn==0.21.1 uvicorn==0.21.1
# via hyperglass # via hyperglass

View file

@ -10,6 +10,8 @@
-e file:. -e file:.
aiofiles==23.2.1 aiofiles==23.2.1
# via hyperglass # via hyperglass
annotated-types==0.6.0
# via pydantic
anyio==4.3.0 anyio==4.3.0
# via httpcore # via httpcore
# via starlette # via starlette
@ -32,7 +34,7 @@ cssselect2==0.7.0
# via svglib # via svglib
distro==1.8.0 distro==1.8.0
# via hyperglass # via hyperglass
fastapi==0.95.1 fastapi==0.110.0
# via hyperglass # via hyperglass
favicons==0.2.2 favicons==0.2.2
# via hyperglass # via hyperglass
@ -40,7 +42,7 @@ freetype-py==2.4.0
# via rlpycairo # via rlpycairo
future==0.18.3 future==0.18.3
# via textfsm # via textfsm
gunicorn==20.1.0 gunicorn==21.2.0
# via hyperglass # via hyperglass
h11==0.14.0 h11==0.14.0
# via httpcore # via httpcore
@ -64,6 +66,8 @@ netmiko==4.1.2
# via hyperglass # via hyperglass
ntc-templates==4.3.0 ntc-templates==4.3.0
# via netmiko # via netmiko
packaging==24.0
# via gunicorn
paramiko==3.4.0 paramiko==3.4.0
# via hyperglass # via hyperglass
# via netmiko # via netmiko
@ -80,9 +84,17 @@ pycairo==1.26.0
# via rlpycairo # via rlpycairo
pycparser==2.21 pycparser==2.21
# via cffi # via cffi
pydantic==1.10.14 pydantic==2.6.3
# via fastapi # via fastapi
# via hyperglass # via hyperglass
# via pydantic-extra-types
# via pydantic-settings
pydantic-core==2.16.3
# via pydantic
pydantic-extra-types==2.6.0
# via hyperglass
pydantic-settings==2.2.1
# via hyperglass
pygments==2.17.2 pygments==2.17.2
# via rich # via rich
pyjwt==2.6.0 pyjwt==2.6.0
@ -91,6 +103,8 @@ pynacl==1.5.0
# via paramiko # via paramiko
pyserial==3.5 pyserial==3.5
# via netmiko # via netmiko
python-dotenv==1.0.1
# via pydantic-settings
pyyaml==6.0.1 pyyaml==6.0.1
# via hyperglass # via hyperglass
# via netmiko # via netmiko
@ -107,7 +121,6 @@ rlpycairo==0.3.0
scp==0.14.5 scp==0.14.5
# via netmiko # via netmiko
setuptools==69.1.0 setuptools==69.1.0
# via gunicorn
# via netmiko # via netmiko
six==1.16.0 six==1.16.0
# via textfsm # via textfsm
@ -115,7 +128,7 @@ sniffio==1.3.0
# via anyio # via anyio
# via httpcore # via httpcore
# via httpx # via httpx
starlette==0.26.1 starlette==0.36.3
# via fastapi # via fastapi
svglib==1.5.1 svglib==1.5.1
# via favicons # via favicons
@ -133,7 +146,9 @@ typer==0.9.0
# via favicons # via favicons
# via hyperglass # via hyperglass
typing-extensions==4.9.0 typing-extensions==4.9.0
# via fastapi
# via pydantic # via pydantic
# via pydantic-core
# via typer # via typer
uvicorn==0.21.1 uvicorn==0.21.1
# via hyperglass # via hyperglass