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
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from fastapi.exceptions import ValidationError, RequestValidationError
from fastapi.exceptions import ValidationException, RequestValidationError
from fastapi.staticfiles import StaticFiles
from starlette.exceptions import HTTPException as StarletteHTTPException
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 Validation Error Handler
app.add_exception_handler(ValidationError, validation_handler)
app.add_exception_handler(ValidationException, validation_handler)
# Uncaught Error Handler
app.add_exception_handler(Exception, default_handler)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,13 @@
"""Default VyOS Directives."""
# Project
from hyperglass.models.directive import Rule, Text, BuiltinDirective
from hyperglass.models.directive import (
RuleWithIPv4,
RuleWithIPv6,
RuleWithPattern,
Text,
BuiltinDirective,
)
__all__ = (
"VyOS_BGPASPath",
@ -15,12 +21,12 @@ VyOS_BGPRoute = BuiltinDirective(
id="__hyperglass_vyos_bgp_route__",
name="BGP Route",
rules=[
Rule(
RuleWithIPv4(
condition="0.0.0.0/0",
action="permit",
command="show ip bgp {target}",
),
Rule(
RuleWithIPv6(
condition="::/0",
action="permit",
command="show ipv6 bgp {target}",
@ -34,7 +40,7 @@ VyOS_BGPASPath = BuiltinDirective(
id="__hyperglass_vyos_bgp_aspath__",
name="BGP AS Path",
rules=[
Rule(
RuleWithPattern(
condition="*",
action="permit",
commands=[
@ -51,7 +57,7 @@ VyOS_BGPCommunity = BuiltinDirective(
id="__hyperglass_vyos_bgp_community__",
name="BGP Community",
rules=[
Rule(
RuleWithPattern(
condition="*",
action="permit",
commands=[
@ -68,12 +74,12 @@ VyOS_Ping = BuiltinDirective(
id="__hyperglass_vyos_ping__",
name="Ping",
rules=[
Rule(
RuleWithIPv4(
condition="0.0.0.0/0",
action="permit",
command="ping {target} count 5 interface {source4}",
),
Rule(
RuleWithIPv6(
condition="::/0",
action="permit",
command="ping {target} count 5 interface {source6}",
@ -87,12 +93,12 @@ VyOS_Traceroute = BuiltinDirective(
id="__hyperglass_vyos_traceroute__",
name="Traceroute",
rules=[
Rule(
RuleWithIPv4(
condition="0.0.0.0/0",
action="permit",
command="mtr -4 -G 1 -c 1 -w -o SAL -a {source4} {target}",
),
Rule(
RuleWithIPv6(
condition="::/0",
action="permit",
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(
**{
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)
}
)
@ -107,10 +107,10 @@ class HttpClient(Connection):
responses += (data,)
except (httpx.TimeoutException) as error:
except httpx.TimeoutException as 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:
raise AuthError(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)
log.debug("UI Build Data:\n{}", build_json)
# Create SHA256 hash from all parameters passed to UI, use as
# build identifier.

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ import secrets
from datetime import datetime
# Third Party
from pydantic import BaseModel, constr, validator
from pydantic import BaseModel, constr, field_validator, ConfigDict, Field
# Project
from hyperglass.log import log
@ -29,17 +29,12 @@ QueryType = constr(strip_whitespace=True, strict=True, min_length=1)
class Query(BaseModel):
"""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_target: t.Union[t.List[QueryTarget], QueryTarget]
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:
"""Initialize the query with a UTC timestamp at initialization time."""
super().__init__(**data)
@ -96,14 +91,14 @@ class Query(BaseModel):
def dict(self) -> t.Dict[str, t.Union[t.List[str], str]]:
"""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
def device(self) -> Device:
"""Get this query's device object by query_location."""
return self._state.devices[self.query_location]
@validator("query_location")
@field_validator("query_location")
def validate_query_location(cls, value):
"""Ensure query_location is defined."""
@ -114,7 +109,7 @@ class Query(BaseModel):
return value
@validator("query_type")
@field_validator("query_type")
def validate_query_type(cls, value: t.Any):
"""Ensure a requested query type exists."""
devices = use_state("devices")

View file

@ -4,25 +4,132 @@
import typing as t
# Third Party
from pydantic import BaseModel, StrictInt, StrictStr, StrictBool, constr, validator
from pydantic import (
BaseModel,
StrictInt,
StrictStr,
StrictBool,
field_validator,
Field,
ConfigDict,
)
# Project
from hyperglass.state import use_state
ErrorName = constr(regex=r"(success|warning|error|danger)")
ResponseLevel = constr(regex=r"success")
ResponseFormat = constr(regex=r"(application\/json|text\/plain)")
ErrorName = t.Literal["success", "warning", "error", "danger"]
ResponseLevel = t.Literal["success"]
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):
"""Query response model."""
output: StrictStr
level: ErrorName = "danger"
id: t.Optional[StrictStr]
keywords: t.List[StrictStr] = []
model_config = ConfigDict(
json_schema_extra={
"title": "Query Error",
"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):
"""If no output is specified, use a customizable generic message."""
if value is None:
@ -30,138 +137,45 @@ class QueryError(BaseModel):
return messages.general
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):
"""Query response model."""
output: t.Union[t.Dict, StrictStr]
level: ResponseLevel = "success"
random: StrictStr
cached: StrictBool
runtime: StrictInt
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"],
}
]
model_config = ConfigDict(
json_schema_extra={
"title": "Query Response",
"description": "Looking glass response",
"examples": schema_query_examples,
}
)
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):
"""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
name: StrictStr
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):
"""Response model for /api/communities."""
@ -174,36 +188,26 @@ class CommunityResponse(BaseModel):
class SupportedQueryResponse(BaseModel):
"""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
display_name: StrictStr
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):
"""Response model for /api/info endpoint."""
name: StrictStr
organization: StrictStr
primary_asn: StrictInt
version: StrictStr
class Config:
"""Pydantic model configuration."""
title = "System Information"
description = "General information about this looking glass."
schema_extra = {
model_config = ConfigDict(
json_schema_extra={
"title": "System Information",
"description": "General information about this looking glass.",
"examples": [
{
"name": "hyperglass",
@ -211,5 +215,11 @@ class InfoResponse(BaseModel):
"primary_asn": 65000,
"version": "hyperglass 1.0.0-beta.52",
}
]
],
}
)
name: StrictStr
organization: StrictStr
primary_asn: StrictInt
version: StrictStr

View file

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

View file

@ -1,26 +1,18 @@
"""Custom validation types."""
import typing as t
from pydantic import AfterValidator
# Project
from hyperglass.constants import SUPPORTED_QUERY_TYPES
class SupportedQuery(str):
"""Query Type Validation Model."""
def validate_query_type(value: str) -> str:
"""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
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__()})"
SupportedQuery = t.Annotated[str, AfterValidator(validate_query_type)]

View file

@ -1,10 +1,10 @@
"""Validation model for Redis cache config."""
# Standard Library
from typing import Union, Optional
import typing as t
# Third Party
from pydantic import SecretStr, StrictInt, StrictStr, StrictBool, IPvAnyAddress
from pydantic import SecretStr, IPvAnyAddress
# Local
from ..main import HyperglassModel
@ -13,30 +13,14 @@ from ..main import HyperglassModel
class CachePublic(HyperglassModel):
"""Public cache parameters."""
timeout: StrictInt = 120
show_text: StrictBool = True
timeout: int = 120
show_text: bool = True
class Cache(CachePublic):
"""Validation model for params.cache."""
host: Union[IPvAnyAddress, StrictStr] = "localhost"
port: StrictInt = 6379
database: StrictInt = 1
password: Optional[SecretStr]
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."},
}
host: t.Union[IPvAnyAddress, str] = "localhost"
port: int = 6379
database: int = 1
password: t.Optional[SecretStr] = None

View file

@ -1,34 +1,35 @@
"""Validate credential configuration variables."""
# Standard Library
from typing import Optional
import typing as t
# Third Party
from pydantic import FilePath, SecretStr, StrictStr, constr, root_validator
from pydantic import FilePath, SecretStr, model_validator
# Local
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"):
"""Model for per-credential config in devices.yaml."""
username: StrictStr
password: Optional[SecretStr]
key: Optional[FilePath]
username: str
password: t.Optional[SecretStr] = None
key: t.Optional[FilePath] = None
_method: t.Optional[AuthMethod] = None
@root_validator
def validate_credential(cls, values):
@model_validator(mode="after")
def validate_credential(cls, data: "Credential"):
"""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(
"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):
"""Set private attribute _method based on validated model."""

View file

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

View file

@ -1,28 +1,31 @@
"""Configuration for API docs feature."""
import typing as t
# Third Party
from pydantic import Field, HttpUrl, StrictStr, StrictBool, constr
from pydantic import Field, HttpUrl
# Local
from ..main import HyperglassModel
from ..fields import AnyUri
DocsMode = constr(regex=r"(swagger|redoc)")
DocsMode = t.Literal["swagger", "redoc"]
class EndpointConfig(HyperglassModel):
"""Validation model for per API endpoint documentation."""
title: StrictStr = Field(
title: str = Field(
...,
title="Endpoint Title",
description="Displayed as the header text above the API endpoint section.",
)
description: StrictStr = Field(
description: str = Field(
...,
title="Endpoint Description",
description="Displayed inside each API endpoint section.",
)
summary: StrictStr = Field(
summary: str = Field(
...,
title="Endpoint Summary",
description="Displayed beside the API endpoint URI.",
@ -32,9 +35,7 @@ class EndpointConfig(HyperglassModel):
class Docs(HyperglassModel):
"""Validation model for params.docs."""
enable: StrictBool = Field(
True, title="Enable", description="Enable or disable API documentation."
)
enable: bool = Field(True, title="Enable", description="Enable or disable API documentation.")
mode: DocsMode = Field(
"redoc",
title="Docs Mode",
@ -50,12 +51,12 @@ class Docs(HyperglassModel):
title="URI",
description="HTTP URI/path where API documentation can be accessed.",
)
title: StrictStr = Field(
title: str = Field(
"{site_title} API Documentation",
title="Title",
description="API documentation title. `{site_title}` may be used to display the `site_title` parameter.",
)
description: StrictStr = Field(
description: str = Field(
"",
title="Description",
description="API documentation description appearing below the title.",
@ -80,27 +81,3 @@ class Docs(HyperglassModel):
description="General information about this looking glass.",
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 (
FilePath,
SecretStr,
StrictInt,
StrictStr,
StrictBool,
PrivateAttr,
IPvAnyAddress,
)
@ -39,23 +36,23 @@ Scheme = t.Literal["http", "https"]
class AttributeMapConfig(HyperglassModel):
"""Allow the user to 'rewrite' hyperglass field names to their own values."""
query_target: t.Optional[StrictStr]
query_type: t.Optional[StrictStr]
query_location: t.Optional[StrictStr]
query_target: t.Optional[str] = None
query_type: t.Optional[str] = None
query_location: t.Optional[str] = None
class AttributeMap(HyperglassModel):
"""Merged implementation of attribute map configuration."""
query_target: StrictStr
query_type: StrictStr
query_location: StrictStr
query_target: str
query_type: str
query_location: str
class HttpBasicAuth(HyperglassModel):
"""Configuration model for HTTP basic authentication."""
username: StrictStr
username: str
password: SecretStr
@ -63,21 +60,21 @@ class HttpConfiguration(HyperglassModel):
"""HTTP client configuration."""
_attribute_map: AttributeMap = PrivateAttr()
path: StrictStr = "/"
path: str = "/"
method: HttpMethod = "GET"
scheme: Scheme = "https"
query: t.Optional[t.Union[t.Literal[False], t.Dict[str, Primitives]]]
verify_ssl: StrictBool = True
ssl_ca: t.Optional[FilePath]
ssl_client: t.Optional[FilePath]
source: t.Optional[IPvAnyAddress]
query: t.Optional[t.Union[t.Literal[False], t.Dict[str, Primitives]]] = None
verify_ssl: bool = True
ssl_ca: t.Optional[FilePath] = None
ssl_client: t.Optional[FilePath] = None
source: t.Optional[IPvAnyAddress] = None
timeout: IntFloat = 5
headers: t.Dict[str, str] = {}
follow_redirects: StrictBool = False
basic_auth: t.Optional[HttpBasicAuth]
follow_redirects: bool = False
basic_auth: t.Optional[HttpBasicAuth] = None
attribute_map: AttributeMapConfig = AttributeMapConfig()
body_format: BodyFormat = "json"
retries: StrictInt = 0
retries: int = 0
def __init__(self, **data: t.Any) -> None:
"""Create HTTP Client Configuration Definition."""

View file

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

View file

@ -1,7 +1,7 @@
"""Validate error message configuration variables."""
# Third Party
from pydantic import Field, StrictStr
from pydantic import Field, ConfigDict
# Local
from ..main import HyperglassModel
@ -10,66 +10,66 @@ from ..main import HyperglassModel
class Messages(HyperglassModel):
"""Validation model for params.messages."""
no_input: StrictStr = Field(
no_input: str = Field(
"{field} must be specified.",
title="No Input",
description="Displayed when no a required field is not specified. `{field}` may be used to display the `display_name` of the field that was omitted.",
)
target_not_allowed: StrictStr = Field(
target_not_allowed: str = Field(
"{target} is 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.",
)
feature_not_enabled: StrictStr = Field(
feature_not_enabled: str = Field(
"{feature} is 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.",
)
invalid_input: StrictStr = Field(
invalid_input: str = Field(
"{target} is not valid.",
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.",
)
invalid_query: StrictStr = Field(
invalid_query: str = Field(
"{target} is not a valid {query_type} target.",
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.",
)
invalid_field: StrictStr = Field(
invalid_field: str = Field(
"{input} is an invalid {field}.",
title="Invalid Field",
description="Displayed when a query field contains an invalid or unsupported value. `{input}` and `{field}` may be used to display the invalid input value and corresponding field name.",
)
general: StrictStr = Field(
general: str = Field(
"Something went wrong.",
title="General Error",
description="Displayed when generalized errors occur. Seeing this error message may indicate a bug in hyperglass, as most other errors produced are highly contextual. If you see this in the wild, try enabling [debug mode](/fixme) and review the logs to pinpoint the source of the error.",
)
not_found: StrictStr = Field(
not_found: str = Field(
"{type} '{name}' 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.",
)
request_timeout: StrictStr = Field(
request_timeout: str = Field(
"Request timed out.",
title="Request Timeout",
description="Displayed when the [request_timeout](/fixme) time expires.",
)
connection_error: StrictStr = Field(
connection_error: str = Field(
"Error connecting to {device_name}: {error}",
title="Displayed when hyperglass is unable to connect to a configured device. Usually, this indicates a configuration error. `{device_name}` and `{error}` may be used to display the device in question and the specific connection error.",
)
authentication_error: StrictStr = Field(
authentication_error: str = Field(
"Authentication error occurred.",
title="Authentication Error",
description="Displayed when hyperglass is unable to authenticate to a configured device. Usually, this indicates a configuration error.",
)
no_response: StrictStr = Field(
no_response: str = Field(
"No response.",
title="No Response",
description="Displayed when hyperglass can connect to a device, but no output able to be read. Seeing this error may indicate a bug in hyperglas or one of its dependencies. If you see this in the wild, try enabling [debug mode](/fixme) and review the logs to pinpoint the source of the error.",
)
no_output: StrictStr = Field(
no_output: str = Field(
"The query completed, but no matching results were found.",
title="No Output",
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:
"""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."""
if not self.has(attr):
raise KeyError(f"'{attr}' does not exist on Messages model")
return getattr(self, attr)
class Config:
"""Pydantic model configuration."""
title = "Messages"
description = "Customize almost all user-facing UI & API messages."
schema_extra = {"level": 2}
model_config = ConfigDict(
title="Messages",
description="Customize almost all user-facing UI & API messages.",
json_schema_extra={"level": 2},
)

View file

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

View file

@ -1,11 +1,11 @@
"""Configuration validation entry point."""
# Standard Library
from typing import Any, Dict, List, Tuple, Union, Literal
import typing as t
from pathlib import Path
# Third Party
from pydantic import Field, StrictInt, StrictStr, StrictBool, validator
from pydantic import Field, field_validator, ValidationInfo, ConfigDict
# Project
from hyperglass.settings import Settings
@ -19,33 +19,33 @@ from .logging import Logging
from .messages import Messages
from .structured import Structured
Localhost = Literal["localhost"]
Localhost = t.Literal["localhost"]
class ParamsPublic(HyperglassModel):
"""Public configuration parameters."""
request_timeout: StrictInt = Field(
request_timeout: int = Field(
90,
title="Request Timeout",
description="Global timeout in seconds for all requests. The frontend application (UI) uses this field's exact value when submitting queries. The backend application uses this field's value, minus one second, for its own timeout handling. This is to ensure a contextual timeout error is presented to the end user in the event of a backend application timeout.",
)
primary_asn: Union[StrictInt, StrictStr] = Field(
primary_asn: t.Union[int, str] = Field(
"65001",
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.",
)
org_name: StrictStr = Field(
org_name: str = Field(
"Beloved Hyperglass User",
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.",
)
site_title: StrictStr = Field(
site_title: str = Field(
"hyperglass",
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.",
)
site_description: StrictStr = Field(
site_description: str = Field(
"{org_name} Network Looking Glass",
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.',
@ -55,19 +55,21 @@ class ParamsPublic(HyperglassModel):
class Params(ParamsPublic, HyperglassModel):
"""Validation model for all configuration variables."""
model_config = ConfigDict(json_schema_extra={"level": 1})
# Top Level Params
fake_output: StrictBool = Field(
fake_output: bool = Field(
False,
title="Fake Output",
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",
description="Allowed CORS hosts. By default, no CORS hosts are allowed.",
)
plugins: List[StrictStr] = []
plugins: t.List[str] = []
# Sub Level Params
cache: Cache = Cache()
@ -77,26 +79,21 @@ class Params(ParamsPublic, HyperglassModel):
structured: Structured = Structured()
web: Web = Web()
class Config:
"""Pydantic model configuration."""
schema_extra = {"level": 1}
def __init__(self, **kw: Any) -> None:
def __init__(self, **kw: t.Any) -> None:
return super().__init__(**self.convert_paths(kw))
@validator("site_description")
def validate_site_description(cls: "Params", value: str, values: Dict[str, Any]) -> str:
"""Format the site descripion with the org_name field."""
return value.format(org_name=values["org_name"])
@field_validator("site_description")
def validate_site_description(cls: "Params", value: str, info: ValidationInfo) -> str:
"""Format the site description with the org_name field."""
return value.format(org_name=info.data.get("org_name"))
@validator("primary_asn")
def validate_primary_asn(cls: "Params", value: Union[int, str]) -> str:
@field_validator("primary_asn")
def validate_primary_asn(cls: "Params", value: t.Union[int, str]) -> str:
"""Stringify primary_asn if passed as an integer."""
return str(value)
@validator("plugins")
def validate_plugins(cls: "Params", value: List[str]) -> List[str]:
@field_validator("plugins")
def validate_plugins(cls: "Params", value: t.List[str]) -> t.List[str]:
"""Validate and register configured plugins."""
plugin_dir = Settings.app_path / "plugins"
@ -111,11 +108,11 @@ class Params(ParamsPublic, HyperglassModel):
return [str(f) for f in matching_plugins]
return []
def common_plugins(self) -> Tuple[Path, ...]:
def common_plugins(self) -> t.Tuple[Path, ...]:
"""Get all validated external common plugins as Path objects."""
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."""
return self.export_dict(

View file

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

View file

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

View file

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

View file

@ -6,7 +6,15 @@ import typing as t
from ipaddress import IPv4Network, IPv6Network, ip_network
# Third Party
from pydantic import Field, FilePath, StrictStr, StrictBool, PrivateAttr, conint, validator
from pydantic import (
Discriminator,
field_validator,
Field,
FilePath,
IPvAnyNetwork,
PrivateAttr,
Tag,
)
# Project
from hyperglass.log import log
@ -18,24 +26,20 @@ from hyperglass.exceptions.private import InputValidationError
from .main import MultiModel, HyperglassModel, HyperglassUniqueModel
from .fields import Action
if t.TYPE_CHECKING:
# Project
from hyperglass.models.api.query import QueryTarget
IPv4PrefixLength = conint(ge=0, le=32)
IPv6PrefixLength = conint(ge=0, le=128)
IPNetwork = t.Union[IPv4Network, IPv6Network]
StringOrArray = t.Union[StrictStr, t.List[StrictStr]]
Condition = t.Union[IPv4Network, IPv6Network, StrictStr]
StringOrArray = t.Union[str, t.List[str]]
Condition = t.Union[IPvAnyNetwork, str]
RuleValidation = t.Union[t.Literal["ipv4", "ipv6", "pattern"], None]
PassedValidation = t.Union[bool, None]
IPFamily = t.Literal["ipv4", "ipv6"]
RuleTypeAttr = t.Literal["ipv4", "ipv6", "pattern", "none"]
class Input(HyperglassModel):
"""Base input field."""
_type: PrivateAttr
description: StrictStr
description: str
@property
def is_select(self) -> bool:
@ -52,15 +56,15 @@ class Text(Input):
"""Text/input field model."""
_type: PrivateAttr = PrivateAttr("text")
validation: t.Optional[StrictStr]
validation: t.Optional[str] = None
class Option(HyperglassModel):
"""Select option model."""
name: t.Optional[StrictStr]
description: t.Optional[StrictStr]
value: StrictStr
name: t.Optional[str] = None
description: t.Optional[str] = None
value: str
class Select(Input):
@ -70,16 +74,16 @@ class Select(Input):
options: t.List[Option]
class Rule(HyperglassModel, allow_population_by_field_name=True):
class Rule(HyperglassModel):
"""Base rule."""
_validation: RuleValidation = PrivateAttr()
_type: RuleTypeAttr = PrivateAttr(Field("none", discriminator="_type"))
_passed: PassedValidation = PrivateAttr(None)
condition: Condition
action: Action = Action("permit")
action: Action = "permit"
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]:
"""Ensure commands is a list."""
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:
"""Validate a query target (Placeholder signature)."""
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):
"""Base IP-based rule."""
_family: PrivateAttr
condition: IPNetwork
allow_reserved: StrictBool = False
allow_unspecified: StrictBool = False
allow_loopback: StrictBool = False
condition: IPvAnyNetwork
allow_reserved: bool = False
allow_unspecified: bool = False
allow_loopback: bool = False
ge: 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."""
log.debug("Checking membership of {} for {}", str(target), str(network))
if (
@ -115,7 +125,7 @@ class RuleWithIP(Rule):
return True
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."""
if target.prefixlen <= self.le and target.prefixlen >= self.ge:
log.debug("{} is in range {}-{}", target, self.ge, self.le)
@ -123,7 +133,7 @@ class RuleWithIP(Rule):
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."""
if isinstance(target, t.List):
@ -169,30 +179,28 @@ class RuleWithIP(Rule):
class RuleWithIPv4(RuleWithIP):
"""A rule by which to evaluate an IPv4 target."""
_family: PrivateAttr = PrivateAttr("ipv4")
_validation: RuleValidation = PrivateAttr("ipv4")
_type: RuleTypeAttr = "ipv4"
condition: IPv4Network
ge: IPv4PrefixLength = 0
le: IPv4PrefixLength = 32
ge: int = Field(0, ge=0, le=32)
le: int = Field(32, ge=0, le=32)
class RuleWithIPv6(RuleWithIP):
"""A rule by which to evaluate an IPv6 target."""
_family: PrivateAttr = PrivateAttr("ipv6")
_validation: RuleValidation = PrivateAttr("ipv6")
_type: RuleTypeAttr = "ipv6"
condition: IPv6Network
ge: IPv6PrefixLength = 0
le: IPv6PrefixLength = 128
ge: int = Field(0, ge=0, le=128)
le: int = Field(128, ge=0, le=128)
class RuleWithPattern(Rule):
"""A rule validated by a regular expression pattern."""
_validation: RuleValidation = PrivateAttr("pattern")
condition: StrictStr
_type: RuleTypeAttr = "pattern"
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."""
def validate_single_value(value: str) -> t.Union[bool, BaseException]:
@ -234,7 +242,7 @@ class RuleWithPattern(Rule):
class RuleWithoutValidation(Rule):
"""A rule with no validation."""
_validation: RuleValidation = PrivateAttr(None)
_type: RuleTypeAttr = "none"
condition: None
def validate_target(self, target: str, *, multiple: bool) -> t.Literal[True]:
@ -243,24 +251,44 @@ class RuleWithoutValidation(Rule):
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")):
"""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
name: StrictStr
rules: t.List[RuleType] = [RuleWithPattern(condition="*")]
id: str
name: str
rules: t.List[RuleType] = [
Field(RuleWithPattern(condition="*"), discriminator=Discriminator(type_discriminator))
]
field: t.Union[Text, Select]
info: t.Optional[FilePath]
plugins: t.List[StrictStr] = []
table_output: t.Optional[StrictStr]
groups: t.List[StrictStr] = []
multiple: StrictBool = False
multiple_separator: StrictStr = " "
info: t.Optional[FilePath] = None
plugins: t.List[str] = []
table_output: t.Optional[str] = None
groups: t.List[str] = []
multiple: bool = False
multiple_separator: str = " "
def validate_target(self, target: str) -> bool:
"""Validate a target against all configured rules."""
@ -281,7 +309,7 @@ class Directive(HyperglassUniqueModel, unique_by=("id", "table_output")):
return "text"
return None
@validator("plugins")
@field_validator("plugins")
def validate_plugins(cls: "Directive", plugins: t.List[str]) -> t.List[str]:
"""Validate and register configured 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")):
"""Natively-supported directive."""
__hyperglass_builtin__: t.ClassVar[bool] = True
_hyperglass_builtin: bool = PrivateAttr(True)
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
for directive in self
if directive.__hyperglass_builtin__ is True
if directive._hyperglass_builtin is True
and platform in getattr(directive, "platforms", ())
)
)

View file

@ -3,12 +3,11 @@
# Standard Library
import re
import typing as t
from pathlib import Path
# 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")
SupportedDriver = t.Literal["netmiko", "hyperglass_agent"]
@ -17,133 +16,42 @@ HttpProvider = t.Literal["msteams", "slack", "generic"]
LogFormat = t.Literal["text", "json"]
Primitives = t.Union[None, float, int, bool, str]
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):
"""Custom field type for HTTP URI, e.g. /example."""
@classmethod
def __get_validators__(cls):
"""Pydantic custom field method."""
yield cls.validate
@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__()})"
def validate_uri(value: str) -> str:
"""Ensure URI string contains a leading forward-slash."""
uri_regex = re.compile(r"^(\/.*)$")
match = uri_regex.fullmatch(value)
if not match:
raise ValueError("Invalid format. A URI must begin with a forward slash, e.g. '/example'")
return match.group()
class Action(str):
"""Custom field type for policy actions."""
def validate_action(value: str) -> ActionValue:
"""Ensure action is an allowed value or acceptable alias."""
permits = ("permit", "allow", "accept")
denies = ("deny", "block", "reject")
value = value.strip().lower()
if value in permits:
return "permit"
if value in denies:
return "deny"
@classmethod
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__()})"
raise ValueError("Action must be one of '{}'".format(", ".join((*permits, *denies))))
class HttpMethod(str):
"""Custom field type for HTTP methods."""
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__()})"
AnyUri = t.Annotated[str, AfterValidator(validate_uri)]
Action = t.Annotated[ActionValue, AfterValidator(validate_action)]
HttpMethod = t.Annotated[HttpMethodValue, BeforeValidator(str.upper)]

View file

@ -9,8 +9,7 @@ import typing as t
from pathlib import Path
# Third Party
from pydantic import HttpUrl, BaseModel, BaseConfig, PrivateAttr
from pydantic.generics import GenericModel
from pydantic import HttpUrl, BaseModel, PrivateAttr, RootModel, ConfigDict
# Project
from hyperglass.log import log
@ -20,38 +19,30 @@ from hyperglass.types import Series
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):
"""Base model for all hyperglass configuration models."""
class Config(BaseConfig):
"""Pydantic model configuration."""
validate_all = True
extra = "forbid"
validate_assignment = True
allow_population_by_field_name = True
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)
model_config = ConfigDict(
extra="forbid",
json_encoders={HttpUrl: lambda v: str(v), Path: str},
populate_by_name=True,
validate_assignment=True,
validate_default=True,
alias_generator=alias_generator,
)
def convert_paths(self, value: t.Any):
"""Change path to relative to app_path."""
@ -98,7 +89,7 @@ class HyperglassModel(BaseModel):
for key in kwargs.keys():
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):
"""Return instance as dictionary."""
@ -108,7 +99,7 @@ class HyperglassModel(BaseModel):
for key in kwargs.keys():
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):
"""Return instance as YAML."""
@ -177,47 +168,45 @@ class HyperglassModelWithId(HyperglassModel):
return hash(self.id)
class MultiModel(GenericModel, t.Generic[MultiModelT]):
class MultiModel(RootModel[MultiModelT]):
"""Extension of HyperglassModel for managing multiple models as a list."""
model_config = ConfigDict(
validate_default=True,
validate_assignment=True,
)
model: t.ClassVar[MultiModelT]
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()
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:
"""Validate items."""
for cls_var in ("model", "unique_by"):
if getattr(self, cls_var, None) is None:
raise AttributeError(f"MultiModel is missing class variable '{cls_var}'")
valid = self._valid_items(*items)
super().__init__(__root__=valid)
self._count = len(self.__root__)
super().__init__(root=valid)
self._count = len(self.root)
def __init_subclass__(cls, **kw: t.Any) -> None:
"""Add class variables from keyword arguments."""
model = kw.pop("model", None)
cls.model = model
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__()
def __repr__(self) -> str:
"""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]:
"""Iterate items."""
return iter(self.__root__)
return iter(self.root)
def __getitem__(self, value: t.Union[int, str]) -> MultiModelT:
"""Get an item by its `unique_by` property."""
@ -228,7 +217,7 @@ class MultiModel(GenericModel, t.Generic[MultiModelT]):
)
)
if isinstance(value, int):
return self.__root__[value]
return self.root[value]
for item in self:
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:
"""Get number of items."""
return len(self.__root__)
return len(self.root)
@property
def ids(self) -> t.Tuple[t.Any, ...]:
@ -283,7 +272,7 @@ class MultiModel(GenericModel, t.Generic[MultiModelT]):
new = type(name, (cls,), cls.__dict__)
new.model = model
new.unique_by = unique_by
new.model_name = getattr(model, "__name__", "MultiModel")
new._model_name = getattr(model, "__name__", "MultiModel")
return new
def _valid_items(
@ -317,7 +306,7 @@ class MultiModel(GenericModel, t.Generic[MultiModelT]):
if getattr(o, unique_by) == v
}
return tuple(unique_by_objects.values())
return (*self.__root__, *to_add)
return (*self.root, *to_add)
def filter(self, *properties: str) -> MultiModelT:
"""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:
"""Add an item to the model."""
new = self._merge_with(*items, unique_by=unique_by)
self.__root__ = new
self._count = len(self.__root__)
self.root = new
self._count = len(self.root)
for item in new:
log.debug(
"Added {} '{!s}' to {}",

View file

@ -1,9 +1,11 @@
"""Data Models for Parsing Arista JSON Response."""
# Standard Library
from typing import Dict, List, Optional
import typing as t
from datetime import datetime
from pydantic import ConfigDict
# Project
from hyperglass.log import log
from hyperglass.models.data import BGPRouteTable
@ -29,16 +31,14 @@ def _alias_generator(field: str) -> str:
class _AristaBase(HyperglassModel):
"""Base Model for Arista validation."""
class Config:
extra = "ignore"
alias_generator = _alias_generator
model_config = ConfigDict(extra="ignore", alias_generator=_alias_generator)
class AristaAsPathEntry(_AristaBase):
"""Validation model for Arista asPathEntry."""
as_path_type: str = "External"
as_path: Optional[str] = ""
as_path: t.Optional[str] = ""
class AristaPeerEntry(_AristaBase):
@ -55,18 +55,18 @@ class AristaRouteType(_AristaBase):
suppressed: bool
valid: bool
active: bool
origin_validity: Optional[str] = "notVerified"
origin_validity: t.Optional[str] = "notVerified"
class AristaRouteDetail(_AristaBase):
"""Validation for Arista routeDetail."""
origin: str
label_stack: List = []
ext_community_list: List[str] = []
ext_community_list_raw: List[str] = []
community_list: List[str] = []
large_community_list: List[str] = []
label_stack: t.List = []
ext_community_list: t.List[str] = []
ext_community_list_raw: t.List[str] = []
community_list: t.List[str] = []
large_community_list: t.List[str] = []
class AristaRoutePath(_AristaBase):
@ -81,16 +81,16 @@ class AristaRoutePath(_AristaBase):
timestamp: int = int(datetime.utcnow().timestamp())
next_hop: str
route_type: AristaRouteType
route_detail: Optional[AristaRouteDetail]
route_detail: t.Optional[AristaRouteDetail]
class AristaRouteEntry(_AristaBase):
"""Validation model for Arista bgpRouteEntries."""
total_paths: int = 0
bgp_advertised_peer_groups: Dict = {}
bgp_advertised_peer_groups: t.Dict = {}
mask_length: int
bgp_route_paths: List[AristaRoutePath] = []
bgp_route_paths: t.List[AristaRoutePath] = []
class AristaBGPTable(_AristaBase):
@ -98,7 +98,7 @@ class AristaBGPTable(_AristaBase):
router_id: 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.
asn: int
@ -109,7 +109,7 @@ class AristaBGPTable(_AristaBase):
return now_timestamp - timestamp
@staticmethod
def _get_as_path(as_path: str) -> List[str]:
def _get_as_path(as_path: str) -> t.List[str]:
if as_path == "":
return []
return [int(p) for p in as_path.split() if p.isdecimal()]
@ -119,11 +119,9 @@ class AristaBGPTable(_AristaBase):
routes = []
count = 0
for prefix, entries in self.bgp_route_entries.items():
count += entries.total_paths
for route in entries.bgp_route_paths:
as_path = self._get_as_path(route.as_path_entry.as_path)
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."""
# Standard Library
from typing import List
import typing as t
from datetime import datetime
# Third Party
from pydantic import StrictInt, StrictStr, StrictBool, constr, root_validator
from pydantic import model_validator, ConfigDict
# Project
from hyperglass.log import log
@ -14,7 +14,7 @@ from hyperglass.models.data import BGPRouteTable
# Local
from ..main import HyperglassModel
FRRPeerType = constr(regex=r"(internal|external)")
FRRPeerType = t.Literal["internal", "external"]
def _alias_generator(field):
@ -23,46 +23,44 @@ def _alias_generator(field):
class _FRRBase(HyperglassModel):
class Config:
alias_generator = _alias_generator
extra = "ignore"
model_config = ConfigDict(alias_generator=_alias_generator, extra="ignore")
class FRRNextHop(_FRRBase):
"""FRR Next Hop Model."""
ip: StrictStr
afi: StrictStr
metric: StrictInt
accessible: StrictBool
used: StrictBool
ip: str
afi: str
metric: int
accessible: bool
used: bool
class FRRPeer(_FRRBase):
"""FRR Peer Model."""
peer_id: StrictStr
router_id: StrictStr
peer_id: str
router_id: str
type: FRRPeerType
class FRRPath(_FRRBase):
"""FRR Path Model."""
aspath: List[StrictInt]
aggregator_as: StrictInt
aggregator_id: StrictStr
med: StrictInt = 0
localpref: StrictInt
weight: StrictInt
valid: StrictBool
last_update: StrictInt
bestpath: StrictBool
community: List[StrictStr]
nexthops: List[FRRNextHop]
aspath: t.List[int]
aggregator_as: int
aggregator_id: str
med: int = 0
localpref: int
weight: int
valid: bool
last_update: int
bestpath: bool
community: t.List[str]
nexthops: t.List[FRRNextHop]
peer: FRRPeer
@root_validator(pre=True)
@model_validator(pre=True)
def validate_path(cls, values):
"""Extract meaningful data from FRR response."""
new = values.copy()
@ -77,8 +75,8 @@ class FRRPath(_FRRBase):
class FRRRoute(_FRRBase):
"""FRR Route Model."""
prefix: StrictStr
paths: List[FRRPath] = []
prefix: str
paths: t.List[FRRPath] = []
def serialize(self):
"""Convert the FRR-specific fields to standard parsed data model."""

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
"""Model utilities."""
# Standard Library
from typing import Any, Dict, Tuple
import typing as t
# Third Party
from pydantic import BaseModel
@ -34,7 +34,7 @@ class LegacyField(BaseModel):
required: bool = True
LEGACY_FIELDS: Dict[str, Tuple[LegacyField, ...]] = {
LEGACY_FIELDS: t.Dict[str, t.Tuple[LegacyField, ...]] = {
"Device": (
LegacyField(old="nos", new="platform", overwrite=True),
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."""
if model in LEGACY_FIELDS:
for field in LEGACY_FIELDS[model]:

View file

@ -5,7 +5,7 @@ import typing as t
from datetime import datetime
# Third Party
from pydantic import StrictStr, root_validator
from pydantic import model_validator, ConfigDict
# Project
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"
def to_snake_case(value: str) -> str:
"""Convert string to snake case."""
return value.replace("_", "-")
class WebhookHeaders(HyperglassModel):
"""Webhook data model."""
user_agent: t.Optional[StrictStr]
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]
model_config = ConfigDict(alias_generator=to_snake_case)
class Config:
"""Pydantic model config."""
fields = {
"user_agent": "user-agent",
"accept_encoding": "accept-encoding",
"accept_language": "accept-language",
"x_real_ip": "x-real-ip",
"x_forwarded_for": "x-forwarded-for",
}
user_agent: t.Optional[str] = None
referer: t.Optional[str] = None
accept_encoding: t.Optional[str] = None
accept_language: t.Optional[str] = None
x_real_ip: t.Optional[str] = None
x_forwarded_for: t.Optional[str] = None
class WebhookNetwork(HyperglassModel, extra="allow"):
class WebhookNetwork(HyperglassModel):
"""Webhook data model."""
prefix: StrictStr = "Unknown"
asn: StrictStr = "Unknown"
org: StrictStr = "Unknown"
country: StrictStr = "Unknown"
model_config = ConfigDict(extra="allow")
prefix: str = "Unknown"
asn: str = "Unknown"
org: str = "Unknown"
country: str = "Unknown"
class Webhook(HyperglassModel):
@ -55,26 +53,26 @@ class Webhook(HyperglassModel):
query_type: str
query_target: t.Union[t.List[str], str]
headers: WebhookHeaders
source: StrictStr = "Unknown"
source: str = "Unknown"
network: WebhookNetwork
timestamp: datetime
@root_validator(pre=True)
def validate_webhook(cls, values):
@model_validator(mode="before")
def validate_webhook(cls, model: "Webhook") -> "Webhook":
"""Reset network attributes if the source is localhost."""
if values.get("source") in ("127.0.0.1", "::1"):
values["network"] = {}
return values
if model.source in ("127.0.0.1", "::1"):
model.network = {}
return model
def msteams(self):
def msteams(self) -> t.Dict[str, t.Any]:
"""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."""
return f"`{str(value)}`"
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")
payload = {
@ -114,7 +112,7 @@ class Webhook(HyperglassModel):
return payload
def slack(self):
def slack(self) -> t.Dict[str, t.Any]:
"""Format the webhook data as a Slack message."""
def make_field(key, value, code=False):
@ -123,7 +121,7 @@ class Webhook(HyperglassModel):
return f"*{key}*\n{value}"
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)
header_data.append(field)

View file

@ -22,7 +22,7 @@ SupportedMethod = t.TypeVar("SupportedMethod")
class HyperglassPlugin(BaseModel, ABC):
"""Plugin to interact with device command output."""
__hyperglass_builtin__: bool = PrivateAttr(False)
_hyperglass_builtin: bool = PrivateAttr(False)
_type: t.ClassVar[str]
name: str
common: bool = False
@ -76,7 +76,7 @@ class HyperglassPlugin(BaseModel, ABC):
table = Table.grid(padding=(0, 1), expand=False)
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(
{
attr: getattr(self, attr)

View file

@ -93,7 +93,7 @@ def validate_large_community(value: str) -> bool:
class ValidateBGPCommunity(InputPlugin):
"""Validate a BGP community string."""
__hyperglass_builtin__: bool = PrivateAttr(True)
_hyperglass_builtin: bool = PrivateAttr(True)
def validate(self, query: "Query") -> "InputPluginValidationReturn":
"""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
for response in output:
try:
parsed: t.Dict = json.loads(response)
@ -68,7 +67,7 @@ def parse_arista(output: t.Sequence[str]) -> "OutputDataModel":
class BGPRoutePluginArista(OutputPlugin):
"""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",)
directives: t.Sequence[str] = (
"__hyperglass_arista_eos_bgp_route_table__",

View file

@ -119,7 +119,7 @@ def parse_juniper(output: Sequence[str]) -> "OutputDataModel": # noqa: C901
class BGPRoutePluginJuniper(OutputPlugin):
"""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",)
directives: Sequence[str] = (
"__hyperglass_juniper_bgp_route_table__",

View file

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

View file

@ -20,7 +20,7 @@ if TYPE_CHECKING:
class RemoveCommand(OutputPlugin):
"""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]:
"""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)
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.
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.
return sorted(
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,
)
@ -111,7 +111,7 @@ class PluginManager(t.Generic[PluginT]):
if issubclass(plugin, HyperglassPlugin):
instance = plugin(*args, **kwargs)
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)
else:
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)
for plugin in (*directives, *common):
log.debug("Output Plugin {!r} starting with\n{!r}", plugin.name, result)
result = plugin.process(output=result, query=query)
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
from .._builtin.bgp_route_arista import BGPRoutePluginArista
from ._fixtures import MockDevice
DEPENDS_KWARGS = {
"depends": [
@ -28,20 +29,17 @@ SAMPLE = Path(__file__).parent.parent.parent.parent / ".samples" / "arista_route
def _tester(sample: str):
plugin = BGPRoutePluginArista()
device = Device(
device = MockDevice(
name="Test Device",
address="127.0.0.1",
group="Test Network",
credential={"username": "", "password": ""},
platform="arista",
structured_output=True,
directives=[],
directives=["__hyperglass_arista_eos_bgp_route_table__"],
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})
result = plugin.process(output=(sample,), query=query)

View file

@ -10,11 +10,11 @@ import pytest
# Project
from hyperglass.log import log
from hyperglass.models.api.query import Query
from hyperglass.models.config.devices import Device
from hyperglass.models.data.bgp_route import BGPRouteTable
# Local
from .._builtin.bgp_route_juniper import BGPRoutePluginJuniper
from ._fixtures import MockDevice
DEPENDS_KWARGS = {
"depends": [
@ -32,7 +32,7 @@ AS_PATH = Path(__file__).parent.parent.parent.parent / ".samples" / "juniper_rou
def _tester(sample: str):
plugin = BGPRoutePluginJuniper()
device = Device(
device = MockDevice(
name="Test Device",
address="127.0.0.1",
group="Test Network",
@ -43,9 +43,6 @@ def _tester(sample: str):
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})
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__ = (
"at_least",
# "build_frontend",
# "build_ui",
"check_path",
"check_python",
"compare_dicts",

View file

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

View file

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

View file

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