forked from mirrors/thatmattlove-hyperglass
upgrade major dependencies
This commit is contained in:
parent
e00cccb0a1
commit
77c0a31256
62 changed files with 1036 additions and 1382 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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=[
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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=[
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from .response import (
|
|||
from .cert_import import EncodedRequest
|
||||
|
||||
__all__ = (
|
||||
"Query",
|
||||
"QueryError",
|
||||
"InfoResponse",
|
||||
"QueryResponse",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
"""hyperglass-agent certificate import models."""
|
||||
|
||||
# Standard Library
|
||||
from typing import Union
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
},
|
||||
}
|
||||
|
|
@ -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] = []
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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", ())
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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 {}",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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__",
|
||||
|
|
|
|||
|
|
@ -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__",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
6
hyperglass/plugins/tests/_fixtures.py
Normal file
6
hyperglass/plugins/tests/_fixtures.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from hyperglass.models.config.devices import Device
|
||||
|
||||
|
||||
class MockDevice(Device):
|
||||
def has_directives(self, *_: str) -> bool:
|
||||
return True
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue