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
|
# Third Party
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from fastapi.exceptions import ValidationError, RequestValidationError
|
from fastapi.exceptions import ValidationException, RequestValidationError
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||||
from fastapi.openapi.utils import get_openapi
|
from fastapi.openapi.utils import get_openapi
|
||||||
|
|
@ -99,7 +99,7 @@ app.add_exception_handler(HyperglassError, app_handler)
|
||||||
app.add_exception_handler(RequestValidationError, validation_handler)
|
app.add_exception_handler(RequestValidationError, validation_handler)
|
||||||
|
|
||||||
# App Validation Error Handler
|
# App Validation Error Handler
|
||||||
app.add_exception_handler(ValidationError, validation_handler)
|
app.add_exception_handler(ValidationException, validation_handler)
|
||||||
|
|
||||||
# Uncaught Error Handler
|
# Uncaught Error Handler
|
||||||
app.add_exception_handler(Exception, default_handler)
|
app.add_exception_handler(Exception, default_handler)
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ def init_params() -> "Params":
|
||||||
# from params.
|
# from params.
|
||||||
try:
|
try:
|
||||||
params.web.text.subtitle = params.web.text.subtitle.format(
|
params.web.text.subtitle = params.web.text.subtitle.format(
|
||||||
**params.dict(exclude={"web", "queries", "messages"})
|
**params.model_dump(exclude={"web", "queries", "messages"})
|
||||||
)
|
)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
"""Default Arista Directives."""
|
"""Default Arista Directives."""
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.models.directive import Rule, Text, BuiltinDirective
|
from hyperglass.models.directive import (
|
||||||
|
BuiltinDirective,
|
||||||
|
RuleWithIPv4,
|
||||||
|
RuleWithIPv6,
|
||||||
|
RuleWithPattern,
|
||||||
|
Text,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"AristaBGPRoute",
|
"AristaBGPRoute",
|
||||||
|
|
@ -18,12 +24,12 @@ AristaBGPRoute = BuiltinDirective(
|
||||||
id="__hyperglass_arista_eos_bgp_route__",
|
id="__hyperglass_arista_eos_bgp_route__",
|
||||||
name="BGP Route",
|
name="BGP Route",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="show ip bgp {target}",
|
command="show ip bgp {target}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="show ipv6 bgp {target}",
|
command="show ipv6 bgp {target}",
|
||||||
|
|
@ -38,7 +44,7 @@ AristaBGPASPath = BuiltinDirective(
|
||||||
id="__hyperglass_arista_eos_bgp_aspath__",
|
id="__hyperglass_arista_eos_bgp_aspath__",
|
||||||
name="BGP AS Path",
|
name="BGP AS Path",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -56,7 +62,7 @@ AristaBGPCommunity = BuiltinDirective(
|
||||||
id="__hyperglass_arista_eos_bgp_community__",
|
id="__hyperglass_arista_eos_bgp_community__",
|
||||||
name="BGP Community",
|
name="BGP Community",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -75,12 +81,12 @@ AristaPing = BuiltinDirective(
|
||||||
id="__hyperglass_arista_eos_ping__",
|
id="__hyperglass_arista_eos_ping__",
|
||||||
name="Ping",
|
name="Ping",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping ip {target} source {source4}",
|
command="ping ip {target} source {source4}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping ipv6 {target} source {source6}",
|
command="ping ipv6 {target} source {source6}",
|
||||||
|
|
@ -94,12 +100,12 @@ AristaTraceroute = BuiltinDirective(
|
||||||
id="__hyperglass_arista_eos_traceroute__",
|
id="__hyperglass_arista_eos_traceroute__",
|
||||||
name="Traceroute",
|
name="Traceroute",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="traceroute ip {target} source {source4}",
|
command="traceroute ip {target} source {source4}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="traceroute ipv6 {target} source {source6}",
|
command="traceroute ipv6 {target} source {source6}",
|
||||||
|
|
@ -115,12 +121,12 @@ AristaBGPRouteTable = BuiltinDirective(
|
||||||
id="__hyperglass_arista_eos_bgp_route_table__",
|
id="__hyperglass_arista_eos_bgp_route_table__",
|
||||||
name="BGP Route",
|
name="BGP Route",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="show ip bgp {target} | json",
|
command="show ip bgp {target} | json",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="show ipv6 bgp {target} | json",
|
command="show ipv6 bgp {target} | json",
|
||||||
|
|
@ -134,7 +140,7 @@ AristaBGPASPathTable = BuiltinDirective(
|
||||||
id="__hyperglass_arista_eos_bgp_aspath_table__",
|
id="__hyperglass_arista_eos_bgp_aspath_table__",
|
||||||
name="BGP AS Path",
|
name="BGP AS Path",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -151,7 +157,7 @@ AristaBGPCommunityTable = BuiltinDirective(
|
||||||
id="__hyperglass_arista_eos_bgp_community_table__",
|
id="__hyperglass_arista_eos_bgp_community_table__",
|
||||||
name="BGP Community",
|
name="BGP Community",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
"""Default BIRD Directives."""
|
"""Default BIRD Directives."""
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.models.directive import Rule, Text, BuiltinDirective
|
from hyperglass.models.directive import (
|
||||||
|
RuleWithIPv4,
|
||||||
|
RuleWithIPv6,
|
||||||
|
RuleWithPattern,
|
||||||
|
Text,
|
||||||
|
BuiltinDirective,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"BIRD_BGPASPath",
|
"BIRD_BGPASPath",
|
||||||
|
|
@ -15,12 +21,12 @@ BIRD_BGPRoute = BuiltinDirective(
|
||||||
id="__hyperglass_bird_bgp_route__",
|
id="__hyperglass_bird_bgp_route__",
|
||||||
name="BGP Route",
|
name="BGP Route",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command='birdc "show route all where {target} ~ net"',
|
command='birdc "show route all where {target} ~ net"',
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command='birdc "show route all where {target} ~ net"',
|
command='birdc "show route all where {target} ~ net"',
|
||||||
|
|
@ -34,7 +40,7 @@ BIRD_BGPASPath = BuiltinDirective(
|
||||||
id="__hyperglass_bird_bgp_aspath__",
|
id="__hyperglass_bird_bgp_aspath__",
|
||||||
name="BGP AS Path",
|
name="BGP AS Path",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -50,7 +56,7 @@ BIRD_BGPCommunity = BuiltinDirective(
|
||||||
id="__hyperglass_bird_bgp_community__",
|
id="__hyperglass_bird_bgp_community__",
|
||||||
name="BGP Community",
|
name="BGP Community",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -66,12 +72,12 @@ BIRD_Ping = BuiltinDirective(
|
||||||
id="__hyperglass_bird_ping__",
|
id="__hyperglass_bird_ping__",
|
||||||
name="Ping",
|
name="Ping",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping -4 -c 5 -I {source4} {target}",
|
command="ping -4 -c 5 -I {source4} {target}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping -6 -c 5 -I {source6} {target}",
|
command="ping -6 -c 5 -I {source6} {target}",
|
||||||
|
|
@ -85,12 +91,12 @@ BIRD_Traceroute = BuiltinDirective(
|
||||||
id="__hyperglass_bird_traceroute__",
|
id="__hyperglass_bird_traceroute__",
|
||||||
name="Traceroute",
|
name="Traceroute",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="traceroute -4 -w 1 -q 1 -s {source4} {target}",
|
command="traceroute -4 -w 1 -q 1 -s {source4} {target}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="traceroute -6 -w 1 -q 1 -s {source6} {target}",
|
command="traceroute -6 -w 1 -q 1 -s {source6} {target}",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
"""Default Cisco IOS Directives."""
|
"""Default Cisco IOS Directives."""
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.models.directive import Rule, Text, BuiltinDirective
|
from hyperglass.models.directive import (
|
||||||
|
RuleWithIPv4,
|
||||||
|
RuleWithIPv6,
|
||||||
|
RuleWithPattern,
|
||||||
|
Text,
|
||||||
|
BuiltinDirective,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"CiscoIOS_BGPASPath",
|
"CiscoIOS_BGPASPath",
|
||||||
|
|
@ -15,12 +21,12 @@ CiscoIOS_BGPRoute = BuiltinDirective(
|
||||||
id="__hyperglass_cisco_ios_bgp_route__",
|
id="__hyperglass_cisco_ios_bgp_route__",
|
||||||
name="BGP Route",
|
name="BGP Route",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="show bgp ipv4 unicast {target} | exclude pathid:|Epoch",
|
command="show bgp ipv4 unicast {target} | exclude pathid:|Epoch",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="show bgp ipv6 unicast {target} | exclude pathid:|Epoch",
|
command="show bgp ipv6 unicast {target} | exclude pathid:|Epoch",
|
||||||
|
|
@ -34,7 +40,7 @@ CiscoIOS_BGPASPath = BuiltinDirective(
|
||||||
id="__hyperglass_cisco_ios_bgp_aspath__",
|
id="__hyperglass_cisco_ios_bgp_aspath__",
|
||||||
name="BGP AS Path",
|
name="BGP AS Path",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -51,7 +57,7 @@ CiscoIOS_BGPCommunity = BuiltinDirective(
|
||||||
id="__hyperglass_cisco_ios_bgp_community__",
|
id="__hyperglass_cisco_ios_bgp_community__",
|
||||||
name="BGP Community",
|
name="BGP Community",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -68,12 +74,12 @@ CiscoIOS_Ping = BuiltinDirective(
|
||||||
id="__hyperglass_cisco_ios_ping__",
|
id="__hyperglass_cisco_ios_ping__",
|
||||||
name="Ping",
|
name="Ping",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping {target} repeat 5 source {source4}",
|
command="ping {target} repeat 5 source {source4}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping ipv6 {target} repeat 5 source {source6}",
|
command="ping ipv6 {target} repeat 5 source {source6}",
|
||||||
|
|
@ -87,12 +93,12 @@ CiscoIOS_Traceroute = BuiltinDirective(
|
||||||
id="__hyperglass_cisco_ios_traceroute__",
|
id="__hyperglass_cisco_ios_traceroute__",
|
||||||
name="Traceroute",
|
name="Traceroute",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="traceroute {target} timeout 1 probe 2 source {source4}",
|
command="traceroute {target} timeout 1 probe 2 source {source4}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="traceroute ipv6 {target} timeout 1 probe 2 source {source6}",
|
command="traceroute ipv6 {target} timeout 1 probe 2 source {source6}",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
"""Default Cisco NX-OS Directives."""
|
"""Default Cisco NX-OS Directives."""
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.models.directive import Rule, Text, BuiltinDirective
|
from hyperglass.models.directive import (
|
||||||
|
RuleWithIPv4,
|
||||||
|
RuleWithIPv6,
|
||||||
|
RuleWithPattern,
|
||||||
|
Text,
|
||||||
|
BuiltinDirective,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"CiscoNXOS_BGPASPath",
|
"CiscoNXOS_BGPASPath",
|
||||||
|
|
@ -15,12 +21,12 @@ CiscoNXOS_BGPRoute = BuiltinDirective(
|
||||||
id="__hyperglass_cisco_nxos_bgp_route__",
|
id="__hyperglass_cisco_nxos_bgp_route__",
|
||||||
name="BGP Route",
|
name="BGP Route",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="show bgp ipv4 unicast {target}",
|
command="show bgp ipv4 unicast {target}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="show bgp ipv6 unicast {target}",
|
command="show bgp ipv6 unicast {target}",
|
||||||
|
|
@ -34,7 +40,7 @@ CiscoNXOS_BGPASPath = BuiltinDirective(
|
||||||
id="__hyperglass_cisco_nxos_bgp_aspath__",
|
id="__hyperglass_cisco_nxos_bgp_aspath__",
|
||||||
name="BGP AS Path",
|
name="BGP AS Path",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -51,7 +57,7 @@ CiscoNXOS_BGPCommunity = BuiltinDirective(
|
||||||
id="__hyperglass_cisco_nxos_bgp_community__",
|
id="__hyperglass_cisco_nxos_bgp_community__",
|
||||||
name="BGP Community",
|
name="BGP Community",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -68,12 +74,12 @@ CiscoNXOS_Ping = BuiltinDirective(
|
||||||
id="__hyperglass_cisco_nxos_ping__",
|
id="__hyperglass_cisco_nxos_ping__",
|
||||||
name="Ping",
|
name="Ping",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping {target} source {source4}",
|
command="ping {target} source {source4}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping6 {target} source {source6}",
|
command="ping6 {target} source {source6}",
|
||||||
|
|
@ -87,12 +93,12 @@ CiscoNXOS_Traceroute = BuiltinDirective(
|
||||||
id="__hyperglass_cisco_nxos_traceroute__",
|
id="__hyperglass_cisco_nxos_traceroute__",
|
||||||
name="Traceroute",
|
name="Traceroute",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="traceroute {target} source {source4}",
|
command="traceroute {target} source {source4}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="traceroute6 {target} source {source6}",
|
command="traceroute6 {target} source {source6}",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
"""Default Cisco IOS-XR Directives."""
|
"""Default Cisco IOS-XR Directives."""
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.models.directive import Rule, Text, BuiltinDirective
|
from hyperglass.models.directive import (
|
||||||
|
RuleWithIPv4,
|
||||||
|
RuleWithIPv6,
|
||||||
|
RuleWithPattern,
|
||||||
|
Text,
|
||||||
|
BuiltinDirective,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"CiscoXR_BGPASPath",
|
"CiscoXR_BGPASPath",
|
||||||
|
|
@ -15,12 +21,12 @@ CiscoXR_BGPRoute = BuiltinDirective(
|
||||||
id="__hyperglass_cisco_xr_bgp_route__",
|
id="__hyperglass_cisco_xr_bgp_route__",
|
||||||
name="BGP Route",
|
name="BGP Route",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="show bgp ipv4 unicast {target}",
|
command="show bgp ipv4 unicast {target}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="show bgp ipv6 unicast {target}",
|
command="show bgp ipv6 unicast {target}",
|
||||||
|
|
@ -34,7 +40,7 @@ CiscoXR_BGPASPath = BuiltinDirective(
|
||||||
id="__hyperglass_cisco_xr_bgp_aspath__",
|
id="__hyperglass_cisco_xr_bgp_aspath__",
|
||||||
name="BGP AS Path",
|
name="BGP AS Path",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -51,7 +57,7 @@ CiscoXR_BGPCommunity = BuiltinDirective(
|
||||||
id="__hyperglass_cisco_xr_bgp_community__",
|
id="__hyperglass_cisco_xr_bgp_community__",
|
||||||
name="BGP Community",
|
name="BGP Community",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -68,12 +74,12 @@ CiscoXR_Ping = BuiltinDirective(
|
||||||
id="__hyperglass_cisco_xr_ping__",
|
id="__hyperglass_cisco_xr_ping__",
|
||||||
name="Ping",
|
name="Ping",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping ipv4 {target} count 5 source {source4}",
|
command="ping ipv4 {target} count 5 source {source4}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping ipv6 {target} count 5 source {source6}",
|
command="ping ipv6 {target} count 5 source {source6}",
|
||||||
|
|
@ -87,12 +93,12 @@ CiscoXR_Traceroute = BuiltinDirective(
|
||||||
id="__hyperglass_cisco_xr_traceroute__",
|
id="__hyperglass_cisco_xr_traceroute__",
|
||||||
name="Traceroute",
|
name="Traceroute",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="traceroute ipv4 {target} timeout 1 probe 2 source {source4}",
|
command="traceroute ipv4 {target} timeout 1 probe 2 source {source4}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="traceroute ipv6 {target} timeout 1 probe 2 source {source6}",
|
command="traceroute ipv6 {target} timeout 1 probe 2 source {source6}",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
"""Default FRRouting Directives."""
|
"""Default FRRouting Directives."""
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.models.directive import Rule, Text, BuiltinDirective
|
from hyperglass.models.directive import (
|
||||||
|
RuleWithIPv4,
|
||||||
|
RuleWithIPv6,
|
||||||
|
RuleWithPattern,
|
||||||
|
Text,
|
||||||
|
BuiltinDirective,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"FRRouting_BGPASPath",
|
"FRRouting_BGPASPath",
|
||||||
|
|
@ -15,12 +21,12 @@ FRRouting_BGPRoute = BuiltinDirective(
|
||||||
id="__hyperglass_frr_bgp_route__",
|
id="__hyperglass_frr_bgp_route__",
|
||||||
name="BGP Route",
|
name="BGP Route",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command='vtysh -c "show bgp ipv4 unicast {target}"',
|
command='vtysh -c "show bgp ipv4 unicast {target}"',
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command='vtysh -c "show bgp ipv6 unicast {target}"',
|
command='vtysh -c "show bgp ipv6 unicast {target}"',
|
||||||
|
|
@ -34,7 +40,7 @@ FRRouting_BGPASPath = BuiltinDirective(
|
||||||
id="__hyperglass_frr_bgp_aspath__",
|
id="__hyperglass_frr_bgp_aspath__",
|
||||||
name="BGP AS Path",
|
name="BGP AS Path",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -51,7 +57,7 @@ FRRouting_BGPCommunity = BuiltinDirective(
|
||||||
id="__hyperglass_frr_bgp_community__",
|
id="__hyperglass_frr_bgp_community__",
|
||||||
name="BGP Community",
|
name="BGP Community",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -68,12 +74,12 @@ FRRouting_Ping = BuiltinDirective(
|
||||||
id="__hyperglass_frr_ping__",
|
id="__hyperglass_frr_ping__",
|
||||||
name="Ping",
|
name="Ping",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping -4 -c 5 -I {source4} {target}",
|
command="ping -4 -c 5 -I {source4} {target}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping -6 -c 5 -I {source6} {target}",
|
command="ping -6 -c 5 -I {source6} {target}",
|
||||||
|
|
@ -87,12 +93,12 @@ FRRouting_Traceroute = BuiltinDirective(
|
||||||
id="__hyperglass_frr_traceroute__",
|
id="__hyperglass_frr_traceroute__",
|
||||||
name="Traceroute",
|
name="Traceroute",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="traceroute -4 -w 1 -q 1 -s {source4} {target}",
|
command="traceroute -4 -w 1 -q 1 -s {source4} {target}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="traceroute -6 -w 1 -q 1 -s {source6} {target}",
|
command="traceroute -6 -w 1 -q 1 -s {source6} {target}",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
"""Default Huawei Directives."""
|
"""Default Huawei Directives."""
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.models.directive import Rule, Text, BuiltinDirective
|
from hyperglass.models.directive import (
|
||||||
|
RuleWithIPv4,
|
||||||
|
RuleWithIPv6,
|
||||||
|
RuleWithPattern,
|
||||||
|
Text,
|
||||||
|
BuiltinDirective,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"Huawei_BGPASPath",
|
"Huawei_BGPASPath",
|
||||||
|
|
@ -15,12 +21,12 @@ Huawei_BGPRoute = BuiltinDirective(
|
||||||
id="__hyperglass_huawei_bgp_route__",
|
id="__hyperglass_huawei_bgp_route__",
|
||||||
name="BGP Route",
|
name="BGP Route",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="display bgp routing-table {target}",
|
command="display bgp routing-table {target}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="display bgp ipv6 routing-table {target}",
|
command="display bgp ipv6 routing-table {target}",
|
||||||
|
|
@ -34,7 +40,7 @@ Huawei_BGPASPath = BuiltinDirective(
|
||||||
id="__hyperglass_huawei_bgp_aspath__",
|
id="__hyperglass_huawei_bgp_aspath__",
|
||||||
name="BGP AS Path",
|
name="BGP AS Path",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -51,7 +57,7 @@ Huawei_BGPCommunity = BuiltinDirective(
|
||||||
id="__hyperglass_huawei_bgp_community__",
|
id="__hyperglass_huawei_bgp_community__",
|
||||||
name="BGP Community",
|
name="BGP Community",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -68,12 +74,12 @@ Huawei_Ping = BuiltinDirective(
|
||||||
id="__hyperglass_huawei_ping__",
|
id="__hyperglass_huawei_ping__",
|
||||||
name="Ping",
|
name="Ping",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping -c 5 -a {source4} {target}",
|
command="ping -c 5 -a {source4} {target}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping ipv6 -c 5 -a {source6} {target}",
|
command="ping ipv6 -c 5 -a {source6} {target}",
|
||||||
|
|
@ -87,12 +93,12 @@ Huawei_Traceroute = BuiltinDirective(
|
||||||
id="__hyperglass_huawei_traceroute__",
|
id="__hyperglass_huawei_traceroute__",
|
||||||
name="Traceroute",
|
name="Traceroute",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="tracert -q 2 -f 1 -a {source4} {target}",
|
command="tracert -q 2 -f 1 -a {source4} {target}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="tracert -q 2 -f 1 -a {source6} {target}",
|
command="tracert -q 2 -f 1 -a {source6} {target}",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
"""Default Juniper Directives."""
|
"""Default Juniper Directives."""
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.models.directive import Rule, Text, BuiltinDirective
|
from hyperglass.models.directive import (
|
||||||
|
RuleWithIPv4,
|
||||||
|
RuleWithIPv6,
|
||||||
|
RuleWithPattern,
|
||||||
|
Text,
|
||||||
|
BuiltinDirective,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"JuniperBGPRoute",
|
"JuniperBGPRoute",
|
||||||
|
|
@ -18,12 +24,12 @@ JuniperBGPRoute = BuiltinDirective(
|
||||||
id="__hyperglass_juniper_bgp_route__",
|
id="__hyperglass_juniper_bgp_route__",
|
||||||
name="BGP Route",
|
name="BGP Route",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="show route protocol bgp table inet.0 {target} detail",
|
command="show route protocol bgp table inet.0 {target} detail",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="show route protocol bgp table inet6.0 {target} detail",
|
command="show route protocol bgp table inet6.0 {target} detail",
|
||||||
|
|
@ -38,7 +44,7 @@ JuniperBGPASPath = BuiltinDirective(
|
||||||
id="__hyperglass_juniper_bgp_aspath__",
|
id="__hyperglass_juniper_bgp_aspath__",
|
||||||
name="BGP AS Path",
|
name="BGP AS Path",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -56,7 +62,7 @@ JuniperBGPCommunity = BuiltinDirective(
|
||||||
id="__hyperglass_juniper_bgp_community__",
|
id="__hyperglass_juniper_bgp_community__",
|
||||||
name="BGP Community",
|
name="BGP Community",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -75,12 +81,12 @@ JuniperPing = BuiltinDirective(
|
||||||
id="__hyperglass_juniper_ping__",
|
id="__hyperglass_juniper_ping__",
|
||||||
name="Ping",
|
name="Ping",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping inet {target} count 5 source {source4}",
|
command="ping inet {target} count 5 source {source4}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping inet6 {target} count 5 source {source6}",
|
command="ping inet6 {target} count 5 source {source6}",
|
||||||
|
|
@ -94,12 +100,12 @@ JuniperTraceroute = BuiltinDirective(
|
||||||
id="__hyperglass_juniper_traceroute__",
|
id="__hyperglass_juniper_traceroute__",
|
||||||
name="Traceroute",
|
name="Traceroute",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="traceroute inet {target} wait 1 source {source4}",
|
command="traceroute inet {target} wait 1 source {source4}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="traceroute inet6 {target} wait 1 source {source6}",
|
command="traceroute inet6 {target} wait 1 source {source6}",
|
||||||
|
|
@ -115,12 +121,12 @@ JuniperBGPRouteTable = BuiltinDirective(
|
||||||
id="__hyperglass_juniper_bgp_route_table__",
|
id="__hyperglass_juniper_bgp_route_table__",
|
||||||
name="BGP Route",
|
name="BGP Route",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="show route protocol bgp table inet.0 {target} best detail | display xml",
|
command="show route protocol bgp table inet.0 {target} best detail | display xml",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="show route protocol bgp table inet6.0 {target} best detail | display xml",
|
command="show route protocol bgp table inet6.0 {target} best detail | display xml",
|
||||||
|
|
@ -134,7 +140,7 @@ JuniperBGPASPathTable = BuiltinDirective(
|
||||||
id="__hyperglass_juniper_bgp_aspath_table__",
|
id="__hyperglass_juniper_bgp_aspath_table__",
|
||||||
name="BGP AS Path",
|
name="BGP AS Path",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -151,7 +157,7 @@ JuniperBGPCommunityTable = BuiltinDirective(
|
||||||
id="__hyperglass_juniper_bgp_community_table__",
|
id="__hyperglass_juniper_bgp_community_table__",
|
||||||
name="BGP Community",
|
name="BGP Community",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
"""Default Mikrotik Directives."""
|
"""Default Mikrotik Directives."""
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.models.directive import Rule, Text, BuiltinDirective
|
from hyperglass.models.directive import (
|
||||||
|
RuleWithIPv4,
|
||||||
|
RuleWithIPv6,
|
||||||
|
RuleWithPattern,
|
||||||
|
Text,
|
||||||
|
BuiltinDirective,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"Mikrotik_BGPASPath",
|
"Mikrotik_BGPASPath",
|
||||||
|
|
@ -15,12 +21,12 @@ Mikrotik_BGPRoute = BuiltinDirective(
|
||||||
id="__hyperglass_mikrotik_bgp_route__",
|
id="__hyperglass_mikrotik_bgp_route__",
|
||||||
name="BGP Route",
|
name="BGP Route",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ip route print where dst-address={target}",
|
command="ip route print where dst-address={target}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ipv6 route print where dst-address={target}",
|
command="ipv6 route print where dst-address={target}",
|
||||||
|
|
@ -34,7 +40,7 @@ Mikrotik_BGPASPath = BuiltinDirective(
|
||||||
id="__hyperglass_mikrotik_bgp_aspath__",
|
id="__hyperglass_mikrotik_bgp_aspath__",
|
||||||
name="BGP AS Path",
|
name="BGP AS Path",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -51,7 +57,7 @@ Mikrotik_BGPCommunity = BuiltinDirective(
|
||||||
id="__hyperglass_mikrotik_bgp_community__",
|
id="__hyperglass_mikrotik_bgp_community__",
|
||||||
name="BGP Community",
|
name="BGP Community",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -68,12 +74,12 @@ Mikrotik_Ping = BuiltinDirective(
|
||||||
id="__hyperglass_mikrotik_ping__",
|
id="__hyperglass_mikrotik_ping__",
|
||||||
name="Ping",
|
name="Ping",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping src-address={source4} count=5 {target}",
|
command="ping src-address={source4} count=5 {target}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping src-address={source6} count=5 {target}",
|
command="ping src-address={source6} count=5 {target}",
|
||||||
|
|
@ -87,12 +93,12 @@ Mikrotik_Traceroute = BuiltinDirective(
|
||||||
id="__hyperglass_mikrotik_traceroute__",
|
id="__hyperglass_mikrotik_traceroute__",
|
||||||
name="Traceroute",
|
name="Traceroute",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="tool traceroute src-address={source4} timeout=1 duration=5 count=1 {target}",
|
command="tool traceroute src-address={source4} timeout=1 duration=5 count=1 {target}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="tool traceroute src-address={source6} timeout=1 duration=5 count=1 {target}",
|
command="tool traceroute src-address={source6} timeout=1 duration=5 count=1 {target}",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
"""Default Nokia SR-OS Directives."""
|
"""Default Nokia SR-OS Directives."""
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.models.directive import Rule, Text, BuiltinDirective
|
from hyperglass.models.directive import (
|
||||||
|
RuleWithIPv4,
|
||||||
|
RuleWithIPv6,
|
||||||
|
RuleWithPattern,
|
||||||
|
Text,
|
||||||
|
BuiltinDirective,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"NokiaSROS_BGPASPath",
|
"NokiaSROS_BGPASPath",
|
||||||
|
|
@ -15,12 +21,12 @@ NokiaSROS_BGPRoute = BuiltinDirective(
|
||||||
id="__hyperglass_nokia_sros_bgp_route__",
|
id="__hyperglass_nokia_sros_bgp_route__",
|
||||||
name="BGP Route",
|
name="BGP Route",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="/show router bgp routes {target} ipv4 hunt",
|
command="/show router bgp routes {target} ipv4 hunt",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="/show router bgp routes {target} ipv6 hunt",
|
command="/show router bgp routes {target} ipv6 hunt",
|
||||||
|
|
@ -34,7 +40,7 @@ NokiaSROS_BGPASPath = BuiltinDirective(
|
||||||
id="__hyperglass_nokia_sros_bgp_aspath__",
|
id="__hyperglass_nokia_sros_bgp_aspath__",
|
||||||
name="BGP AS Path",
|
name="BGP AS Path",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -50,7 +56,7 @@ NokiaSROS_BGPCommunity = BuiltinDirective(
|
||||||
id="__hyperglass_nokia_sros_bgp_community__",
|
id="__hyperglass_nokia_sros_bgp_community__",
|
||||||
name="BGP Community",
|
name="BGP Community",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -66,12 +72,12 @@ NokiaSROS_Ping = BuiltinDirective(
|
||||||
id="__hyperglass_nokia_sros_ping__",
|
id="__hyperglass_nokia_sros_ping__",
|
||||||
name="Ping",
|
name="Ping",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="/ping {target} source-address {source4}",
|
command="/ping {target} source-address {source4}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="/ping {target} source-address {source6}",
|
command="/ping {target} source-address {source6}",
|
||||||
|
|
@ -85,12 +91,12 @@ NokiaSROS_Traceroute = BuiltinDirective(
|
||||||
id="__hyperglass_nokia_sros_traceroute__",
|
id="__hyperglass_nokia_sros_traceroute__",
|
||||||
name="Traceroute",
|
name="Traceroute",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="/traceroute {target} source-address {source4} wait 2 seconds",
|
command="/traceroute {target} source-address {source4} wait 2 seconds",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="/traceroute {target} source-address {source6} wait 2 seconds",
|
command="/traceroute {target} source-address {source6} wait 2 seconds",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
"""Default FRRouting Directives."""
|
"""Default FRRouting Directives."""
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.models.directive import Rule, Text, BuiltinDirective
|
from hyperglass.models.directive import (
|
||||||
|
RuleWithIPv4,
|
||||||
|
RuleWithIPv6,
|
||||||
|
RuleWithPattern,
|
||||||
|
Text,
|
||||||
|
BuiltinDirective,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"OpenBGPD_BGPASPath",
|
"OpenBGPD_BGPASPath",
|
||||||
|
|
@ -15,12 +21,12 @@ OpenBGPD_BGPRoute = BuiltinDirective(
|
||||||
id="__hyperglass_openbgpd_bgp_route__",
|
id="__hyperglass_openbgpd_bgp_route__",
|
||||||
name="BGP Route",
|
name="BGP Route",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="bgpctl show rib inet {target}",
|
command="bgpctl show rib inet {target}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="bgpctl show rib inet6 {target}",
|
command="bgpctl show rib inet6 {target}",
|
||||||
|
|
@ -34,7 +40,7 @@ OpenBGPD_BGPASPath = BuiltinDirective(
|
||||||
id="__hyperglass_openbgpd_bgp_aspath__",
|
id="__hyperglass_openbgpd_bgp_aspath__",
|
||||||
name="BGP AS Path",
|
name="BGP AS Path",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -51,7 +57,7 @@ OpenBGPD_BGPCommunity = BuiltinDirective(
|
||||||
id="__hyperglass_openbgpd_bgp_community__",
|
id="__hyperglass_openbgpd_bgp_community__",
|
||||||
name="BGP Community",
|
name="BGP Community",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -68,12 +74,12 @@ OpenBGPD_Ping = BuiltinDirective(
|
||||||
id="__hyperglass_openbgpd_ping__",
|
id="__hyperglass_openbgpd_ping__",
|
||||||
name="Ping",
|
name="Ping",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping -4 -c 5 -I {source4} {target}",
|
command="ping -4 -c 5 -I {source4} {target}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping -6 -c 5 -I {source6} {target}",
|
command="ping -6 -c 5 -I {source6} {target}",
|
||||||
|
|
@ -87,12 +93,12 @@ OpenBGPD_Traceroute = BuiltinDirective(
|
||||||
id="__hyperglass_openbgpd_traceroute__",
|
id="__hyperglass_openbgpd_traceroute__",
|
||||||
name="Traceroute",
|
name="Traceroute",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="traceroute -4 -w 1 -q 1 -s {source4} {target}",
|
command="traceroute -4 -w 1 -q 1 -s {source4} {target}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="traceroute -6 -w 1 -q 1 -s {source6} {target}",
|
command="traceroute -6 -w 1 -q 1 -s {source6} {target}",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
"""Default TNSR Directives."""
|
"""Default TNSR Directives."""
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.models.directive import Rule, Text, BuiltinDirective
|
from hyperglass.models.directive import (
|
||||||
|
RuleWithIPv4,
|
||||||
|
RuleWithIPv6,
|
||||||
|
RuleWithPattern,
|
||||||
|
Text,
|
||||||
|
BuiltinDirective,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"TNSR_BGPASPath",
|
"TNSR_BGPASPath",
|
||||||
|
|
@ -15,12 +21,12 @@ TNSR_BGPRoute = BuiltinDirective(
|
||||||
id="__hyperglass_tnsr_bgp_route__",
|
id="__hyperglass_tnsr_bgp_route__",
|
||||||
name="BGP Route",
|
name="BGP Route",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command='dataplane shell sudo vtysh -c "show bgp ipv4 unicast {target}"',
|
command='dataplane shell sudo vtysh -c "show bgp ipv4 unicast {target}"',
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command='dataplane shell sudo vtysh -c "show bgp ipv6 unicast {target}"',
|
command='dataplane shell sudo vtysh -c "show bgp ipv6 unicast {target}"',
|
||||||
|
|
@ -34,7 +40,7 @@ TNSR_BGPASPath = BuiltinDirective(
|
||||||
id="__hyperglass_tnsr_bgp_aspath__",
|
id="__hyperglass_tnsr_bgp_aspath__",
|
||||||
name="BGP AS Path",
|
name="BGP AS Path",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -51,7 +57,7 @@ TNSR_BGPCommunity = BuiltinDirective(
|
||||||
id="__hyperglass_tnsr_bgp_community__",
|
id="__hyperglass_tnsr_bgp_community__",
|
||||||
name="BGP Community",
|
name="BGP Community",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -68,12 +74,12 @@ TNSR_Ping = BuiltinDirective(
|
||||||
id="__hyperglass_tnsr_ping__",
|
id="__hyperglass_tnsr_ping__",
|
||||||
name="Ping",
|
name="Ping",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping {target} ipv4 source {source4} count 5 timeout 1",
|
command="ping {target} ipv4 source {source4} count 5 timeout 1",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping {target} ipv6 source {source6} count 5 timeout 1",
|
command="ping {target} ipv6 source {source6} count 5 timeout 1",
|
||||||
|
|
@ -87,12 +93,12 @@ TNSR_Traceroute = BuiltinDirective(
|
||||||
id="__hyperglass_tnsr_traceroute__",
|
id="__hyperglass_tnsr_traceroute__",
|
||||||
name="Traceroute",
|
name="Traceroute",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="traceroute {target} ipv4 source {source4} timeout 1 waittime 1",
|
command="traceroute {target} ipv4 source {source4} timeout 1 waittime 1",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="traceroute {target} ipv6 source {source6} timeout 1 waittime 1",
|
command="traceroute {target} ipv6 source {source6} timeout 1 waittime 1",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
"""Default VyOS Directives."""
|
"""Default VyOS Directives."""
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.models.directive import Rule, Text, BuiltinDirective
|
from hyperglass.models.directive import (
|
||||||
|
RuleWithIPv4,
|
||||||
|
RuleWithIPv6,
|
||||||
|
RuleWithPattern,
|
||||||
|
Text,
|
||||||
|
BuiltinDirective,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"VyOS_BGPASPath",
|
"VyOS_BGPASPath",
|
||||||
|
|
@ -15,12 +21,12 @@ VyOS_BGPRoute = BuiltinDirective(
|
||||||
id="__hyperglass_vyos_bgp_route__",
|
id="__hyperglass_vyos_bgp_route__",
|
||||||
name="BGP Route",
|
name="BGP Route",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="show ip bgp {target}",
|
command="show ip bgp {target}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="show ipv6 bgp {target}",
|
command="show ipv6 bgp {target}",
|
||||||
|
|
@ -34,7 +40,7 @@ VyOS_BGPASPath = BuiltinDirective(
|
||||||
id="__hyperglass_vyos_bgp_aspath__",
|
id="__hyperglass_vyos_bgp_aspath__",
|
||||||
name="BGP AS Path",
|
name="BGP AS Path",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -51,7 +57,7 @@ VyOS_BGPCommunity = BuiltinDirective(
|
||||||
id="__hyperglass_vyos_bgp_community__",
|
id="__hyperglass_vyos_bgp_community__",
|
||||||
name="BGP Community",
|
name="BGP Community",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithPattern(
|
||||||
condition="*",
|
condition="*",
|
||||||
action="permit",
|
action="permit",
|
||||||
commands=[
|
commands=[
|
||||||
|
|
@ -68,12 +74,12 @@ VyOS_Ping = BuiltinDirective(
|
||||||
id="__hyperglass_vyos_ping__",
|
id="__hyperglass_vyos_ping__",
|
||||||
name="Ping",
|
name="Ping",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping {target} count 5 interface {source4}",
|
command="ping {target} count 5 interface {source4}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="ping {target} count 5 interface {source6}",
|
command="ping {target} count 5 interface {source6}",
|
||||||
|
|
@ -87,12 +93,12 @@ VyOS_Traceroute = BuiltinDirective(
|
||||||
id="__hyperglass_vyos_traceroute__",
|
id="__hyperglass_vyos_traceroute__",
|
||||||
name="Traceroute",
|
name="Traceroute",
|
||||||
rules=[
|
rules=[
|
||||||
Rule(
|
RuleWithIPv4(
|
||||||
condition="0.0.0.0/0",
|
condition="0.0.0.0/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="mtr -4 -G 1 -c 1 -w -o SAL -a {source4} {target}",
|
command="mtr -4 -G 1 -c 1 -w -o SAL -a {source4} {target}",
|
||||||
),
|
),
|
||||||
Rule(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="mtr -6 -G 1 -c 1 -w -o SAL -a {source6} {target}",
|
command="mtr -6 -G 1 -c 1 -w -o SAL -a {source6} {target}",
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ class HttpClient(Connection):
|
||||||
key: value.format(
|
key: value.format(
|
||||||
**{
|
**{
|
||||||
str(v): str(getattr(self.query_data, k, None))
|
str(v): str(getattr(self.query_data, k, None))
|
||||||
for k, v in self.config.attribute_map.dict().items()
|
for k, v in self.config.attribute_map.model_dump().items()
|
||||||
if v in get_fmt_keys(value)
|
if v in get_fmt_keys(value)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -107,10 +107,10 @@ class HttpClient(Connection):
|
||||||
|
|
||||||
responses += (data,)
|
responses += (data,)
|
||||||
|
|
||||||
except (httpx.TimeoutException) as error:
|
except httpx.TimeoutException as error:
|
||||||
raise DeviceTimeout(error=error, device=self.device) from error
|
raise DeviceTimeout(error=error, device=self.device) from error
|
||||||
|
|
||||||
except (httpx.HTTPStatusError) as error:
|
except httpx.HTTPStatusError as error:
|
||||||
if error.response.status_code == 401:
|
if error.response.status_code == 401:
|
||||||
raise AuthError(error=error, device=self.device) from error
|
raise AuthError(error=error, device=self.device) from error
|
||||||
raise RestError(error=error, device=self.device) from error
|
raise RestError(error=error, device=self.device) from error
|
||||||
|
|
|
||||||
|
|
@ -338,6 +338,7 @@ async def build_frontend( # noqa: C901
|
||||||
}
|
}
|
||||||
|
|
||||||
build_json = json.dumps(build_data, default=str)
|
build_json = json.dumps(build_data, default=str)
|
||||||
|
log.debug("UI Build Data:\n{}", build_json)
|
||||||
|
|
||||||
# Create SHA256 hash from all parameters passed to UI, use as
|
# Create SHA256 hash from all parameters passed to UI, use as
|
||||||
# build identifier.
|
# build identifier.
|
||||||
|
|
|
||||||
|
|
@ -119,9 +119,9 @@ def on_exit(_: t.Any) -> None:
|
||||||
"""Gunicorn shutdown tasks."""
|
"""Gunicorn shutdown tasks."""
|
||||||
|
|
||||||
state = use_state()
|
state = use_state()
|
||||||
state.clear()
|
if not Settings.dev_mode:
|
||||||
|
state.clear()
|
||||||
log.info("Cleared hyperglass state")
|
log.info("Cleared hyperglass state")
|
||||||
|
|
||||||
unregister_all_plugins()
|
unregister_all_plugins()
|
||||||
|
|
||||||
|
|
@ -195,6 +195,7 @@ def run(_workers: int = None):
|
||||||
|
|
||||||
start(log_level=log_level, workers=workers)
|
start(log_level=log_level, workers=workers)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
|
log.critical(error)
|
||||||
# Handle app exceptions.
|
# Handle app exceptions.
|
||||||
if not Settings.dev_mode:
|
if not Settings.dev_mode:
|
||||||
state = use_state()
|
state = use_state()
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from .response import (
|
||||||
from .cert_import import EncodedRequest
|
from .cert_import import EncodedRequest
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
"Query",
|
||||||
"QueryError",
|
"QueryError",
|
||||||
"InfoResponse",
|
"InfoResponse",
|
||||||
"QueryResponse",
|
"QueryResponse",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""hyperglass-agent certificate import models."""
|
"""hyperglass-agent certificate import models."""
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import secrets
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import BaseModel, constr, validator
|
from pydantic import BaseModel, constr, field_validator, ConfigDict, Field
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
|
|
@ -29,17 +29,12 @@ QueryType = constr(strip_whitespace=True, strict=True, min_length=1)
|
||||||
class Query(BaseModel):
|
class Query(BaseModel):
|
||||||
"""Validation model for input query parameters."""
|
"""Validation model for input query parameters."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="allow", alias_generator=snake_to_camel, populate_by_name=True)
|
||||||
|
|
||||||
query_location: QueryLocation # Device `name` field
|
query_location: QueryLocation # Device `name` field
|
||||||
query_target: t.Union[t.List[QueryTarget], QueryTarget]
|
query_target: t.Union[t.List[QueryTarget], QueryTarget]
|
||||||
query_type: QueryType # Directive `id` field
|
query_type: QueryType # Directive `id` field
|
||||||
|
|
||||||
class Config:
|
|
||||||
"""Pydantic model configuration."""
|
|
||||||
|
|
||||||
extra = "allow"
|
|
||||||
alias_generator = snake_to_camel
|
|
||||||
allow_population_by_field_name = True
|
|
||||||
|
|
||||||
def __init__(self, **data) -> None:
|
def __init__(self, **data) -> None:
|
||||||
"""Initialize the query with a UTC timestamp at initialization time."""
|
"""Initialize the query with a UTC timestamp at initialization time."""
|
||||||
super().__init__(**data)
|
super().__init__(**data)
|
||||||
|
|
@ -96,14 +91,14 @@ class Query(BaseModel):
|
||||||
|
|
||||||
def dict(self) -> t.Dict[str, t.Union[t.List[str], str]]:
|
def dict(self) -> t.Dict[str, t.Union[t.List[str], str]]:
|
||||||
"""Include only public fields."""
|
"""Include only public fields."""
|
||||||
return super().dict(include={"query_location", "query_target", "query_type"})
|
return super().model_dump(include={"query_location", "query_target", "query_type"})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device(self) -> Device:
|
def device(self) -> Device:
|
||||||
"""Get this query's device object by query_location."""
|
"""Get this query's device object by query_location."""
|
||||||
return self._state.devices[self.query_location]
|
return self._state.devices[self.query_location]
|
||||||
|
|
||||||
@validator("query_location")
|
@field_validator("query_location")
|
||||||
def validate_query_location(cls, value):
|
def validate_query_location(cls, value):
|
||||||
"""Ensure query_location is defined."""
|
"""Ensure query_location is defined."""
|
||||||
|
|
||||||
|
|
@ -114,7 +109,7 @@ class Query(BaseModel):
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@validator("query_type")
|
@field_validator("query_type")
|
||||||
def validate_query_type(cls, value: t.Any):
|
def validate_query_type(cls, value: t.Any):
|
||||||
"""Ensure a requested query type exists."""
|
"""Ensure a requested query type exists."""
|
||||||
devices = use_state("devices")
|
devices = use_state("devices")
|
||||||
|
|
|
||||||
|
|
@ -4,25 +4,132 @@
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import BaseModel, StrictInt, StrictStr, StrictBool, constr, validator
|
from pydantic import (
|
||||||
|
BaseModel,
|
||||||
|
StrictInt,
|
||||||
|
StrictStr,
|
||||||
|
StrictBool,
|
||||||
|
field_validator,
|
||||||
|
Field,
|
||||||
|
ConfigDict,
|
||||||
|
)
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.state import use_state
|
from hyperglass.state import use_state
|
||||||
|
|
||||||
ErrorName = constr(regex=r"(success|warning|error|danger)")
|
ErrorName = t.Literal["success", "warning", "error", "danger"]
|
||||||
ResponseLevel = constr(regex=r"success")
|
ResponseLevel = t.Literal["success"]
|
||||||
ResponseFormat = constr(regex=r"(application\/json|text\/plain)")
|
ResponseFormat = t.Literal[r"text/plain", r"application/json"]
|
||||||
|
|
||||||
|
schema_query_output = {
|
||||||
|
"title": "Output",
|
||||||
|
"description": "Looking Glass Response",
|
||||||
|
"example": """
|
||||||
|
BGP routing table entry for 1.1.1.0/24, version 224184946
|
||||||
|
BGP Bestpath: deterministic-med
|
||||||
|
Paths: (12 available, best #1, table default)
|
||||||
|
Advertised to update-groups:
|
||||||
|
1 40
|
||||||
|
13335, (aggregated by 13335 172.68.129.1), (received & used)
|
||||||
|
192.0.2.1 (metric 51) from 192.0.2.1 (192.0.2.1)
|
||||||
|
Origin IGP, metric 0, localpref 250, valid, internal
|
||||||
|
Community: 65000:1 65000:2
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
|
||||||
|
schema_query_level = {"title": "Level", "description": "Severity"}
|
||||||
|
|
||||||
|
schema_query_random = {
|
||||||
|
"title": "Random",
|
||||||
|
"description": "Random string to prevent client or intermediate caching.",
|
||||||
|
"example": "504cbdb47eb8310ca237bf512c3e10b44b0a3d85868c4b64a20037dc1c3ef857",
|
||||||
|
}
|
||||||
|
|
||||||
|
schema_query_cached = {
|
||||||
|
"title": "Cached",
|
||||||
|
"description": "`true` if the response is from a previously cached query.",
|
||||||
|
}
|
||||||
|
|
||||||
|
schema_query_runtime = {
|
||||||
|
"title": "Runtime",
|
||||||
|
"description": "Time it took to run the query in seconds.",
|
||||||
|
"example": 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
schema_query_keywords = {
|
||||||
|
"title": "Keywords",
|
||||||
|
"description": "Relevant keyword values contained in the `output` field, which can be used for formatting.",
|
||||||
|
"example": ["1.1.1.0/24", "best #1"],
|
||||||
|
}
|
||||||
|
|
||||||
|
schema_query_timestamp = {
|
||||||
|
"title": "Timestamp",
|
||||||
|
"description": "UTC Time at which the backend application received the query.",
|
||||||
|
"example": "2020-04-18 14:45:37",
|
||||||
|
}
|
||||||
|
|
||||||
|
schema_query_format = {
|
||||||
|
"title": "Format",
|
||||||
|
"description": "Response [MIME Type](http://www.iana.org/assignments/media-types/media-types.xhtml). Supported values: `text/plain` and `application/json`.",
|
||||||
|
"example": "text/plain",
|
||||||
|
}
|
||||||
|
|
||||||
|
schema_query_examples = [
|
||||||
|
{
|
||||||
|
"output": """
|
||||||
|
BGP routing table entry for 1.1.1.0/24, version 224184946
|
||||||
|
BGP Bestpath: deterministic-med
|
||||||
|
Paths: (12 available, best #1, table default)
|
||||||
|
Advertised to update-groups:
|
||||||
|
1 40
|
||||||
|
13335, (aggregated by 13335 172.68.129.1), (received & used)
|
||||||
|
192.0.2.1 (metric 51) from 192.0.2.1 (192.0.2.1)
|
||||||
|
Origin IGP, metric 0, localpref 250, valid, internal
|
||||||
|
Community: 65000:1 65000:2
|
||||||
|
""",
|
||||||
|
"level": "success",
|
||||||
|
"keywords": ["1.1.1.0/24", "best #1"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
schema_query_error_output = {
|
||||||
|
"title": "Output",
|
||||||
|
"description": "Error Details",
|
||||||
|
"example": "192.0.2.1/32 is not allowed.",
|
||||||
|
}
|
||||||
|
|
||||||
|
schema_query_error_level = {"title": "Level", "description": "Error Severity", "example": "danger"}
|
||||||
|
|
||||||
|
schema_query_error_keywords = {
|
||||||
|
"title": "Keywords",
|
||||||
|
"description": "Relevant keyword values contained in the `output` field, which can be used for formatting.",
|
||||||
|
"example": ["192.0.2.1/32"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class QueryError(BaseModel):
|
class QueryError(BaseModel):
|
||||||
"""Query response model."""
|
"""Query response model."""
|
||||||
|
|
||||||
output: StrictStr
|
model_config = ConfigDict(
|
||||||
level: ErrorName = "danger"
|
json_schema_extra={
|
||||||
id: t.Optional[StrictStr]
|
"title": "Query Error",
|
||||||
keywords: t.List[StrictStr] = []
|
"description": "Response received when there is an error executing the requested query.",
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"output": "192.0.2.1/32 is not allowed.",
|
||||||
|
"level": "danger",
|
||||||
|
"keywords": ["192.0.2.1/32"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@validator("output")
|
output: str = Field(json_schema_extra=schema_query_error_output)
|
||||||
|
level: ErrorName = Field("danger", json_schema_extra=schema_query_error_level)
|
||||||
|
# id: t.Optional[StrictStr]
|
||||||
|
keywords: t.List[StrictStr] = Field([], json_schema_extra=schema_query_error_keywords)
|
||||||
|
|
||||||
|
@field_validator("output")
|
||||||
def validate_output(cls: "QueryError", value):
|
def validate_output(cls: "QueryError", value):
|
||||||
"""If no output is specified, use a customizable generic message."""
|
"""If no output is specified, use a customizable generic message."""
|
||||||
if value is None:
|
if value is None:
|
||||||
|
|
@ -30,138 +137,45 @@ class QueryError(BaseModel):
|
||||||
return messages.general
|
return messages.general
|
||||||
return value
|
return value
|
||||||
|
|
||||||
class Config:
|
|
||||||
"""Pydantic model configuration."""
|
|
||||||
|
|
||||||
title = "Query Error"
|
|
||||||
description = "Response received when there is an error executing the requested query."
|
|
||||||
fields = {
|
|
||||||
"output": {
|
|
||||||
"title": "Output",
|
|
||||||
"description": "Error Details",
|
|
||||||
"example": "192.0.2.1/32 is not allowed.",
|
|
||||||
},
|
|
||||||
"level": {"title": "Level", "description": "Error Severity", "example": "danger"},
|
|
||||||
"keywords": {
|
|
||||||
"title": "Keywords",
|
|
||||||
"description": "Relevant keyword values contained in the `output` field, which can be used for formatting.",
|
|
||||||
"example": ["192.0.2.1/32"],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
schema_extra = {
|
|
||||||
"examples": [
|
|
||||||
{
|
|
||||||
"output": "192.0.2.1/32 is not allowed.",
|
|
||||||
"level": "danger",
|
|
||||||
"keywords": ["192.0.2.1/32"],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class QueryResponse(BaseModel):
|
class QueryResponse(BaseModel):
|
||||||
"""Query response model."""
|
"""Query response model."""
|
||||||
|
|
||||||
output: t.Union[t.Dict, StrictStr]
|
model_config = ConfigDict(
|
||||||
level: ResponseLevel = "success"
|
json_schema_extra={
|
||||||
random: StrictStr
|
"title": "Query Response",
|
||||||
cached: StrictBool
|
"description": "Looking glass response",
|
||||||
runtime: StrictInt
|
"examples": schema_query_examples,
|
||||||
keywords: t.List[StrictStr] = []
|
|
||||||
timestamp: StrictStr
|
|
||||||
format: ResponseFormat = "text/plain"
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
"""Pydantic model configuration."""
|
|
||||||
|
|
||||||
title = "Query Response"
|
|
||||||
description = "Looking glass response"
|
|
||||||
fields = {
|
|
||||||
"level": {"title": "Level", "description": "Severity"},
|
|
||||||
"cached": {
|
|
||||||
"title": "Cached",
|
|
||||||
"description": "`true` if the response is from a previously cached query.",
|
|
||||||
},
|
|
||||||
"random": {
|
|
||||||
"title": "Random",
|
|
||||||
"description": "Random string to prevent client or intermediate caching.",
|
|
||||||
"example": "504cbdb47eb8310ca237bf512c3e10b44b0a3d85868c4b64a20037dc1c3ef857",
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"title": "Runtime",
|
|
||||||
"description": "Time it took to run the query in seconds.",
|
|
||||||
"example": 6,
|
|
||||||
},
|
|
||||||
"timestamp": {
|
|
||||||
"title": "Timestamp",
|
|
||||||
"description": "UTC Time at which the backend application received the query.",
|
|
||||||
"example": "2020-04-18 14:45:37",
|
|
||||||
},
|
|
||||||
"format": {
|
|
||||||
"title": "Format",
|
|
||||||
"description": "Response [MIME Type](http://www.iana.org/assignments/media-types/media-types.xhtml). Supported values: `text/plain` and `application/json`.",
|
|
||||||
"example": "text/plain",
|
|
||||||
},
|
|
||||||
"keywords": {
|
|
||||||
"title": "Keywords",
|
|
||||||
"description": "Relevant keyword values contained in the `output` field, which can be used for formatting.",
|
|
||||||
"example": ["1.1.1.0/24", "best #1"],
|
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"title": "Output",
|
|
||||||
"description": "Looking Glass Response",
|
|
||||||
"example": """
|
|
||||||
BGP routing table entry for 1.1.1.0/24, version 224184946
|
|
||||||
BGP Bestpath: deterministic-med
|
|
||||||
Paths: (12 available, best #1, table default)
|
|
||||||
Advertised to update-groups:
|
|
||||||
1 40
|
|
||||||
13335, (aggregated by 13335 172.68.129.1), (received & used)
|
|
||||||
192.0.2.1 (metric 51) from 192.0.2.1 (192.0.2.1)
|
|
||||||
Origin IGP, metric 0, localpref 250, valid, internal
|
|
||||||
Community: 65000:1 65000:2
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
schema_extra = {
|
|
||||||
"examples": [
|
|
||||||
{
|
|
||||||
"output": """
|
|
||||||
BGP routing table entry for 1.1.1.0/24, version 224184946
|
|
||||||
BGP Bestpath: deterministic-med
|
|
||||||
Paths: (12 available, best #1, table default)
|
|
||||||
Advertised to update-groups:
|
|
||||||
1 40
|
|
||||||
13335, (aggregated by 13335 172.68.129.1), (received & used)
|
|
||||||
192.0.2.1 (metric 51) from 192.0.2.1 (192.0.2.1)
|
|
||||||
Origin IGP, metric 0, localpref 250, valid, internal
|
|
||||||
Community: 65000:1 65000:2
|
|
||||||
""",
|
|
||||||
"level": "success",
|
|
||||||
"keywords": ["1.1.1.0/24", "best #1"],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
output: t.Union[t.Dict, StrictStr] = Field(json_schema_extra=schema_query_output)
|
||||||
|
level: ResponseLevel = Field("success", json_schema_extra=schema_query_level)
|
||||||
|
random: str = Field(json_schema_extra=schema_query_random)
|
||||||
|
cached: bool = Field(json_schema_extra=schema_query_cached)
|
||||||
|
runtime: int = Field(json_schema_extra=schema_query_runtime)
|
||||||
|
keywords: t.List[str] = Field([], json_schema_extra=schema_query_keywords)
|
||||||
|
timestamp: str = Field(json_schema_extra=schema_query_timestamp)
|
||||||
|
format: ResponseFormat = Field("text/plain", json_schema_extra=schema_query_format)
|
||||||
|
|
||||||
|
|
||||||
class RoutersResponse(BaseModel):
|
class RoutersResponse(BaseModel):
|
||||||
"""Response model for /api/devices list items."""
|
"""Response model for /api/devices list items."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
json_schema_extra={
|
||||||
|
"title": "Device",
|
||||||
|
"description": "Device attributes",
|
||||||
|
"examples": [
|
||||||
|
{"id": "nyc_router_1", "name": "NYC Router 1", "group": "New York City, NY"}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
id: StrictStr
|
id: StrictStr
|
||||||
name: StrictStr
|
name: StrictStr
|
||||||
group: t.Union[StrictStr, None]
|
group: t.Union[StrictStr, None]
|
||||||
|
|
||||||
class Config:
|
|
||||||
"""Pydantic model configuration."""
|
|
||||||
|
|
||||||
title = "Device"
|
|
||||||
description = "Device attributes"
|
|
||||||
schema_extra = {
|
|
||||||
"examples": [
|
|
||||||
{"id": "nyc_router_1", "name": "NYC Router 1", "group": "New York City, NY"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class CommunityResponse(BaseModel):
|
class CommunityResponse(BaseModel):
|
||||||
"""Response model for /api/communities."""
|
"""Response model for /api/communities."""
|
||||||
|
|
@ -174,36 +188,26 @@ class CommunityResponse(BaseModel):
|
||||||
class SupportedQueryResponse(BaseModel):
|
class SupportedQueryResponse(BaseModel):
|
||||||
"""Response model for /api/queries list items."""
|
"""Response model for /api/queries list items."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
json_schema_extra={
|
||||||
|
"title": "Query Type",
|
||||||
|
"description": "If enabled is `true`, the `name` field may be used to specify the query type.",
|
||||||
|
"examples": [{"name": "bgp_route", "display_name": "BGP Route", "enable": True}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
name: StrictStr
|
name: StrictStr
|
||||||
display_name: StrictStr
|
display_name: StrictStr
|
||||||
enable: StrictBool
|
enable: StrictBool
|
||||||
|
|
||||||
class Config:
|
|
||||||
"""Pydantic model configuration."""
|
|
||||||
|
|
||||||
title = "Query Type"
|
|
||||||
description = (
|
|
||||||
"If enabled is `true`, the `name` field may be used to specify the query type."
|
|
||||||
)
|
|
||||||
schema_extra = {
|
|
||||||
"examples": [{"name": "bgp_route", "display_name": "BGP Route", "enable": True}]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class InfoResponse(BaseModel):
|
class InfoResponse(BaseModel):
|
||||||
"""Response model for /api/info endpoint."""
|
"""Response model for /api/info endpoint."""
|
||||||
|
|
||||||
name: StrictStr
|
model_config = ConfigDict(
|
||||||
organization: StrictStr
|
json_schema_extra={
|
||||||
primary_asn: StrictInt
|
"title": "System Information",
|
||||||
version: StrictStr
|
"description": "General information about this looking glass.",
|
||||||
|
|
||||||
class Config:
|
|
||||||
"""Pydantic model configuration."""
|
|
||||||
|
|
||||||
title = "System Information"
|
|
||||||
description = "General information about this looking glass."
|
|
||||||
schema_extra = {
|
|
||||||
"examples": [
|
"examples": [
|
||||||
{
|
{
|
||||||
"name": "hyperglass",
|
"name": "hyperglass",
|
||||||
|
|
@ -211,5 +215,11 @@ class InfoResponse(BaseModel):
|
||||||
"primary_asn": 65000,
|
"primary_asn": 65000,
|
||||||
"version": "hyperglass 1.0.0-beta.52",
|
"version": "hyperglass 1.0.0-beta.52",
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
name: StrictStr
|
||||||
|
organization: StrictStr
|
||||||
|
primary_asn: StrictInt
|
||||||
|
version: StrictStr
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@
|
||||||
# flake8: noqa
|
# flake8: noqa
|
||||||
import math
|
import math
|
||||||
import secrets
|
import secrets
|
||||||
from typing import List, Union, Optional
|
import typing as t
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import BaseModel, StrictInt, StrictStr, StrictFloat, constr, validator
|
from pydantic import BaseModel, field_validator, ConfigDict, Field
|
||||||
|
|
||||||
"""Patterns:
|
"""Patterns:
|
||||||
GET /.well-known/looking-glass/v1/ping/2001:DB8::35?protocol=2,1
|
GET /.well-known/looking-glass/v1/ping/2001:DB8::35?protocol=2,1
|
||||||
|
|
@ -22,55 +22,49 @@ GET /.well-known/looking-glass/v1/routers/1
|
||||||
GET /.well-known/looking-glass/v1/cmd
|
GET /.well-known/looking-glass/v1/cmd
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
QueryFormat = t.Literal[r"text/plain", r"application/json"]
|
||||||
|
|
||||||
|
|
||||||
class _HyperglassQuery(BaseModel):
|
class _HyperglassQuery(BaseModel):
|
||||||
class Config:
|
model_config = ConfigDict(validate_assignment=True, validate_default=True)
|
||||||
validate_all = True
|
|
||||||
validate_assignment = True
|
|
||||||
|
|
||||||
|
|
||||||
class BaseQuery(_HyperglassQuery):
|
class BaseQuery(_HyperglassQuery):
|
||||||
protocol: StrictStr = "1,1"
|
protocol: str = "1,1"
|
||||||
router: StrictStr
|
router: str
|
||||||
routerindex: StrictInt
|
routerindex: int
|
||||||
random: StrictStr = secrets.token_urlsafe(16)
|
random: str = secrets.token_urlsafe(16)
|
||||||
vrf: Optional[StrictStr]
|
runtime: int = 30
|
||||||
runtime: StrictInt = 30
|
query_format: QueryFormat = Field("text/plain", alias="format")
|
||||||
query_format: constr(regex=r"(text\/plain|application\/json)") = "text/plain"
|
|
||||||
|
|
||||||
@validator("runtime")
|
@field_validator("runtime")
|
||||||
def validate_runtime(cls, value):
|
def validate_runtime(cls, value):
|
||||||
if isinstance(value, float) and math.modf(value)[0] == 0:
|
if isinstance(value, float) and math.modf(value)[0] == 0:
|
||||||
value = math.ceil(value)
|
value = math.ceil(value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
class Config:
|
|
||||||
fields = {"query_format": "format"}
|
|
||||||
|
|
||||||
|
|
||||||
class BaseData(_HyperglassQuery):
|
class BaseData(_HyperglassQuery):
|
||||||
router: StrictStr
|
model_config = ConfigDict(extra="allow")
|
||||||
performed_at: datetime
|
|
||||||
runtime: Union[StrictFloat, StrictInt]
|
|
||||||
output: List[StrictStr]
|
|
||||||
data_format: StrictStr
|
|
||||||
|
|
||||||
@validator("runtime")
|
router: str
|
||||||
|
performed_at: datetime
|
||||||
|
runtime: t.Union[float, int]
|
||||||
|
output: t.List[str]
|
||||||
|
data_format: str = Field(alias="format")
|
||||||
|
|
||||||
|
@field_validator("runtime")
|
||||||
def validate_runtime(cls, value):
|
def validate_runtime(cls, value):
|
||||||
if isinstance(value, float) and math.modf(value)[0] == 0:
|
if isinstance(value, float) and math.modf(value)[0] == 0:
|
||||||
value = math.ceil(value)
|
value = math.ceil(value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
class Config:
|
|
||||||
fields = {"data_format": "format"}
|
|
||||||
extra = "allow"
|
|
||||||
|
|
||||||
|
|
||||||
class QueryError(_HyperglassQuery):
|
class QueryError(_HyperglassQuery):
|
||||||
status: constr(regex=r"error")
|
status: t.Literal["error"]
|
||||||
message: StrictStr
|
message: str
|
||||||
|
|
||||||
|
|
||||||
class QueryResponse(_HyperglassQuery):
|
class QueryResponse(_HyperglassQuery):
|
||||||
status: constr(regex=r"success|fail")
|
status: t.Literal["success", "fail"]
|
||||||
data: BaseData
|
data: BaseData
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,18 @@
|
||||||
"""Custom validation types."""
|
"""Custom validation types."""
|
||||||
|
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from pydantic import AfterValidator
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.constants import SUPPORTED_QUERY_TYPES
|
from hyperglass.constants import SUPPORTED_QUERY_TYPES
|
||||||
|
|
||||||
|
|
||||||
class SupportedQuery(str):
|
def validate_query_type(value: str) -> str:
|
||||||
"""Query Type Validation Model."""
|
"""Ensure query type is supported by hyperglass."""
|
||||||
|
if value not in SUPPORTED_QUERY_TYPES:
|
||||||
|
raise ValueError("'{}' is not a supported query type".format(value))
|
||||||
|
return value
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def __get_validators__(cls):
|
|
||||||
"""Pydantic custom type method."""
|
|
||||||
yield cls.validate
|
|
||||||
|
|
||||||
@classmethod
|
SupportedQuery = t.Annotated[str, AfterValidator(validate_query_type)]
|
||||||
def validate(cls, value):
|
|
||||||
"""Ensure query type is supported by hyperglass."""
|
|
||||||
if not isinstance(value, str):
|
|
||||||
raise TypeError("query_type must be a string")
|
|
||||||
if value not in SUPPORTED_QUERY_TYPES:
|
|
||||||
raise ValueError(f"'{value}' is not a supported query type")
|
|
||||||
return value
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
"""Stringify custom field representation."""
|
|
||||||
return f"SupportedQuery({super().__repr__()})"
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
"""Validation model for Redis cache config."""
|
"""Validation model for Redis cache config."""
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
from typing import Union, Optional
|
import typing as t
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import SecretStr, StrictInt, StrictStr, StrictBool, IPvAnyAddress
|
from pydantic import SecretStr, IPvAnyAddress
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from ..main import HyperglassModel
|
from ..main import HyperglassModel
|
||||||
|
|
@ -13,30 +13,14 @@ from ..main import HyperglassModel
|
||||||
class CachePublic(HyperglassModel):
|
class CachePublic(HyperglassModel):
|
||||||
"""Public cache parameters."""
|
"""Public cache parameters."""
|
||||||
|
|
||||||
timeout: StrictInt = 120
|
timeout: int = 120
|
||||||
show_text: StrictBool = True
|
show_text: bool = True
|
||||||
|
|
||||||
|
|
||||||
class Cache(CachePublic):
|
class Cache(CachePublic):
|
||||||
"""Validation model for params.cache."""
|
"""Validation model for params.cache."""
|
||||||
|
|
||||||
host: Union[IPvAnyAddress, StrictStr] = "localhost"
|
host: t.Union[IPvAnyAddress, str] = "localhost"
|
||||||
port: StrictInt = 6379
|
port: int = 6379
|
||||||
database: StrictInt = 1
|
database: int = 1
|
||||||
password: Optional[SecretStr]
|
password: t.Optional[SecretStr] = None
|
||||||
|
|
||||||
class Config:
|
|
||||||
"""Pydantic model configuration."""
|
|
||||||
|
|
||||||
title = "Cache"
|
|
||||||
description = "Redis server & cache timeout configuration."
|
|
||||||
fields = {
|
|
||||||
"host": {"description": "Redis server IP address or hostname."},
|
|
||||||
"port": {"description": "Redis server TCP port."},
|
|
||||||
"database": {"description": "Redis server database ID."},
|
|
||||||
"password": {"description": "Redis authentication password."},
|
|
||||||
"timeout": {
|
|
||||||
"description": "Time in seconds query output will be kept in the Redis cache."
|
|
||||||
},
|
|
||||||
"show_test": {description: "Show the cache text in the hyperglass UI."},
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,35 @@
|
||||||
"""Validate credential configuration variables."""
|
"""Validate credential configuration variables."""
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
from typing import Optional
|
import typing as t
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import FilePath, SecretStr, StrictStr, constr, root_validator
|
from pydantic import FilePath, SecretStr, model_validator
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from ..main import HyperglassModel
|
from ..main import HyperglassModel
|
||||||
|
|
||||||
Methods = constr(regex=r"(password|unencrypted_key|encrypted_key)")
|
AuthMethod = t.Literal["password", "unencrypted_key", "encrypted_key"]
|
||||||
|
|
||||||
|
|
||||||
class Credential(HyperglassModel, extra="allow"):
|
class Credential(HyperglassModel, extra="allow"):
|
||||||
"""Model for per-credential config in devices.yaml."""
|
"""Model for per-credential config in devices.yaml."""
|
||||||
|
|
||||||
username: StrictStr
|
username: str
|
||||||
password: Optional[SecretStr]
|
password: t.Optional[SecretStr] = None
|
||||||
key: Optional[FilePath]
|
key: t.Optional[FilePath] = None
|
||||||
|
_method: t.Optional[AuthMethod] = None
|
||||||
|
|
||||||
@root_validator
|
@model_validator(mode="after")
|
||||||
def validate_credential(cls, values):
|
def validate_credential(cls, data: "Credential"):
|
||||||
"""Ensure either a password or an SSH key is set."""
|
"""Ensure either a password or an SSH key is set."""
|
||||||
if values.get("key") is None and values.get("password") is None:
|
if data.key is None and data.password is None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Either a password or an SSH key must be specified for user '{}'".format(
|
"Either a password or an SSH key must be specified for user '{}'".format(
|
||||||
values["username"]
|
data.username
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return values
|
return data
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""Set private attribute _method based on validated model."""
|
"""Set private attribute _method based on validated model."""
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from pathlib import Path
|
||||||
from ipaddress import IPv4Address, IPv6Address
|
from ipaddress import IPv4Address, IPv6Address
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import FilePath, StrictInt, StrictStr, StrictBool, validator
|
from pydantic import FilePath, field_validator, ValidationInfo
|
||||||
from netmiko.ssh_dispatcher import CLASS_MAPPER # type: ignore
|
from netmiko.ssh_dispatcher import CLASS_MAPPER # type: ignore
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
|
|
@ -38,27 +38,27 @@ ALL_DEVICE_TYPES = {*DRIVER_MAP.keys(), *CLASS_MAPPER.keys()}
|
||||||
class DirectiveOptions(HyperglassModel, extra="ignore"):
|
class DirectiveOptions(HyperglassModel, extra="ignore"):
|
||||||
"""Per-device directive options."""
|
"""Per-device directive options."""
|
||||||
|
|
||||||
builtins: t.Union[StrictBool, t.List[StrictStr]] = True
|
builtins: t.Union[bool, t.List[str]] = True
|
||||||
|
|
||||||
|
|
||||||
class Device(HyperglassModelWithId, extra="allow"):
|
class Device(HyperglassModelWithId, extra="allow"):
|
||||||
"""Validation model for per-router config in devices.yaml."""
|
"""Validation model for per-router config in devices.yaml."""
|
||||||
|
|
||||||
id: StrictStr
|
id: str
|
||||||
name: StrictStr
|
name: str
|
||||||
description: t.Optional[StrictStr]
|
description: t.Optional[str] = None
|
||||||
avatar: t.Optional[FilePath]
|
avatar: t.Optional[FilePath] = None
|
||||||
address: t.Union[IPv4Address, IPv6Address, StrictStr]
|
address: t.Union[IPv4Address, IPv6Address, str]
|
||||||
group: t.Optional[StrictStr]
|
group: t.Optional[str] = None
|
||||||
credential: Credential
|
credential: Credential
|
||||||
proxy: t.Optional[Proxy]
|
proxy: t.Optional[Proxy] = None
|
||||||
display_name: t.Optional[StrictStr]
|
display_name: t.Optional[str] = None
|
||||||
port: StrictInt = 22
|
port: int = 22
|
||||||
http: HttpConfiguration = HttpConfiguration()
|
http: HttpConfiguration = HttpConfiguration()
|
||||||
platform: StrictStr
|
platform: str
|
||||||
structured_output: t.Optional[StrictBool]
|
structured_output: t.Optional[bool] = None
|
||||||
directives: Directives = Directives()
|
directives: Directives = Directives()
|
||||||
driver: t.Optional[SupportedDriver]
|
driver: t.Optional[SupportedDriver] = None
|
||||||
driver_config: t.Dict[str, t.Any] = {}
|
driver_config: t.Dict[str, t.Any] = {}
|
||||||
attrs: t.Dict[str, str] = {}
|
attrs: t.Dict[str, str] = {}
|
||||||
|
|
||||||
|
|
@ -162,7 +162,7 @@ class Device(HyperglassModelWithId, extra="allow"):
|
||||||
a=key,
|
a=key,
|
||||||
)
|
)
|
||||||
|
|
||||||
@validator("address")
|
@field_validator("address")
|
||||||
def validate_address(
|
def validate_address(
|
||||||
cls, value: t.Union[IPv4Address, IPv6Address, str], values: t.Dict[str, t.Any]
|
cls, value: t.Union[IPv4Address, IPv6Address, str], values: t.Dict[str, t.Any]
|
||||||
) -> t.Union[IPv4Address, IPv6Address, str]:
|
) -> t.Union[IPv4Address, IPv6Address, str]:
|
||||||
|
|
@ -177,7 +177,7 @@ class Device(HyperglassModelWithId, extra="allow"):
|
||||||
)
|
)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@validator("avatar")
|
@field_validator("avatar")
|
||||||
def validate_avatar(
|
def validate_avatar(
|
||||||
cls, value: t.Union[FilePath, None], values: t.Dict[str, t.Any]
|
cls, value: t.Union[FilePath, None], values: t.Dict[str, t.Any]
|
||||||
) -> t.Union[FilePath, None]:
|
) -> t.Union[FilePath, None]:
|
||||||
|
|
@ -199,7 +199,7 @@ class Device(HyperglassModelWithId, extra="allow"):
|
||||||
src.save(target)
|
src.save(target)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@validator("platform", pre=True, always=True)
|
@field_validator("platform", mode="before")
|
||||||
def validate_platform(cls: "Device", value: t.Any, values: t.Dict[str, t.Any]) -> str:
|
def validate_platform(cls: "Device", value: t.Any, values: t.Dict[str, t.Any]) -> str:
|
||||||
"""Validate & rewrite device platform, set default `directives`."""
|
"""Validate & rewrite device platform, set default `directives`."""
|
||||||
|
|
||||||
|
|
@ -220,35 +220,35 @@ class Device(HyperglassModelWithId, extra="allow"):
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@validator("structured_output", pre=True, always=True)
|
@field_validator("structured_output", mode="before")
|
||||||
def validate_structured_output(cls, value: bool, values: t.Dict[str, t.Any]) -> bool:
|
def validate_structured_output(cls, value: bool, info: ValidationInfo) -> bool:
|
||||||
"""Validate structured output is supported on the device & set a default."""
|
"""Validate structured output is supported on the device & set a default."""
|
||||||
|
|
||||||
if value is True:
|
if value is True:
|
||||||
if values["platform"] not in SUPPORTED_STRUCTURED_OUTPUT:
|
if info.data.get("platform") not in SUPPORTED_STRUCTURED_OUTPUT:
|
||||||
raise ConfigError(
|
raise ConfigError(
|
||||||
"The 'structured_output' field is set to 'true' on device '{d}' with "
|
"The 'structured_output' field is set to 'true' on device '{}' with "
|
||||||
+ "platform '{p}', which does not support structured output",
|
+ "platform '{}', which does not support structured output",
|
||||||
d=values["name"],
|
info.data.get("name"),
|
||||||
p=values["platform"],
|
info.data.get("platform"),
|
||||||
)
|
)
|
||||||
return value
|
return value
|
||||||
if value is None and values["platform"] in SUPPORTED_STRUCTURED_OUTPUT:
|
if value is None and info.data.get("platform") in SUPPORTED_STRUCTURED_OUTPUT:
|
||||||
value = True
|
value = True
|
||||||
else:
|
else:
|
||||||
value = False
|
value = False
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@validator("directives", pre=True, always=True)
|
@field_validator("directives", mode="before")
|
||||||
def validate_directives(
|
def validate_directives(
|
||||||
cls: "Device", value: t.Optional[t.List[str]], values: t.Dict[str, t.Any]
|
cls: "Device", value: t.Optional[t.List[str]], info: ValidationInfo
|
||||||
) -> "Directives":
|
) -> "Directives":
|
||||||
"""Associate directive IDs to loaded directive objects."""
|
"""Associate directive IDs to loaded directive objects."""
|
||||||
directives = use_state("directives")
|
directives = use_state("directives")
|
||||||
|
|
||||||
directive_ids = value or []
|
directive_ids = value or []
|
||||||
structured_output = values.get("structured_output", False)
|
structured_output = info.data.get("structured_output", False)
|
||||||
platform = values.get("platform")
|
platform = info.data.get("platform")
|
||||||
|
|
||||||
# Directive options
|
# Directive options
|
||||||
directive_options = DirectiveOptions(
|
directive_options = DirectiveOptions(
|
||||||
|
|
@ -280,10 +280,10 @@ class Device(HyperglassModelWithId, extra="allow"):
|
||||||
|
|
||||||
return device_directives
|
return device_directives
|
||||||
|
|
||||||
@validator("driver")
|
@field_validator("driver")
|
||||||
def validate_driver(cls: "Device", value: t.Optional[str], values: t.Dict[str, t.Any]) -> str:
|
def validate_driver(cls: "Device", value: t.Optional[str], info: ValidationInfo) -> str:
|
||||||
"""Set the correct driver and override if supported."""
|
"""Set the correct driver and override if supported."""
|
||||||
return get_driver(values["platform"], value)
|
return get_driver(info.data.get("platform"), value)
|
||||||
|
|
||||||
|
|
||||||
class Devices(MultiModel, model=Device, unique_by="id"):
|
class Devices(MultiModel, model=Device, unique_by="id"):
|
||||||
|
|
@ -305,9 +305,9 @@ class Devices(MultiModel, model=Device, unique_by="id"):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def directive_plugins(self: "Devices") -> t.Dict[Path, t.Tuple[StrictStr]]:
|
def directive_plugins(self: "Devices") -> t.Dict[Path, t.Tuple[str]]:
|
||||||
"""Get a mapping of plugin paths to associated directive IDs."""
|
"""Get a mapping of plugin paths to associated directive IDs."""
|
||||||
result: t.Dict[Path, t.Set[StrictStr]] = {}
|
result: t.Dict[Path, t.Set[str]] = {}
|
||||||
# Unique set of all directives.
|
# Unique set of all directives.
|
||||||
directives = {directive for device in self for directive in device.directives}
|
directives = {directive for device in self for directive in device.directives}
|
||||||
# Unique set of all plugin file names.
|
# Unique set of all plugin file names.
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,31 @@
|
||||||
"""Configuration for API docs feature."""
|
"""Configuration for API docs feature."""
|
||||||
|
|
||||||
|
import typing as t
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import Field, HttpUrl, StrictStr, StrictBool, constr
|
from pydantic import Field, HttpUrl
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from ..main import HyperglassModel
|
from ..main import HyperglassModel
|
||||||
from ..fields import AnyUri
|
from ..fields import AnyUri
|
||||||
|
|
||||||
DocsMode = constr(regex=r"(swagger|redoc)")
|
DocsMode = t.Literal["swagger", "redoc"]
|
||||||
|
|
||||||
|
|
||||||
class EndpointConfig(HyperglassModel):
|
class EndpointConfig(HyperglassModel):
|
||||||
"""Validation model for per API endpoint documentation."""
|
"""Validation model for per API endpoint documentation."""
|
||||||
|
|
||||||
title: StrictStr = Field(
|
title: str = Field(
|
||||||
...,
|
...,
|
||||||
title="Endpoint Title",
|
title="Endpoint Title",
|
||||||
description="Displayed as the header text above the API endpoint section.",
|
description="Displayed as the header text above the API endpoint section.",
|
||||||
)
|
)
|
||||||
description: StrictStr = Field(
|
description: str = Field(
|
||||||
...,
|
...,
|
||||||
title="Endpoint Description",
|
title="Endpoint Description",
|
||||||
description="Displayed inside each API endpoint section.",
|
description="Displayed inside each API endpoint section.",
|
||||||
)
|
)
|
||||||
summary: StrictStr = Field(
|
summary: str = Field(
|
||||||
...,
|
...,
|
||||||
title="Endpoint Summary",
|
title="Endpoint Summary",
|
||||||
description="Displayed beside the API endpoint URI.",
|
description="Displayed beside the API endpoint URI.",
|
||||||
|
|
@ -32,9 +35,7 @@ class EndpointConfig(HyperglassModel):
|
||||||
class Docs(HyperglassModel):
|
class Docs(HyperglassModel):
|
||||||
"""Validation model for params.docs."""
|
"""Validation model for params.docs."""
|
||||||
|
|
||||||
enable: StrictBool = Field(
|
enable: bool = Field(True, title="Enable", description="Enable or disable API documentation.")
|
||||||
True, title="Enable", description="Enable or disable API documentation."
|
|
||||||
)
|
|
||||||
mode: DocsMode = Field(
|
mode: DocsMode = Field(
|
||||||
"redoc",
|
"redoc",
|
||||||
title="Docs Mode",
|
title="Docs Mode",
|
||||||
|
|
@ -50,12 +51,12 @@ class Docs(HyperglassModel):
|
||||||
title="URI",
|
title="URI",
|
||||||
description="HTTP URI/path where API documentation can be accessed.",
|
description="HTTP URI/path where API documentation can be accessed.",
|
||||||
)
|
)
|
||||||
title: StrictStr = Field(
|
title: str = Field(
|
||||||
"{site_title} API Documentation",
|
"{site_title} API Documentation",
|
||||||
title="Title",
|
title="Title",
|
||||||
description="API documentation title. `{site_title}` may be used to display the `site_title` parameter.",
|
description="API documentation title. `{site_title}` may be used to display the `site_title` parameter.",
|
||||||
)
|
)
|
||||||
description: StrictStr = Field(
|
description: str = Field(
|
||||||
"",
|
"",
|
||||||
title="Description",
|
title="Description",
|
||||||
description="API documentation description appearing below the title.",
|
description="API documentation description appearing below the title.",
|
||||||
|
|
@ -80,27 +81,3 @@ class Docs(HyperglassModel):
|
||||||
description="General information about this looking glass.",
|
description="General information about this looking glass.",
|
||||||
summary="System Information",
|
summary="System Information",
|
||||||
)
|
)
|
||||||
|
|
||||||
class Config:
|
|
||||||
"""Pydantic model configuration."""
|
|
||||||
|
|
||||||
title = "API Docs"
|
|
||||||
description = "API documentation configuration parameters"
|
|
||||||
fields = {
|
|
||||||
"query": {
|
|
||||||
"title": "Query API Endpoint",
|
|
||||||
"description": "`/api/query/` API documentation options.",
|
|
||||||
},
|
|
||||||
"devices": {
|
|
||||||
"title": "Devices API Endpoint",
|
|
||||||
"description": "`/api/devices` API documentation options.",
|
|
||||||
},
|
|
||||||
"queries": {
|
|
||||||
"title": "Queries API Endpoint",
|
|
||||||
"description": "`/api/devices` API documentation options.",
|
|
||||||
},
|
|
||||||
"communities": {
|
|
||||||
"title": "BGP Communities API Endpoint",
|
|
||||||
"description": "`/api/communities` API documentation options.",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,6 @@ import httpx
|
||||||
from pydantic import (
|
from pydantic import (
|
||||||
FilePath,
|
FilePath,
|
||||||
SecretStr,
|
SecretStr,
|
||||||
StrictInt,
|
|
||||||
StrictStr,
|
|
||||||
StrictBool,
|
|
||||||
PrivateAttr,
|
PrivateAttr,
|
||||||
IPvAnyAddress,
|
IPvAnyAddress,
|
||||||
)
|
)
|
||||||
|
|
@ -39,23 +36,23 @@ Scheme = t.Literal["http", "https"]
|
||||||
class AttributeMapConfig(HyperglassModel):
|
class AttributeMapConfig(HyperglassModel):
|
||||||
"""Allow the user to 'rewrite' hyperglass field names to their own values."""
|
"""Allow the user to 'rewrite' hyperglass field names to their own values."""
|
||||||
|
|
||||||
query_target: t.Optional[StrictStr]
|
query_target: t.Optional[str] = None
|
||||||
query_type: t.Optional[StrictStr]
|
query_type: t.Optional[str] = None
|
||||||
query_location: t.Optional[StrictStr]
|
query_location: t.Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class AttributeMap(HyperglassModel):
|
class AttributeMap(HyperglassModel):
|
||||||
"""Merged implementation of attribute map configuration."""
|
"""Merged implementation of attribute map configuration."""
|
||||||
|
|
||||||
query_target: StrictStr
|
query_target: str
|
||||||
query_type: StrictStr
|
query_type: str
|
||||||
query_location: StrictStr
|
query_location: str
|
||||||
|
|
||||||
|
|
||||||
class HttpBasicAuth(HyperglassModel):
|
class HttpBasicAuth(HyperglassModel):
|
||||||
"""Configuration model for HTTP basic authentication."""
|
"""Configuration model for HTTP basic authentication."""
|
||||||
|
|
||||||
username: StrictStr
|
username: str
|
||||||
password: SecretStr
|
password: SecretStr
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -63,21 +60,21 @@ class HttpConfiguration(HyperglassModel):
|
||||||
"""HTTP client configuration."""
|
"""HTTP client configuration."""
|
||||||
|
|
||||||
_attribute_map: AttributeMap = PrivateAttr()
|
_attribute_map: AttributeMap = PrivateAttr()
|
||||||
path: StrictStr = "/"
|
path: str = "/"
|
||||||
method: HttpMethod = "GET"
|
method: HttpMethod = "GET"
|
||||||
scheme: Scheme = "https"
|
scheme: Scheme = "https"
|
||||||
query: t.Optional[t.Union[t.Literal[False], t.Dict[str, Primitives]]]
|
query: t.Optional[t.Union[t.Literal[False], t.Dict[str, Primitives]]] = None
|
||||||
verify_ssl: StrictBool = True
|
verify_ssl: bool = True
|
||||||
ssl_ca: t.Optional[FilePath]
|
ssl_ca: t.Optional[FilePath] = None
|
||||||
ssl_client: t.Optional[FilePath]
|
ssl_client: t.Optional[FilePath] = None
|
||||||
source: t.Optional[IPvAnyAddress]
|
source: t.Optional[IPvAnyAddress] = None
|
||||||
timeout: IntFloat = 5
|
timeout: IntFloat = 5
|
||||||
headers: t.Dict[str, str] = {}
|
headers: t.Dict[str, str] = {}
|
||||||
follow_redirects: StrictBool = False
|
follow_redirects: bool = False
|
||||||
basic_auth: t.Optional[HttpBasicAuth]
|
basic_auth: t.Optional[HttpBasicAuth] = None
|
||||||
attribute_map: AttributeMapConfig = AttributeMapConfig()
|
attribute_map: AttributeMapConfig = AttributeMapConfig()
|
||||||
body_format: BodyFormat = "json"
|
body_format: BodyFormat = "json"
|
||||||
retries: StrictInt = 0
|
retries: int = 0
|
||||||
|
|
||||||
def __init__(self, **data: t.Any) -> None:
|
def __init__(self, **data: t.Any) -> None:
|
||||||
"""Create HTTP Client Configuration Definition."""
|
"""Create HTTP Client Configuration Definition."""
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,16 @@
|
||||||
"""Validate logging configuration."""
|
"""Validate logging configuration."""
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
from typing import Dict, Union, Optional
|
import typing as t
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import (
|
from pydantic import (
|
||||||
ByteSize,
|
ByteSize,
|
||||||
SecretStr,
|
SecretStr,
|
||||||
StrictInt,
|
|
||||||
StrictStr,
|
|
||||||
AnyHttpUrl,
|
AnyHttpUrl,
|
||||||
StrictBool,
|
|
||||||
StrictFloat,
|
|
||||||
DirectoryPath,
|
DirectoryPath,
|
||||||
validator,
|
field_validator,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
|
|
@ -28,18 +24,18 @@ from ..fields import LogFormat, HttpAuthMode, HttpProvider
|
||||||
class Syslog(HyperglassModel):
|
class Syslog(HyperglassModel):
|
||||||
"""Validation model for syslog configuration."""
|
"""Validation model for syslog configuration."""
|
||||||
|
|
||||||
enable: StrictBool = True
|
enable: bool = True
|
||||||
host: StrictStr
|
host: str
|
||||||
port: StrictInt = 514
|
port: int = 514
|
||||||
|
|
||||||
|
|
||||||
class HttpAuth(HyperglassModel):
|
class HttpAuth(HyperglassModel):
|
||||||
"""HTTP hook authentication parameters."""
|
"""HTTP hook authentication parameters."""
|
||||||
|
|
||||||
mode: HttpAuthMode = "basic"
|
mode: HttpAuthMode = "basic"
|
||||||
username: Optional[StrictStr]
|
username: t.Optional[str] = None
|
||||||
password: SecretStr
|
password: SecretStr
|
||||||
header: StrictStr = "x-api-key"
|
header: str = "x-api-key"
|
||||||
|
|
||||||
def api_key(self):
|
def api_key(self):
|
||||||
"""Represent authentication as an API key header."""
|
"""Represent authentication as an API key header."""
|
||||||
|
|
@ -53,16 +49,16 @@ class HttpAuth(HyperglassModel):
|
||||||
class Http(HyperglassModel, extra="allow"):
|
class Http(HyperglassModel, extra="allow"):
|
||||||
"""HTTP logging parameters."""
|
"""HTTP logging parameters."""
|
||||||
|
|
||||||
enable: StrictBool = True
|
enable: bool = True
|
||||||
provider: HttpProvider = "generic"
|
provider: HttpProvider = "generic"
|
||||||
host: AnyHttpUrl
|
host: AnyHttpUrl
|
||||||
authentication: Optional[HttpAuth]
|
authentication: t.Optional[HttpAuth] = None
|
||||||
headers: Dict[StrictStr, Union[StrictStr, StrictInt, StrictBool, None]] = {}
|
headers: t.Dict[str, t.Union[str, int, bool, None]] = {}
|
||||||
params: Dict[StrictStr, Union[StrictStr, StrictInt, StrictBool, None]] = {}
|
params: t.Dict[str, t.Union[str, int, bool, None]] = {}
|
||||||
verify_ssl: StrictBool = True
|
verify_ssl: bool = True
|
||||||
timeout: Union[StrictFloat, StrictInt] = 5.0
|
timeout: t.Union[float, int] = 5.0
|
||||||
|
|
||||||
@validator("headers", "params")
|
@field_validator("headers", "params")
|
||||||
def stringify_headers_params(cls, value):
|
def stringify_headers_params(cls, value):
|
||||||
"""Ensure headers and URL parameters are strings."""
|
"""Ensure headers and URL parameters are strings."""
|
||||||
for k, v in value.items():
|
for k, v in value.items():
|
||||||
|
|
@ -94,5 +90,5 @@ class Logging(HyperglassModel):
|
||||||
directory: DirectoryPath = Path("/tmp") # noqa: S108
|
directory: DirectoryPath = Path("/tmp") # noqa: S108
|
||||||
format: LogFormat = "text"
|
format: LogFormat = "text"
|
||||||
max_size: ByteSize = "50MB"
|
max_size: ByteSize = "50MB"
|
||||||
syslog: Optional[Syslog]
|
syslog: t.Optional[Syslog] = None
|
||||||
http: Optional[Http]
|
http: t.Optional[Http] = None
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""Validate error message configuration variables."""
|
"""Validate error message configuration variables."""
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import Field, StrictStr
|
from pydantic import Field, ConfigDict
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from ..main import HyperglassModel
|
from ..main import HyperglassModel
|
||||||
|
|
@ -10,66 +10,66 @@ from ..main import HyperglassModel
|
||||||
class Messages(HyperglassModel):
|
class Messages(HyperglassModel):
|
||||||
"""Validation model for params.messages."""
|
"""Validation model for params.messages."""
|
||||||
|
|
||||||
no_input: StrictStr = Field(
|
no_input: str = Field(
|
||||||
"{field} must be specified.",
|
"{field} must be specified.",
|
||||||
title="No Input",
|
title="No Input",
|
||||||
description="Displayed when no a required field is not specified. `{field}` may be used to display the `display_name` of the field that was omitted.",
|
description="Displayed when no a required field is not specified. `{field}` may be used to display the `display_name` of the field that was omitted.",
|
||||||
)
|
)
|
||||||
target_not_allowed: StrictStr = Field(
|
target_not_allowed: str = Field(
|
||||||
"{target} is not allowed.",
|
"{target} is not allowed.",
|
||||||
title="Target Not Allowed",
|
title="Target Not Allowed",
|
||||||
description="Displayed when a query target is implicitly denied by a configured rule. `{target}` will be used to display the denied query target.",
|
description="Displayed when a query target is implicitly denied by a configured rule. `{target}` will be used to display the denied query target.",
|
||||||
)
|
)
|
||||||
feature_not_enabled: StrictStr = Field(
|
feature_not_enabled: str = Field(
|
||||||
"{feature} is not enabled.",
|
"{feature} is not enabled.",
|
||||||
title="Feature Not Enabled",
|
title="Feature Not Enabled",
|
||||||
description="Displayed when a query type is submitted that is not supported or disabled. The hyperglass UI performs validation of supported query types prior to submitting any requests, so this is primarily relevant to the hyperglass API. `{feature}` may be used to display the disabled feature.",
|
description="Displayed when a query type is submitted that is not supported or disabled. The hyperglass UI performs validation of supported query types prior to submitting any requests, so this is primarily relevant to the hyperglass API. `{feature}` may be used to display the disabled feature.",
|
||||||
)
|
)
|
||||||
invalid_input: StrictStr = Field(
|
invalid_input: str = Field(
|
||||||
"{target} is not valid.",
|
"{target} is not valid.",
|
||||||
title="Invalid Input",
|
title="Invalid Input",
|
||||||
description="Displayed when a query target's value is invalid in relation to the corresponding query type. `{target}` may be used to display the invalid target.",
|
description="Displayed when a query target's value is invalid in relation to the corresponding query type. `{target}` may be used to display the invalid target.",
|
||||||
)
|
)
|
||||||
invalid_query: StrictStr = Field(
|
invalid_query: str = Field(
|
||||||
"{target} is not a valid {query_type} target.",
|
"{target} is not a valid {query_type} target.",
|
||||||
title="Invalid Query",
|
title="Invalid Query",
|
||||||
description="Displayed when a query target's value is invalid in relation to the corresponding query type. `{target}` and `{query_type}` may be used to display the invalid target and corresponding query type.",
|
description="Displayed when a query target's value is invalid in relation to the corresponding query type. `{target}` and `{query_type}` may be used to display the invalid target and corresponding query type.",
|
||||||
)
|
)
|
||||||
invalid_field: StrictStr = Field(
|
invalid_field: str = Field(
|
||||||
"{input} is an invalid {field}.",
|
"{input} is an invalid {field}.",
|
||||||
title="Invalid Field",
|
title="Invalid Field",
|
||||||
description="Displayed when a query field contains an invalid or unsupported value. `{input}` and `{field}` may be used to display the invalid input value and corresponding field name.",
|
description="Displayed when a query field contains an invalid or unsupported value. `{input}` and `{field}` may be used to display the invalid input value and corresponding field name.",
|
||||||
)
|
)
|
||||||
general: StrictStr = Field(
|
general: str = Field(
|
||||||
"Something went wrong.",
|
"Something went wrong.",
|
||||||
title="General Error",
|
title="General Error",
|
||||||
description="Displayed when generalized errors occur. Seeing this error message may indicate a bug in hyperglass, as most other errors produced are highly contextual. If you see this in the wild, try enabling [debug mode](/fixme) and review the logs to pinpoint the source of the error.",
|
description="Displayed when generalized errors occur. Seeing this error message may indicate a bug in hyperglass, as most other errors produced are highly contextual. If you see this in the wild, try enabling [debug mode](/fixme) and review the logs to pinpoint the source of the error.",
|
||||||
)
|
)
|
||||||
not_found: StrictStr = Field(
|
not_found: str = Field(
|
||||||
"{type} '{name}' not found.",
|
"{type} '{name}' not found.",
|
||||||
title="Not Found",
|
title="Not Found",
|
||||||
description="Displayed when an object property does not exist in the configuration. `{type}` corresponds to a user-friendly name of the object type (for example, 'Device'), `{name}` corresponds to the object name that was not found.",
|
description="Displayed when an object property does not exist in the configuration. `{type}` corresponds to a user-friendly name of the object type (for example, 'Device'), `{name}` corresponds to the object name that was not found.",
|
||||||
)
|
)
|
||||||
request_timeout: StrictStr = Field(
|
request_timeout: str = Field(
|
||||||
"Request timed out.",
|
"Request timed out.",
|
||||||
title="Request Timeout",
|
title="Request Timeout",
|
||||||
description="Displayed when the [request_timeout](/fixme) time expires.",
|
description="Displayed when the [request_timeout](/fixme) time expires.",
|
||||||
)
|
)
|
||||||
connection_error: StrictStr = Field(
|
connection_error: str = Field(
|
||||||
"Error connecting to {device_name}: {error}",
|
"Error connecting to {device_name}: {error}",
|
||||||
title="Displayed when hyperglass is unable to connect to a configured device. Usually, this indicates a configuration error. `{device_name}` and `{error}` may be used to display the device in question and the specific connection error.",
|
title="Displayed when hyperglass is unable to connect to a configured device. Usually, this indicates a configuration error. `{device_name}` and `{error}` may be used to display the device in question and the specific connection error.",
|
||||||
)
|
)
|
||||||
authentication_error: StrictStr = Field(
|
authentication_error: str = Field(
|
||||||
"Authentication error occurred.",
|
"Authentication error occurred.",
|
||||||
title="Authentication Error",
|
title="Authentication Error",
|
||||||
description="Displayed when hyperglass is unable to authenticate to a configured device. Usually, this indicates a configuration error.",
|
description="Displayed when hyperglass is unable to authenticate to a configured device. Usually, this indicates a configuration error.",
|
||||||
)
|
)
|
||||||
no_response: StrictStr = Field(
|
no_response: str = Field(
|
||||||
"No response.",
|
"No response.",
|
||||||
title="No Response",
|
title="No Response",
|
||||||
description="Displayed when hyperglass can connect to a device, but no output able to be read. Seeing this error may indicate a bug in hyperglas or one of its dependencies. If you see this in the wild, try enabling [debug mode](/fixme) and review the logs to pinpoint the source of the error.",
|
description="Displayed when hyperglass can connect to a device, but no output able to be read. Seeing this error may indicate a bug in hyperglas or one of its dependencies. If you see this in the wild, try enabling [debug mode](/fixme) and review the logs to pinpoint the source of the error.",
|
||||||
)
|
)
|
||||||
no_output: StrictStr = Field(
|
no_output: str = Field(
|
||||||
"The query completed, but no matching results were found.",
|
"The query completed, but no matching results were found.",
|
||||||
title="No Output",
|
title="No Output",
|
||||||
description="Displayed when hyperglass can connect to a device and execute a query, but the response is empty.",
|
description="Displayed when hyperglass can connect to a device and execute a query, but the response is empty.",
|
||||||
|
|
@ -77,18 +77,17 @@ class Messages(HyperglassModel):
|
||||||
|
|
||||||
def has(self, attr: str) -> bool:
|
def has(self, attr: str) -> bool:
|
||||||
"""Determine if message type exists in Messages model."""
|
"""Determine if message type exists in Messages model."""
|
||||||
return attr in self.dict().keys()
|
return attr in self.model_dump().keys()
|
||||||
|
|
||||||
def __getitem__(self, attr: str) -> StrictStr:
|
def __getitem__(self, attr: str) -> str:
|
||||||
"""Make messages subscriptable."""
|
"""Make messages subscriptable."""
|
||||||
|
|
||||||
if not self.has(attr):
|
if not self.has(attr):
|
||||||
raise KeyError(f"'{attr}' does not exist on Messages model")
|
raise KeyError(f"'{attr}' does not exist on Messages model")
|
||||||
return getattr(self, attr)
|
return getattr(self, attr)
|
||||||
|
|
||||||
class Config:
|
model_config = ConfigDict(
|
||||||
"""Pydantic model configuration."""
|
title="Messages",
|
||||||
|
description="Customize almost all user-facing UI & API messages.",
|
||||||
title = "Messages"
|
json_schema_extra={"level": 2},
|
||||||
description = "Customize almost all user-facing UI & API messages."
|
)
|
||||||
schema_extra = {"level": 2}
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import FilePath, validator
|
from pydantic import FilePath, field_validator
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from ..main import HyperglassModel
|
from ..main import HyperglassModel
|
||||||
|
|
@ -17,7 +17,7 @@ class OpenGraph(HyperglassModel):
|
||||||
|
|
||||||
image: FilePath = DEFAULT_IMAGES / "hyperglass-opengraph.jpg"
|
image: FilePath = DEFAULT_IMAGES / "hyperglass-opengraph.jpg"
|
||||||
|
|
||||||
@validator("image")
|
@field_validator("image")
|
||||||
def validate_opengraph(cls, value):
|
def validate_opengraph(cls, value):
|
||||||
"""Ensure the opengraph image is a supported format."""
|
"""Ensure the opengraph image is a supported format."""
|
||||||
supported_extensions = (".jpg", ".jpeg", ".png")
|
supported_extensions = (".jpg", ".jpeg", ".png")
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
"""Configuration validation entry point."""
|
"""Configuration validation entry point."""
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
from typing import Any, Dict, List, Tuple, Union, Literal
|
import typing as t
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import Field, StrictInt, StrictStr, StrictBool, validator
|
from pydantic import Field, field_validator, ValidationInfo, ConfigDict
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.settings import Settings
|
from hyperglass.settings import Settings
|
||||||
|
|
@ -19,33 +19,33 @@ from .logging import Logging
|
||||||
from .messages import Messages
|
from .messages import Messages
|
||||||
from .structured import Structured
|
from .structured import Structured
|
||||||
|
|
||||||
Localhost = Literal["localhost"]
|
Localhost = t.Literal["localhost"]
|
||||||
|
|
||||||
|
|
||||||
class ParamsPublic(HyperglassModel):
|
class ParamsPublic(HyperglassModel):
|
||||||
"""Public configuration parameters."""
|
"""Public configuration parameters."""
|
||||||
|
|
||||||
request_timeout: StrictInt = Field(
|
request_timeout: int = Field(
|
||||||
90,
|
90,
|
||||||
title="Request Timeout",
|
title="Request Timeout",
|
||||||
description="Global timeout in seconds for all requests. The frontend application (UI) uses this field's exact value when submitting queries. The backend application uses this field's value, minus one second, for its own timeout handling. This is to ensure a contextual timeout error is presented to the end user in the event of a backend application timeout.",
|
description="Global timeout in seconds for all requests. The frontend application (UI) uses this field's exact value when submitting queries. The backend application uses this field's value, minus one second, for its own timeout handling. This is to ensure a contextual timeout error is presented to the end user in the event of a backend application timeout.",
|
||||||
)
|
)
|
||||||
primary_asn: Union[StrictInt, StrictStr] = Field(
|
primary_asn: t.Union[int, str] = Field(
|
||||||
"65001",
|
"65001",
|
||||||
title="Primary ASN",
|
title="Primary ASN",
|
||||||
description="Your network's primary ASN. This field is used to set some useful defaults such as the subtitle and PeeringDB URL.",
|
description="Your network's primary ASN. This field is used to set some useful defaults such as the subtitle and PeeringDB URL.",
|
||||||
)
|
)
|
||||||
org_name: StrictStr = Field(
|
org_name: str = Field(
|
||||||
"Beloved Hyperglass User",
|
"Beloved Hyperglass User",
|
||||||
title="Organization Name",
|
title="Organization Name",
|
||||||
description="Your organization's name. This field is used in the UI & API documentation to set fields such as `<meta/>` HTML tags for SEO and the terms & conditions footer component.",
|
description="Your organization's name. This field is used in the UI & API documentation to set fields such as `<meta/>` HTML tags for SEO and the terms & conditions footer component.",
|
||||||
)
|
)
|
||||||
site_title: StrictStr = Field(
|
site_title: str = Field(
|
||||||
"hyperglass",
|
"hyperglass",
|
||||||
title="Site Title",
|
title="Site Title",
|
||||||
description="The name of your hyperglass site. This field is used in the UI & API documentation to set fields such as the `<title/>` HTML tag, and the terms & conditions footer component.",
|
description="The name of your hyperglass site. This field is used in the UI & API documentation to set fields such as the `<title/>` HTML tag, and the terms & conditions footer component.",
|
||||||
)
|
)
|
||||||
site_description: StrictStr = Field(
|
site_description: str = Field(
|
||||||
"{org_name} Network Looking Glass",
|
"{org_name} Network Looking Glass",
|
||||||
title="Site Description",
|
title="Site Description",
|
||||||
description='A short description of your hyperglass site. This field is used in th UI & API documentation to set the `<meta name="description"/>` tag. `{org_name}` may be used to insert the value of the `org_name` field.',
|
description='A short description of your hyperglass site. This field is used in th UI & API documentation to set the `<meta name="description"/>` tag. `{org_name}` may be used to insert the value of the `org_name` field.',
|
||||||
|
|
@ -55,19 +55,21 @@ class ParamsPublic(HyperglassModel):
|
||||||
class Params(ParamsPublic, HyperglassModel):
|
class Params(ParamsPublic, HyperglassModel):
|
||||||
"""Validation model for all configuration variables."""
|
"""Validation model for all configuration variables."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(json_schema_extra={"level": 1})
|
||||||
|
|
||||||
# Top Level Params
|
# Top Level Params
|
||||||
|
|
||||||
fake_output: StrictBool = Field(
|
fake_output: bool = Field(
|
||||||
False,
|
False,
|
||||||
title="Fake Output",
|
title="Fake Output",
|
||||||
description="If enabled, the hyperglass backend will return static fake output for development/testing purposes.",
|
description="If enabled, the hyperglass backend will return static fake output for development/testing purposes.",
|
||||||
)
|
)
|
||||||
cors_origins: List[StrictStr] = Field(
|
cors_origins: t.List[str] = Field(
|
||||||
[],
|
[],
|
||||||
title="Cross-Origin Resource Sharing",
|
title="Cross-Origin Resource Sharing",
|
||||||
description="Allowed CORS hosts. By default, no CORS hosts are allowed.",
|
description="Allowed CORS hosts. By default, no CORS hosts are allowed.",
|
||||||
)
|
)
|
||||||
plugins: List[StrictStr] = []
|
plugins: t.List[str] = []
|
||||||
|
|
||||||
# Sub Level Params
|
# Sub Level Params
|
||||||
cache: Cache = Cache()
|
cache: Cache = Cache()
|
||||||
|
|
@ -77,26 +79,21 @@ class Params(ParamsPublic, HyperglassModel):
|
||||||
structured: Structured = Structured()
|
structured: Structured = Structured()
|
||||||
web: Web = Web()
|
web: Web = Web()
|
||||||
|
|
||||||
class Config:
|
def __init__(self, **kw: t.Any) -> None:
|
||||||
"""Pydantic model configuration."""
|
|
||||||
|
|
||||||
schema_extra = {"level": 1}
|
|
||||||
|
|
||||||
def __init__(self, **kw: Any) -> None:
|
|
||||||
return super().__init__(**self.convert_paths(kw))
|
return super().__init__(**self.convert_paths(kw))
|
||||||
|
|
||||||
@validator("site_description")
|
@field_validator("site_description")
|
||||||
def validate_site_description(cls: "Params", value: str, values: Dict[str, Any]) -> str:
|
def validate_site_description(cls: "Params", value: str, info: ValidationInfo) -> str:
|
||||||
"""Format the site descripion with the org_name field."""
|
"""Format the site description with the org_name field."""
|
||||||
return value.format(org_name=values["org_name"])
|
return value.format(org_name=info.data.get("org_name"))
|
||||||
|
|
||||||
@validator("primary_asn")
|
@field_validator("primary_asn")
|
||||||
def validate_primary_asn(cls: "Params", value: Union[int, str]) -> str:
|
def validate_primary_asn(cls: "Params", value: t.Union[int, str]) -> str:
|
||||||
"""Stringify primary_asn if passed as an integer."""
|
"""Stringify primary_asn if passed as an integer."""
|
||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
@validator("plugins")
|
@field_validator("plugins")
|
||||||
def validate_plugins(cls: "Params", value: List[str]) -> List[str]:
|
def validate_plugins(cls: "Params", value: t.List[str]) -> t.List[str]:
|
||||||
"""Validate and register configured plugins."""
|
"""Validate and register configured plugins."""
|
||||||
plugin_dir = Settings.app_path / "plugins"
|
plugin_dir = Settings.app_path / "plugins"
|
||||||
|
|
||||||
|
|
@ -111,11 +108,11 @@ class Params(ParamsPublic, HyperglassModel):
|
||||||
return [str(f) for f in matching_plugins]
|
return [str(f) for f in matching_plugins]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def common_plugins(self) -> Tuple[Path, ...]:
|
def common_plugins(self) -> t.Tuple[Path, ...]:
|
||||||
"""Get all validated external common plugins as Path objects."""
|
"""Get all validated external common plugins as Path objects."""
|
||||||
return tuple(Path(p) for p in self.plugins)
|
return tuple(Path(p) for p in self.plugins)
|
||||||
|
|
||||||
def frontend(self) -> Dict[str, Any]:
|
def frontend(self) -> t.Dict[str, t.Any]:
|
||||||
"""Export UI-specific parameters."""
|
"""Export UI-specific parameters."""
|
||||||
|
|
||||||
return self.export_dict(
|
return self.export_dict(
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
"""Validate SSH proxy configuration variables."""
|
"""Validate SSH proxy configuration variables."""
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
from typing import Any, Dict, Union
|
import typing as t
|
||||||
from ipaddress import IPv4Address, IPv6Address
|
from ipaddress import IPv4Address, IPv6Address
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import StrictInt, StrictStr, validator
|
from pydantic import field_validator, ValidationInfo
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.util import resolve_hostname
|
from hyperglass.util import resolve_hostname
|
||||||
|
|
@ -20,12 +20,12 @@ from .credential import Credential
|
||||||
class Proxy(HyperglassModel):
|
class Proxy(HyperglassModel):
|
||||||
"""Validation model for per-proxy config in devices.yaml."""
|
"""Validation model for per-proxy config in devices.yaml."""
|
||||||
|
|
||||||
address: Union[IPv4Address, IPv6Address, StrictStr]
|
address: t.Union[IPv4Address, IPv6Address, str]
|
||||||
port: StrictInt = 22
|
port: int = 22
|
||||||
credential: Credential
|
credential: Credential
|
||||||
platform: StrictStr = "linux_ssh"
|
platform: str = "linux_ssh"
|
||||||
|
|
||||||
def __init__(self: "Proxy", **kwargs: Any) -> None:
|
def __init__(self: "Proxy", **kwargs: t.Any) -> None:
|
||||||
"""Check for legacy fields."""
|
"""Check for legacy fields."""
|
||||||
kwargs = check_legacy_fields(model="Proxy", data=kwargs)
|
kwargs = check_legacy_fields(model="Proxy", data=kwargs)
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
@ -34,7 +34,7 @@ class Proxy(HyperglassModel):
|
||||||
def _target(self):
|
def _target(self):
|
||||||
return str(self.address)
|
return str(self.address)
|
||||||
|
|
||||||
@validator("address")
|
@field_validator("address")
|
||||||
def validate_address(cls, value):
|
def validate_address(cls, value):
|
||||||
"""Ensure a hostname is resolvable."""
|
"""Ensure a hostname is resolvable."""
|
||||||
|
|
||||||
|
|
@ -46,14 +46,14 @@ class Proxy(HyperglassModel):
|
||||||
)
|
)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@validator("platform", pre=True, always=True)
|
@field_validator("platform", mode="before")
|
||||||
def validate_type(cls: "Proxy", value: Any, values: Dict[str, Any]) -> str:
|
def validate_type(cls: "Proxy", value: t.Any, info: ValidationInfo) -> str:
|
||||||
"""Validate device type."""
|
"""Validate device type."""
|
||||||
|
|
||||||
if value != "linux_ssh":
|
if value != "linux_ssh":
|
||||||
raise UnsupportedDevice(
|
raise UnsupportedDevice(
|
||||||
"Proxy '{p}' uses platform '{t}', which is currently unsupported.",
|
"Proxy '{}' uses platform '{}', which is currently unsupported.",
|
||||||
p=values["address"],
|
info.data.get("address"),
|
||||||
t=value,
|
value,
|
||||||
)
|
)
|
||||||
return value
|
return value
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,21 @@
|
||||||
"""Structured data configuration variables."""
|
"""Structured data configuration variables."""
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
from typing import List
|
import typing as t
|
||||||
|
|
||||||
# Third Party
|
|
||||||
from pydantic import StrictStr, constr
|
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from ..main import HyperglassModel
|
from ..main import HyperglassModel
|
||||||
|
|
||||||
StructuredCommunityMode = constr(regex=r"(permit|deny)")
|
StructuredCommunityMode = t.Literal["permit", "deny"]
|
||||||
StructuredRPKIMode = constr(regex=r"(router|external)")
|
StructuredRPKIMode = t.Literal["router", "external"]
|
||||||
|
|
||||||
|
|
||||||
class StructuredCommunities(HyperglassModel):
|
class StructuredCommunities(HyperglassModel):
|
||||||
"""Control structured data response for BGP communities."""
|
"""Control structured data response for BGP communities."""
|
||||||
|
|
||||||
mode: StructuredCommunityMode = "deny"
|
mode: StructuredCommunityMode = "deny"
|
||||||
items: List[StrictStr] = []
|
items: t.List[str] = []
|
||||||
|
|
||||||
|
|
||||||
class StructuredRpki(HyperglassModel):
|
class StructuredRpki(HyperglassModel):
|
||||||
|
|
|
||||||
|
|
@ -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
|
from pathlib import Path
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import (
|
from pydantic import HttpUrl, FilePath, constr, field_validator, model_validator, ValidationInfo
|
||||||
HttpUrl,
|
from pydantic_extra_types.color import Color
|
||||||
FilePath,
|
|
||||||
StrictInt,
|
|
||||||
StrictStr,
|
|
||||||
StrictBool,
|
|
||||||
constr,
|
|
||||||
validator,
|
|
||||||
root_validator,
|
|
||||||
)
|
|
||||||
from pydantic.color import Color
|
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.defaults import DEFAULT_HELP, DEFAULT_TERMS
|
from hyperglass.defaults import DEFAULT_HELP, DEFAULT_TERMS
|
||||||
|
|
@ -27,40 +18,40 @@ from .opengraph import OpenGraph
|
||||||
|
|
||||||
DEFAULT_IMAGES = Path(__file__).parent.parent.parent / "images"
|
DEFAULT_IMAGES = Path(__file__).parent.parent.parent / "images"
|
||||||
|
|
||||||
Percentage = constr(regex=r"^([1-9][0-9]?|100)\%$")
|
Percentage = constr(pattern=r"^([1-9][0-9]?|100)\%$")
|
||||||
TitleMode = constr(regex=("logo_only|text_only|logo_subtitle|all"))
|
TitleMode = t.Literal["logo_only", "text_only", "logo_subtitle", "all"]
|
||||||
ColorMode = constr(regex=r"light|dark")
|
ColorMode = t.Literal["light", "dark"]
|
||||||
DOHProvider = constr(regex="|".join(DNS_OVER_HTTPS.keys()))
|
DOHProvider = constr(pattern="|".join(DNS_OVER_HTTPS.keys()))
|
||||||
Title = constr(max_length=32)
|
Title = constr(max_length=32)
|
||||||
Side = constr(regex=r"left|right")
|
Side = t.Literal["left", "right"]
|
||||||
LocationDisplayMode = t.Literal["auto", "dropdown", "gallery"]
|
LocationDisplayMode = t.Literal["auto", "dropdown", "gallery"]
|
||||||
|
|
||||||
|
|
||||||
class Credit(HyperglassModel):
|
class Credit(HyperglassModel):
|
||||||
"""Validation model for developer credit."""
|
"""Validation model for developer credit."""
|
||||||
|
|
||||||
enable: StrictBool = True
|
enable: bool = True
|
||||||
|
|
||||||
|
|
||||||
class Link(HyperglassModel):
|
class Link(HyperglassModel):
|
||||||
"""Validation model for generic link."""
|
"""Validation model for generic link."""
|
||||||
|
|
||||||
title: StrictStr
|
title: str
|
||||||
url: HttpUrl
|
url: HttpUrl
|
||||||
show_icon: StrictBool = True
|
show_icon: bool = True
|
||||||
side: Side = "left"
|
side: Side = "left"
|
||||||
order: StrictInt = 0
|
order: int = 0
|
||||||
|
|
||||||
|
|
||||||
class Menu(HyperglassModel):
|
class Menu(HyperglassModel):
|
||||||
"""Validation model for generic menu."""
|
"""Validation model for generic menu."""
|
||||||
|
|
||||||
title: StrictStr
|
title: str
|
||||||
content: StrictStr
|
content: str
|
||||||
side: Side = "left"
|
side: Side = "left"
|
||||||
order: StrictInt = 0
|
order: int = 0
|
||||||
|
|
||||||
@validator("content")
|
@field_validator("content")
|
||||||
def validate_content(cls: "Menu", value: str) -> str:
|
def validate_content(cls: "Menu", value: str) -> str:
|
||||||
"""Read content from file if a path is provided."""
|
"""Read content from file if a path is provided."""
|
||||||
|
|
||||||
|
|
@ -75,16 +66,16 @@ class Menu(HyperglassModel):
|
||||||
class Greeting(HyperglassModel):
|
class Greeting(HyperglassModel):
|
||||||
"""Validation model for greeting modal."""
|
"""Validation model for greeting modal."""
|
||||||
|
|
||||||
enable: StrictBool = False
|
enable: bool = False
|
||||||
file: t.Optional[FilePath]
|
file: t.Optional[FilePath] = None
|
||||||
title: StrictStr = "Welcome"
|
title: str = "Welcome"
|
||||||
button: StrictStr = "Continue"
|
button: str = "Continue"
|
||||||
required: StrictBool = False
|
required: bool = False
|
||||||
|
|
||||||
@validator("file")
|
@field_validator("file")
|
||||||
def validate_file(cls, value, values):
|
def validate_file(cls, value: str, info: ValidationInfo):
|
||||||
"""Ensure file is specified if greeting is enabled."""
|
"""Ensure file is specified if greeting is enabled."""
|
||||||
if values["enable"] and value is None:
|
if info.data.get("enable") and value is None:
|
||||||
raise ValueError("Greeting is enabled, but no file is specified.")
|
raise ValueError("Greeting is enabled, but no file is specified.")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
@ -95,15 +86,15 @@ class Logo(HyperglassModel):
|
||||||
light: FilePath = DEFAULT_IMAGES / "hyperglass-light.svg"
|
light: FilePath = DEFAULT_IMAGES / "hyperglass-light.svg"
|
||||||
dark: FilePath = DEFAULT_IMAGES / "hyperglass-dark.svg"
|
dark: FilePath = DEFAULT_IMAGES / "hyperglass-dark.svg"
|
||||||
favicon: FilePath = DEFAULT_IMAGES / "hyperglass-icon.svg"
|
favicon: FilePath = DEFAULT_IMAGES / "hyperglass-icon.svg"
|
||||||
width: t.Optional[t.Union[StrictInt, Percentage]] = "100%"
|
width: t.Optional[t.Union[int, Percentage]] = "100%"
|
||||||
height: t.Optional[t.Union[StrictInt, Percentage]]
|
height: t.Optional[t.Union[int, Percentage]] = None
|
||||||
|
|
||||||
|
|
||||||
class LogoPublic(Logo):
|
class LogoPublic(Logo):
|
||||||
"""Public logo configuration."""
|
"""Public logo configuration."""
|
||||||
|
|
||||||
light_format: StrictStr
|
light_format: str
|
||||||
dark_format: StrictStr
|
dark_format: str
|
||||||
|
|
||||||
|
|
||||||
class Text(HyperglassModel):
|
class Text(HyperglassModel):
|
||||||
|
|
@ -112,27 +103,27 @@ class Text(HyperglassModel):
|
||||||
title_mode: TitleMode = "logo_only"
|
title_mode: TitleMode = "logo_only"
|
||||||
title: Title = "hyperglass"
|
title: Title = "hyperglass"
|
||||||
subtitle: Title = "Network Looking Glass"
|
subtitle: Title = "Network Looking Glass"
|
||||||
query_location: StrictStr = "Location"
|
query_location: str = "Location"
|
||||||
query_type: StrictStr = "Query Type"
|
query_type: str = "Query Type"
|
||||||
query_target: StrictStr = "Target"
|
query_target: str = "Target"
|
||||||
fqdn_tooltip: StrictStr = "Use {protocol}" # Formatted by Javascript
|
fqdn_tooltip: str = "Use {protocol}" # Formatted by Javascript
|
||||||
fqdn_message: StrictStr = "Your browser has resolved {fqdn} to" # Formatted by Javascript
|
fqdn_message: str = "Your browser has resolved {fqdn} to" # Formatted by Javascript
|
||||||
fqdn_error: StrictStr = "Unable to resolve {fqdn}" # Formatted by Javascript
|
fqdn_error: str = "Unable to resolve {fqdn}" # Formatted by Javascript
|
||||||
fqdn_error_button: StrictStr = "Try Again"
|
fqdn_error_button: str = "Try Again"
|
||||||
cache_prefix: StrictStr = "Results cached for "
|
cache_prefix: str = "Results cached for "
|
||||||
cache_icon: StrictStr = "Cached from {time} UTC" # Formatted by Javascript
|
cache_icon: str = "Cached from {time} UTC" # Formatted by Javascript
|
||||||
complete_time: StrictStr = "Completed in {seconds}" # Formatted by Javascript
|
complete_time: str = "Completed in {seconds}" # Formatted by Javascript
|
||||||
rpki_invalid: StrictStr = "Invalid"
|
rpki_invalid: str = "Invalid"
|
||||||
rpki_valid: StrictStr = "Valid"
|
rpki_valid: str = "Valid"
|
||||||
rpki_unknown: StrictStr = "No ROAs Exist"
|
rpki_unknown: str = "No ROAs Exist"
|
||||||
rpki_unverified: StrictStr = "Not Verified"
|
rpki_unverified: str = "Not Verified"
|
||||||
no_communities: StrictStr = "No Communities"
|
no_communities: str = "No Communities"
|
||||||
ip_error: StrictStr = "Unable to determine IP Address"
|
ip_error: str = "Unable to determine IP Address"
|
||||||
no_ip: StrictStr = "No {protocol} Address"
|
no_ip: str = "No {protocol} Address"
|
||||||
ip_select: StrictStr = "Select an IP Address"
|
ip_select: str = "Select an IP Address"
|
||||||
ip_button: StrictStr = "My IP"
|
ip_button: str = "My IP"
|
||||||
|
|
||||||
@validator("cache_prefix")
|
@field_validator("cache_prefix")
|
||||||
def validate_cache_prefix(cls: "Text", value: str) -> str:
|
def validate_cache_prefix(cls: "Text", value: str) -> str:
|
||||||
"""Ensure trailing whitespace."""
|
"""Ensure trailing whitespace."""
|
||||||
return " ".join(value.split()) + " "
|
return " ".join(value.split()) + " "
|
||||||
|
|
@ -155,21 +146,19 @@ class ThemeColors(HyperglassModel):
|
||||||
cyan: Color = "#118ab2"
|
cyan: Color = "#118ab2"
|
||||||
pink: Color = "#f2607d"
|
pink: Color = "#f2607d"
|
||||||
purple: Color = "#8d30b5"
|
purple: Color = "#8d30b5"
|
||||||
primary: t.Optional[Color]
|
primary: t.Optional[Color] = None
|
||||||
secondary: t.Optional[Color]
|
secondary: t.Optional[Color] = None
|
||||||
success: t.Optional[Color]
|
success: t.Optional[Color] = None
|
||||||
warning: t.Optional[Color]
|
warning: t.Optional[Color] = None
|
||||||
error: t.Optional[Color]
|
error: t.Optional[Color] = None
|
||||||
danger: t.Optional[Color]
|
danger: t.Optional[Color] = None
|
||||||
|
|
||||||
@validator(*FUNC_COLOR_MAP.keys(), pre=True, always=True)
|
@field_validator(*FUNC_COLOR_MAP.keys(), mode="before")
|
||||||
def validate_colors(
|
def validate_colors(cls: "ThemeColors", value: str, info: ValidationInfo) -> str:
|
||||||
cls: "ThemeColors", value: str, values: t.Dict[str, t.Optional[str]], field
|
|
||||||
) -> str:
|
|
||||||
"""Set default functional color mapping."""
|
"""Set default functional color mapping."""
|
||||||
if value is None:
|
if value is None:
|
||||||
default_color = FUNC_COLOR_MAP[field.name]
|
default_color = FUNC_COLOR_MAP[info.field_name]
|
||||||
value = str(values[default_color])
|
value = str(info.data[default_color])
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def dict(self, *args: t.Any, **kwargs: t.Any) -> t.Dict[str, str]:
|
def dict(self, *args: t.Any, **kwargs: t.Any) -> t.Dict[str, str]:
|
||||||
|
|
@ -180,15 +169,15 @@ class ThemeColors(HyperglassModel):
|
||||||
class ThemeFonts(HyperglassModel):
|
class ThemeFonts(HyperglassModel):
|
||||||
"""Validation model for theme fonts."""
|
"""Validation model for theme fonts."""
|
||||||
|
|
||||||
body: StrictStr = "Nunito"
|
body: str = "Nunito"
|
||||||
mono: StrictStr = "Fira Code"
|
mono: str = "Fira Code"
|
||||||
|
|
||||||
|
|
||||||
class Theme(HyperglassModel):
|
class Theme(HyperglassModel):
|
||||||
"""Validation model for theme variables."""
|
"""Validation model for theme variables."""
|
||||||
|
|
||||||
colors: ThemeColors = ThemeColors()
|
colors: ThemeColors = ThemeColors()
|
||||||
default_color_mode: t.Optional[ColorMode]
|
default_color_mode: t.Optional[ColorMode] = None
|
||||||
fonts: ThemeFonts = ThemeFonts()
|
fonts: ThemeFonts = ThemeFonts()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -196,27 +185,30 @@ class DnsOverHttps(HyperglassModel):
|
||||||
"""Validation model for DNS over HTTPS resolution."""
|
"""Validation model for DNS over HTTPS resolution."""
|
||||||
|
|
||||||
name: DOHProvider = "cloudflare"
|
name: DOHProvider = "cloudflare"
|
||||||
url: StrictStr = ""
|
url: str = ""
|
||||||
|
|
||||||
@root_validator
|
@model_validator(mode="before")
|
||||||
def validate_dns(cls: "DnsOverHttps", values: t.Dict[str, str]) -> t.Dict[str, str]:
|
def validate_dns(cls, data: "DnsOverHttps") -> t.Dict[str, str]:
|
||||||
"""Assign url field to model based on selected provider."""
|
"""Assign url field to model based on selected provider."""
|
||||||
provider = values["name"]
|
name = data.get("name", "cloudflare")
|
||||||
values["url"] = DNS_OVER_HTTPS[provider]
|
url = DNS_OVER_HTTPS[name]
|
||||||
return values
|
return {
|
||||||
|
"name": name,
|
||||||
|
"url": url,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class HighlightPattern(HyperglassModel):
|
class HighlightPattern(HyperglassModel):
|
||||||
"""Validation model for highlight pattern configuration."""
|
"""Validation model for highlight pattern configuration."""
|
||||||
|
|
||||||
pattern: StrictStr
|
pattern: str
|
||||||
label: t.Optional[StrictStr] = None
|
label: t.Optional[str] = None
|
||||||
color: StrictStr = "primary"
|
color: str = "primary"
|
||||||
|
|
||||||
@validator("color")
|
@field_validator("color")
|
||||||
def validate_color(cls: "HighlightPattern", value: str) -> str:
|
def validate_color(cls: "HighlightPattern", value: str) -> str:
|
||||||
"""Ensure highlight color is a valid theme color."""
|
"""Ensure highlight color is a valid theme color."""
|
||||||
colors = list(ThemeColors.__fields__.keys())
|
colors = list(ThemeColors.model_fields.keys())
|
||||||
color_list = "\n - ".join(("", *colors))
|
color_list = "\n - ".join(("", *colors))
|
||||||
if value not in colors:
|
if value not in colors:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
|
@ -243,8 +235,8 @@ class Web(HyperglassModel):
|
||||||
text: Text = Text()
|
text: Text = Text()
|
||||||
theme: Theme = Theme()
|
theme: Theme = Theme()
|
||||||
location_display_mode: LocationDisplayMode = "auto"
|
location_display_mode: LocationDisplayMode = "auto"
|
||||||
custom_javascript: t.Optional[FilePath]
|
custom_javascript: t.Optional[FilePath] = None
|
||||||
custom_html: t.Optional[FilePath]
|
custom_html: t.Optional[FilePath] = None
|
||||||
highlight: t.List[HighlightPattern] = []
|
highlight: t.List[HighlightPattern] = []
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
import re
|
import re
|
||||||
from typing import List, Literal
|
import typing as t
|
||||||
from ipaddress import ip_network
|
from ipaddress import ip_network
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import StrictInt, StrictStr, StrictBool, validator
|
from pydantic import field_validator
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.state import use_state
|
from hyperglass.state import use_state
|
||||||
|
|
@ -15,27 +15,27 @@ from hyperglass.external.rpki import rpki_state
|
||||||
# Local
|
# Local
|
||||||
from ..main import HyperglassModel
|
from ..main import HyperglassModel
|
||||||
|
|
||||||
WinningWeight = Literal["low", "high"]
|
WinningWeight = t.Literal["low", "high"]
|
||||||
|
|
||||||
|
|
||||||
class BGPRoute(HyperglassModel):
|
class BGPRoute(HyperglassModel):
|
||||||
"""Post-parsed BGP route."""
|
"""Post-parsed BGP route."""
|
||||||
|
|
||||||
prefix: StrictStr
|
prefix: str
|
||||||
active: StrictBool
|
active: bool
|
||||||
age: StrictInt
|
age: int
|
||||||
weight: StrictInt
|
weight: int
|
||||||
med: StrictInt
|
med: int
|
||||||
local_preference: StrictInt
|
local_preference: int
|
||||||
as_path: List[StrictInt]
|
as_path: t.List[int]
|
||||||
communities: List[StrictStr]
|
communities: t.List[str]
|
||||||
next_hop: StrictStr
|
next_hop: str
|
||||||
source_as: StrictInt
|
source_as: int
|
||||||
source_rid: StrictStr
|
source_rid: str
|
||||||
peer_rid: StrictStr
|
peer_rid: str
|
||||||
rpki_state: StrictInt
|
rpki_state: int
|
||||||
|
|
||||||
@validator("communities")
|
@field_validator("communities")
|
||||||
def validate_communities(cls, value):
|
def validate_communities(cls, value):
|
||||||
"""Filter returned communities against configured policy.
|
"""Filter returned communities against configured policy.
|
||||||
|
|
||||||
|
|
@ -69,7 +69,7 @@ class BGPRoute(HyperglassModel):
|
||||||
|
|
||||||
return [c for c in value if func(c)]
|
return [c for c in value if func(c)]
|
||||||
|
|
||||||
@validator("rpki_state")
|
@field_validator("rpki_state")
|
||||||
def validate_rpki_state(cls, value, values):
|
def validate_rpki_state(cls, value, values):
|
||||||
"""If external RPKI validation is enabled, get validation state."""
|
"""If external RPKI validation is enabled, get validation state."""
|
||||||
|
|
||||||
|
|
@ -106,9 +106,9 @@ class BGPRoute(HyperglassModel):
|
||||||
class BGPRouteTable(HyperglassModel):
|
class BGPRouteTable(HyperglassModel):
|
||||||
"""Post-parsed BGP route table."""
|
"""Post-parsed BGP route table."""
|
||||||
|
|
||||||
vrf: StrictStr
|
vrf: str
|
||||||
count: StrictInt = 0
|
count: int = 0
|
||||||
routes: List[BGPRoute]
|
routes: t.List[BGPRoute]
|
||||||
winning_weight: WinningWeight
|
winning_weight: WinningWeight
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,15 @@ import typing as t
|
||||||
from ipaddress import IPv4Network, IPv6Network, ip_network
|
from ipaddress import IPv4Network, IPv6Network, ip_network
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import Field, FilePath, StrictStr, StrictBool, PrivateAttr, conint, validator
|
from pydantic import (
|
||||||
|
Discriminator,
|
||||||
|
field_validator,
|
||||||
|
Field,
|
||||||
|
FilePath,
|
||||||
|
IPvAnyNetwork,
|
||||||
|
PrivateAttr,
|
||||||
|
Tag,
|
||||||
|
)
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
|
|
@ -18,24 +26,20 @@ from hyperglass.exceptions.private import InputValidationError
|
||||||
from .main import MultiModel, HyperglassModel, HyperglassUniqueModel
|
from .main import MultiModel, HyperglassModel, HyperglassUniqueModel
|
||||||
from .fields import Action
|
from .fields import Action
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
|
||||||
# Project
|
|
||||||
from hyperglass.models.api.query import QueryTarget
|
|
||||||
|
|
||||||
IPv4PrefixLength = conint(ge=0, le=32)
|
StringOrArray = t.Union[str, t.List[str]]
|
||||||
IPv6PrefixLength = conint(ge=0, le=128)
|
Condition = t.Union[IPvAnyNetwork, str]
|
||||||
IPNetwork = t.Union[IPv4Network, IPv6Network]
|
|
||||||
StringOrArray = t.Union[StrictStr, t.List[StrictStr]]
|
|
||||||
Condition = t.Union[IPv4Network, IPv6Network, StrictStr]
|
|
||||||
RuleValidation = t.Union[t.Literal["ipv4", "ipv6", "pattern"], None]
|
RuleValidation = t.Union[t.Literal["ipv4", "ipv6", "pattern"], None]
|
||||||
PassedValidation = t.Union[bool, None]
|
PassedValidation = t.Union[bool, None]
|
||||||
|
IPFamily = t.Literal["ipv4", "ipv6"]
|
||||||
|
RuleTypeAttr = t.Literal["ipv4", "ipv6", "pattern", "none"]
|
||||||
|
|
||||||
|
|
||||||
class Input(HyperglassModel):
|
class Input(HyperglassModel):
|
||||||
"""Base input field."""
|
"""Base input field."""
|
||||||
|
|
||||||
_type: PrivateAttr
|
_type: PrivateAttr
|
||||||
description: StrictStr
|
description: str
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_select(self) -> bool:
|
def is_select(self) -> bool:
|
||||||
|
|
@ -52,15 +56,15 @@ class Text(Input):
|
||||||
"""Text/input field model."""
|
"""Text/input field model."""
|
||||||
|
|
||||||
_type: PrivateAttr = PrivateAttr("text")
|
_type: PrivateAttr = PrivateAttr("text")
|
||||||
validation: t.Optional[StrictStr]
|
validation: t.Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class Option(HyperglassModel):
|
class Option(HyperglassModel):
|
||||||
"""Select option model."""
|
"""Select option model."""
|
||||||
|
|
||||||
name: t.Optional[StrictStr]
|
name: t.Optional[str] = None
|
||||||
description: t.Optional[StrictStr]
|
description: t.Optional[str] = None
|
||||||
value: StrictStr
|
value: str
|
||||||
|
|
||||||
|
|
||||||
class Select(Input):
|
class Select(Input):
|
||||||
|
|
@ -70,16 +74,16 @@ class Select(Input):
|
||||||
options: t.List[Option]
|
options: t.List[Option]
|
||||||
|
|
||||||
|
|
||||||
class Rule(HyperglassModel, allow_population_by_field_name=True):
|
class Rule(HyperglassModel):
|
||||||
"""Base rule."""
|
"""Base rule."""
|
||||||
|
|
||||||
_validation: RuleValidation = PrivateAttr()
|
_type: RuleTypeAttr = PrivateAttr(Field("none", discriminator="_type"))
|
||||||
_passed: PassedValidation = PrivateAttr(None)
|
_passed: PassedValidation = PrivateAttr(None)
|
||||||
condition: Condition
|
condition: Condition
|
||||||
action: Action = Action("permit")
|
action: Action = "permit"
|
||||||
commands: t.List[str] = Field([], alias="command")
|
commands: t.List[str] = Field([], alias="command")
|
||||||
|
|
||||||
@validator("commands", pre=True, allow_reuse=True)
|
@field_validator("commands", mode="before")
|
||||||
def validate_commands(cls, value: t.Union[str, t.List[str]]) -> t.List[str]:
|
def validate_commands(cls, value: t.Union[str, t.List[str]]) -> t.List[str]:
|
||||||
"""Ensure commands is a list."""
|
"""Ensure commands is a list."""
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
|
|
@ -89,22 +93,28 @@ class Rule(HyperglassModel, allow_population_by_field_name=True):
|
||||||
def validate_target(self, target: str, *, multiple: bool) -> bool:
|
def validate_target(self, target: str, *, multiple: bool) -> bool:
|
||||||
"""Validate a query target (Placeholder signature)."""
|
"""Validate a query target (Placeholder signature)."""
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
f"{self._validation} rule does not implement a 'validate_target()' method"
|
f"{self._type} rule does not implement a 'validate_target()' method"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class RuleWithIP(Rule):
|
class RuleWithIP(Rule):
|
||||||
"""Base IP-based rule."""
|
"""Base IP-based rule."""
|
||||||
|
|
||||||
_family: PrivateAttr
|
condition: IPvAnyNetwork
|
||||||
condition: IPNetwork
|
allow_reserved: bool = False
|
||||||
allow_reserved: StrictBool = False
|
allow_unspecified: bool = False
|
||||||
allow_unspecified: StrictBool = False
|
allow_loopback: bool = False
|
||||||
allow_loopback: StrictBool = False
|
|
||||||
ge: int
|
ge: int
|
||||||
le: int
|
le: int
|
||||||
|
|
||||||
def membership(self, target: IPNetwork, network: IPNetwork) -> bool:
|
def __init__(self, **kw) -> None:
|
||||||
|
super().__init__(**kw)
|
||||||
|
if self.condition.network_address.version == 4:
|
||||||
|
self._type = "ipv4"
|
||||||
|
else:
|
||||||
|
self._type = "ipv6"
|
||||||
|
|
||||||
|
def membership(self, target: IPvAnyNetwork, network: IPvAnyNetwork) -> bool:
|
||||||
"""Check if IP address belongs to network."""
|
"""Check if IP address belongs to network."""
|
||||||
log.debug("Checking membership of {} for {}", str(target), str(network))
|
log.debug("Checking membership of {} for {}", str(target), str(network))
|
||||||
if (
|
if (
|
||||||
|
|
@ -115,7 +125,7 @@ class RuleWithIP(Rule):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def in_range(self, target: IPNetwork) -> bool:
|
def in_range(self, target: IPvAnyNetwork) -> bool:
|
||||||
"""Verify if target prefix length is within ge/le threshold."""
|
"""Verify if target prefix length is within ge/le threshold."""
|
||||||
if target.prefixlen <= self.le and target.prefixlen >= self.ge:
|
if target.prefixlen <= self.le and target.prefixlen >= self.ge:
|
||||||
log.debug("{} is in range {}-{}", target, self.ge, self.le)
|
log.debug("{} is in range {}-{}", target, self.ge, self.le)
|
||||||
|
|
@ -123,7 +133,7 @@ class RuleWithIP(Rule):
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def validate_target(self, target: "QueryTarget", *, multiple: bool) -> bool:
|
def validate_target(self, target: str, *, multiple: bool) -> bool:
|
||||||
"""Validate an IP address target against this rule's conditions."""
|
"""Validate an IP address target against this rule's conditions."""
|
||||||
|
|
||||||
if isinstance(target, t.List):
|
if isinstance(target, t.List):
|
||||||
|
|
@ -169,30 +179,28 @@ class RuleWithIP(Rule):
|
||||||
class RuleWithIPv4(RuleWithIP):
|
class RuleWithIPv4(RuleWithIP):
|
||||||
"""A rule by which to evaluate an IPv4 target."""
|
"""A rule by which to evaluate an IPv4 target."""
|
||||||
|
|
||||||
_family: PrivateAttr = PrivateAttr("ipv4")
|
_type: RuleTypeAttr = "ipv4"
|
||||||
_validation: RuleValidation = PrivateAttr("ipv4")
|
|
||||||
condition: IPv4Network
|
condition: IPv4Network
|
||||||
ge: IPv4PrefixLength = 0
|
ge: int = Field(0, ge=0, le=32)
|
||||||
le: IPv4PrefixLength = 32
|
le: int = Field(32, ge=0, le=32)
|
||||||
|
|
||||||
|
|
||||||
class RuleWithIPv6(RuleWithIP):
|
class RuleWithIPv6(RuleWithIP):
|
||||||
"""A rule by which to evaluate an IPv6 target."""
|
"""A rule by which to evaluate an IPv6 target."""
|
||||||
|
|
||||||
_family: PrivateAttr = PrivateAttr("ipv6")
|
_type: RuleTypeAttr = "ipv6"
|
||||||
_validation: RuleValidation = PrivateAttr("ipv6")
|
|
||||||
condition: IPv6Network
|
condition: IPv6Network
|
||||||
ge: IPv6PrefixLength = 0
|
ge: int = Field(0, ge=0, le=128)
|
||||||
le: IPv6PrefixLength = 128
|
le: int = Field(128, ge=0, le=128)
|
||||||
|
|
||||||
|
|
||||||
class RuleWithPattern(Rule):
|
class RuleWithPattern(Rule):
|
||||||
"""A rule validated by a regular expression pattern."""
|
"""A rule validated by a regular expression pattern."""
|
||||||
|
|
||||||
_validation: RuleValidation = PrivateAttr("pattern")
|
_type: RuleTypeAttr = "pattern"
|
||||||
condition: StrictStr
|
condition: str
|
||||||
|
|
||||||
def validate_target(self, target: "QueryTarget", *, multiple: bool) -> str: # noqa: C901
|
def validate_target(self, target: str, *, multiple: bool) -> str: # noqa: C901
|
||||||
"""Validate a string target against configured regex patterns."""
|
"""Validate a string target against configured regex patterns."""
|
||||||
|
|
||||||
def validate_single_value(value: str) -> t.Union[bool, BaseException]:
|
def validate_single_value(value: str) -> t.Union[bool, BaseException]:
|
||||||
|
|
@ -234,7 +242,7 @@ class RuleWithPattern(Rule):
|
||||||
class RuleWithoutValidation(Rule):
|
class RuleWithoutValidation(Rule):
|
||||||
"""A rule with no validation."""
|
"""A rule with no validation."""
|
||||||
|
|
||||||
_validation: RuleValidation = PrivateAttr(None)
|
_type: RuleTypeAttr = "none"
|
||||||
condition: None
|
condition: None
|
||||||
|
|
||||||
def validate_target(self, target: str, *, multiple: bool) -> t.Literal[True]:
|
def validate_target(self, target: str, *, multiple: bool) -> t.Literal[True]:
|
||||||
|
|
@ -243,24 +251,44 @@ class RuleWithoutValidation(Rule):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
RuleType = t.Union[RuleWithIPv4, RuleWithIPv6, RuleWithPattern, RuleWithoutValidation]
|
RuleWithIPv4Type = t.Annotated[RuleWithIPv4, Tag("ipv4")]
|
||||||
|
RuleWithIPv6Type = t.Annotated[RuleWithIPv6, Tag("ipv6")]
|
||||||
|
RuleWithPatternType = t.Annotated[RuleWithPattern, Tag("pattern")]
|
||||||
|
RuleWithoutValidationType = t.Annotated[RuleWithoutValidation, Tag("none")]
|
||||||
|
|
||||||
|
# RuleType = t.Union[RuleWithIPv4, RuleWithIPv6, RuleWithPattern, RuleWithoutValidation]
|
||||||
|
RuleType = t.Union[
|
||||||
|
RuleWithIPv4Type,
|
||||||
|
RuleWithIPv6Type,
|
||||||
|
RuleWithPatternType,
|
||||||
|
RuleWithoutValidationType,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def type_discriminator(value: t.Any) -> RuleTypeAttr:
|
||||||
|
"""Pydantic type discriminator."""
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return value.get("_type")
|
||||||
|
return getattr(value, "_type", None)
|
||||||
|
|
||||||
|
|
||||||
class Directive(HyperglassUniqueModel, unique_by=("id", "table_output")):
|
class Directive(HyperglassUniqueModel, unique_by=("id", "table_output")):
|
||||||
"""A directive contains commands that can be run on a device, as long as defined rules are met."""
|
"""A directive contains commands that can be run on a device, as long as defined rules are met."""
|
||||||
|
|
||||||
__hyperglass_builtin__: t.ClassVar[bool] = False
|
_hyperglass_builtin: bool = PrivateAttr(False)
|
||||||
|
|
||||||
id: StrictStr
|
id: str
|
||||||
name: StrictStr
|
name: str
|
||||||
rules: t.List[RuleType] = [RuleWithPattern(condition="*")]
|
rules: t.List[RuleType] = [
|
||||||
|
Field(RuleWithPattern(condition="*"), discriminator=Discriminator(type_discriminator))
|
||||||
|
]
|
||||||
field: t.Union[Text, Select]
|
field: t.Union[Text, Select]
|
||||||
info: t.Optional[FilePath]
|
info: t.Optional[FilePath] = None
|
||||||
plugins: t.List[StrictStr] = []
|
plugins: t.List[str] = []
|
||||||
table_output: t.Optional[StrictStr]
|
table_output: t.Optional[str] = None
|
||||||
groups: t.List[StrictStr] = []
|
groups: t.List[str] = []
|
||||||
multiple: StrictBool = False
|
multiple: bool = False
|
||||||
multiple_separator: StrictStr = " "
|
multiple_separator: str = " "
|
||||||
|
|
||||||
def validate_target(self, target: str) -> bool:
|
def validate_target(self, target: str) -> bool:
|
||||||
"""Validate a target against all configured rules."""
|
"""Validate a target against all configured rules."""
|
||||||
|
|
@ -281,7 +309,7 @@ class Directive(HyperglassUniqueModel, unique_by=("id", "table_output")):
|
||||||
return "text"
|
return "text"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@validator("plugins")
|
@field_validator("plugins")
|
||||||
def validate_plugins(cls: "Directive", plugins: t.List[str]) -> t.List[str]:
|
def validate_plugins(cls: "Directive", plugins: t.List[str]) -> t.List[str]:
|
||||||
"""Validate and register configured plugins."""
|
"""Validate and register configured plugins."""
|
||||||
plugin_dir = Settings.app_path / "plugins"
|
plugin_dir = Settings.app_path / "plugins"
|
||||||
|
|
@ -322,7 +350,7 @@ class Directive(HyperglassUniqueModel, unique_by=("id", "table_output")):
|
||||||
class BuiltinDirective(Directive, unique_by=("id", "table_output", "platforms")):
|
class BuiltinDirective(Directive, unique_by=("id", "table_output", "platforms")):
|
||||||
"""Natively-supported directive."""
|
"""Natively-supported directive."""
|
||||||
|
|
||||||
__hyperglass_builtin__: t.ClassVar[bool] = True
|
_hyperglass_builtin: bool = PrivateAttr(True)
|
||||||
platforms: Series[str] = []
|
platforms: Series[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -339,7 +367,7 @@ class Directives(MultiModel[Directive], model=Directive, unique_by="id"):
|
||||||
*(
|
*(
|
||||||
self.table_if_available(directive) if table_output else directive # noqa: IF100 GFY
|
self.table_if_available(directive) if table_output else directive # noqa: IF100 GFY
|
||||||
for directive in self
|
for directive in self
|
||||||
if directive.__hyperglass_builtin__ is True
|
if directive._hyperglass_builtin is True
|
||||||
and platform in getattr(directive, "platforms", ())
|
and platform in getattr(directive, "platforms", ())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,11 @@
|
||||||
# Standard Library
|
# Standard Library
|
||||||
import re
|
import re
|
||||||
import typing as t
|
import typing as t
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import StrictInt, StrictFloat
|
from pydantic import AfterValidator, BeforeValidator
|
||||||
|
|
||||||
IntFloat = t.TypeVar("IntFloat", StrictInt, StrictFloat)
|
IntFloat = t.TypeVar("IntFloat", int, float)
|
||||||
J = t.TypeVar("J")
|
J = t.TypeVar("J")
|
||||||
|
|
||||||
SupportedDriver = t.Literal["netmiko", "hyperglass_agent"]
|
SupportedDriver = t.Literal["netmiko", "hyperglass_agent"]
|
||||||
|
|
@ -17,133 +16,42 @@ HttpProvider = t.Literal["msteams", "slack", "generic"]
|
||||||
LogFormat = t.Literal["text", "json"]
|
LogFormat = t.Literal["text", "json"]
|
||||||
Primitives = t.Union[None, float, int, bool, str]
|
Primitives = t.Union[None, float, int, bool, str]
|
||||||
JsonValue = t.Union[J, t.Sequence[J], t.Dict[str, J]]
|
JsonValue = t.Union[J, t.Sequence[J], t.Dict[str, J]]
|
||||||
|
ActionValue = t.Literal["permit", "deny"]
|
||||||
|
HttpMethodValue = t.Literal[
|
||||||
|
"CONNECT",
|
||||||
|
"DELETE",
|
||||||
|
"GET",
|
||||||
|
"HEAD",
|
||||||
|
"OPTIONS",
|
||||||
|
"PATCH",
|
||||||
|
"POST",
|
||||||
|
"PUT",
|
||||||
|
"TRACE",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class AnyUri(str):
|
def validate_uri(value: str) -> str:
|
||||||
"""Custom field type for HTTP URI, e.g. /example."""
|
"""Ensure URI string contains a leading forward-slash."""
|
||||||
|
uri_regex = re.compile(r"^(\/.*)$")
|
||||||
@classmethod
|
match = uri_regex.fullmatch(value)
|
||||||
def __get_validators__(cls):
|
if not match:
|
||||||
"""Pydantic custom field method."""
|
raise ValueError("Invalid format. A URI must begin with a forward slash, e.g. '/example'")
|
||||||
yield cls.validate
|
return match.group()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def validate(cls, value):
|
|
||||||
"""Ensure URI string contains a leading forward-slash."""
|
|
||||||
uri_regex = re.compile(r"^(\/.*)$")
|
|
||||||
if not isinstance(value, str):
|
|
||||||
raise TypeError("AnyUri type must be a string")
|
|
||||||
match = uri_regex.fullmatch(value)
|
|
||||||
if not match:
|
|
||||||
raise ValueError(
|
|
||||||
"Invalid format. A URI must begin with a forward slash, e.g. '/example'"
|
|
||||||
)
|
|
||||||
return cls(match.group())
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
"""Stringify custom field representation."""
|
|
||||||
return f"AnyUri({super().__repr__()})"
|
|
||||||
|
|
||||||
|
|
||||||
class Action(str):
|
def validate_action(value: str) -> ActionValue:
|
||||||
"""Custom field type for policy actions."""
|
"""Ensure action is an allowed value or acceptable alias."""
|
||||||
|
|
||||||
permits = ("permit", "allow", "accept")
|
permits = ("permit", "allow", "accept")
|
||||||
denies = ("deny", "block", "reject")
|
denies = ("deny", "block", "reject")
|
||||||
|
value = value.strip().lower()
|
||||||
|
if value in permits:
|
||||||
|
return "permit"
|
||||||
|
if value in denies:
|
||||||
|
return "deny"
|
||||||
|
|
||||||
@classmethod
|
raise ValueError("Action must be one of '{}'".format(", ".join((*permits, *denies))))
|
||||||
def __get_validators__(cls):
|
|
||||||
"""Pydantic custom field method."""
|
|
||||||
yield cls.validate
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def validate(cls, value: str):
|
|
||||||
"""Ensure action is an allowed value or acceptable alias."""
|
|
||||||
if not isinstance(value, str):
|
|
||||||
raise TypeError("Action type must be a string")
|
|
||||||
value = value.strip().lower()
|
|
||||||
|
|
||||||
if value in cls.permits:
|
|
||||||
return cls("permit")
|
|
||||||
if value in cls.denies:
|
|
||||||
return cls("deny")
|
|
||||||
|
|
||||||
raise ValueError(
|
|
||||||
"Action must be one of '{}'".format(", ".join((*cls.permits, *cls.denies)))
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
"""Stringify custom field representation."""
|
|
||||||
return f"Action({super().__repr__()})"
|
|
||||||
|
|
||||||
|
|
||||||
class HttpMethod(str):
|
AnyUri = t.Annotated[str, AfterValidator(validate_uri)]
|
||||||
"""Custom field type for HTTP methods."""
|
Action = t.Annotated[ActionValue, AfterValidator(validate_action)]
|
||||||
|
HttpMethod = t.Annotated[HttpMethodValue, BeforeValidator(str.upper)]
|
||||||
methods = (
|
|
||||||
"CONNECT",
|
|
||||||
"DELETE",
|
|
||||||
"GET",
|
|
||||||
"HEAD",
|
|
||||||
"OPTIONS",
|
|
||||||
"PATCH",
|
|
||||||
"POST",
|
|
||||||
"PUT",
|
|
||||||
"TRACE",
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def __get_validators__(cls):
|
|
||||||
"""Pydantic custom field method."""
|
|
||||||
yield cls.validate
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def validate(cls, value: str):
|
|
||||||
"""Ensure http method is valid."""
|
|
||||||
if not isinstance(value, str):
|
|
||||||
raise TypeError("HTTP Method must be a string")
|
|
||||||
value = value.strip().upper()
|
|
||||||
|
|
||||||
if value in cls.methods:
|
|
||||||
return cls(value)
|
|
||||||
|
|
||||||
raise ValueError("HTTP Method must be one of {!r}".format(", ".join(cls.methods)))
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
"""Stringify custom field representation."""
|
|
||||||
return f"HttpMethod({super().__repr__()})"
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigPathItem(Path):
|
|
||||||
"""Custom field type for files or directories contained within app_path."""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def __get_validators__(cls):
|
|
||||||
"""Pydantic custom field method."""
|
|
||||||
yield cls.validate
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def validate(cls, value: Path) -> Path:
|
|
||||||
"""Ensure path is within app path."""
|
|
||||||
|
|
||||||
if isinstance(value, str):
|
|
||||||
value = Path(value)
|
|
||||||
|
|
||||||
if not isinstance(value, Path):
|
|
||||||
raise TypeError("Unable to convert type {} to ConfigPathItem".format(type(value)))
|
|
||||||
|
|
||||||
# Project
|
|
||||||
from hyperglass.settings import Settings
|
|
||||||
|
|
||||||
if not value.is_relative_to(Settings.app_path):
|
|
||||||
raise ValueError("{!s} must be relative to {!s}".format(value, Settings.app_path))
|
|
||||||
|
|
||||||
if Settings.container:
|
|
||||||
value = Settings.default_app_path.joinpath(
|
|
||||||
*(p for p in value.parts if p not in Settings.app_path.parts)
|
|
||||||
)
|
|
||||||
return value
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
"""Stringify custom field representation."""
|
|
||||||
return f"ConfigPathItem({super().__repr__()})"
|
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@ import typing as t
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import HttpUrl, BaseModel, BaseConfig, PrivateAttr
|
from pydantic import HttpUrl, BaseModel, PrivateAttr, RootModel, ConfigDict
|
||||||
from pydantic.generics import GenericModel
|
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
|
|
@ -20,38 +19,30 @@ from hyperglass.types import Series
|
||||||
MultiModelT = t.TypeVar("MultiModelT", bound=BaseModel)
|
MultiModelT = t.TypeVar("MultiModelT", bound=BaseModel)
|
||||||
|
|
||||||
|
|
||||||
|
def alias_generator(field: str) -> str:
|
||||||
|
"""Remove unsupported characters from field names.
|
||||||
|
|
||||||
|
Converts any "desirable" separators to underscore, then removes all
|
||||||
|
characters that are unsupported in Python class variable names.
|
||||||
|
Also removes leading numbers underscores.
|
||||||
|
"""
|
||||||
|
_replaced = re.sub(r"[\-|\.|\@|\~|\:\/|\s]", "_", field)
|
||||||
|
_scrubbed = "".join(re.findall(r"([a-zA-Z]\w+|\_+)", _replaced))
|
||||||
|
snake_field = _scrubbed.lower()
|
||||||
|
return snake_to_camel(snake_field)
|
||||||
|
|
||||||
|
|
||||||
class HyperglassModel(BaseModel):
|
class HyperglassModel(BaseModel):
|
||||||
"""Base model for all hyperglass configuration models."""
|
"""Base model for all hyperglass configuration models."""
|
||||||
|
|
||||||
class Config(BaseConfig):
|
model_config = ConfigDict(
|
||||||
"""Pydantic model configuration."""
|
extra="forbid",
|
||||||
|
json_encoders={HttpUrl: lambda v: str(v), Path: str},
|
||||||
validate_all = True
|
populate_by_name=True,
|
||||||
extra = "forbid"
|
validate_assignment=True,
|
||||||
validate_assignment = True
|
validate_default=True,
|
||||||
allow_population_by_field_name = True
|
alias_generator=alias_generator,
|
||||||
json_encoders = {HttpUrl: lambda v: str(v), Path: str}
|
)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def alias_generator(cls: "HyperglassModel", field: str) -> str:
|
|
||||||
"""Remove unsupported characters from field names.
|
|
||||||
|
|
||||||
Converts any "desirable" separators to underscore, then removes all
|
|
||||||
characters that are unsupported in Python class variable names.
|
|
||||||
Also removes leading numbers underscores.
|
|
||||||
"""
|
|
||||||
_replaced = re.sub(r"[\-|\.|\@|\~|\:\/|\s]", "_", field)
|
|
||||||
_scrubbed = "".join(re.findall(r"([a-zA-Z]\w+|\_+)", _replaced))
|
|
||||||
snake_field = _scrubbed.lower()
|
|
||||||
if snake_field != field:
|
|
||||||
log.debug(
|
|
||||||
"Model field '{}.{}' was converted from {} to {}",
|
|
||||||
cls.__module__,
|
|
||||||
snake_field,
|
|
||||||
repr(field),
|
|
||||||
repr(snake_field),
|
|
||||||
)
|
|
||||||
return snake_to_camel(snake_field)
|
|
||||||
|
|
||||||
def convert_paths(self, value: t.Any):
|
def convert_paths(self, value: t.Any):
|
||||||
"""Change path to relative to app_path."""
|
"""Change path to relative to app_path."""
|
||||||
|
|
@ -98,7 +89,7 @@ class HyperglassModel(BaseModel):
|
||||||
for key in kwargs.keys():
|
for key in kwargs.keys():
|
||||||
export_kwargs.pop(key, None)
|
export_kwargs.pop(key, None)
|
||||||
|
|
||||||
return self.json(*args, **export_kwargs, **kwargs)
|
return self.model_dump_json(*args, **export_kwargs, **kwargs)
|
||||||
|
|
||||||
def export_dict(self, *args, **kwargs):
|
def export_dict(self, *args, **kwargs):
|
||||||
"""Return instance as dictionary."""
|
"""Return instance as dictionary."""
|
||||||
|
|
@ -108,7 +99,7 @@ class HyperglassModel(BaseModel):
|
||||||
for key in kwargs.keys():
|
for key in kwargs.keys():
|
||||||
export_kwargs.pop(key, None)
|
export_kwargs.pop(key, None)
|
||||||
|
|
||||||
return self.dict(*args, **export_kwargs, **kwargs)
|
return self.model_dump(*args, **export_kwargs, **kwargs)
|
||||||
|
|
||||||
def export_yaml(self, *args, **kwargs):
|
def export_yaml(self, *args, **kwargs):
|
||||||
"""Return instance as YAML."""
|
"""Return instance as YAML."""
|
||||||
|
|
@ -177,47 +168,45 @@ class HyperglassModelWithId(HyperglassModel):
|
||||||
return hash(self.id)
|
return hash(self.id)
|
||||||
|
|
||||||
|
|
||||||
class MultiModel(GenericModel, t.Generic[MultiModelT]):
|
class MultiModel(RootModel[MultiModelT]):
|
||||||
"""Extension of HyperglassModel for managing multiple models as a list."""
|
"""Extension of HyperglassModel for managing multiple models as a list."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
validate_default=True,
|
||||||
|
validate_assignment=True,
|
||||||
|
)
|
||||||
|
|
||||||
model: t.ClassVar[MultiModelT]
|
model: t.ClassVar[MultiModelT]
|
||||||
unique_by: t.ClassVar[str]
|
unique_by: t.ClassVar[str]
|
||||||
model_name: t.ClassVar[str] = "MultiModel"
|
_model_name: t.ClassVar[str] = "MultiModel"
|
||||||
|
|
||||||
__root__: t.List[MultiModelT] = []
|
root: t.List[MultiModelT] = []
|
||||||
_count: int = PrivateAttr()
|
_count: int = PrivateAttr()
|
||||||
|
|
||||||
class Config(BaseConfig):
|
|
||||||
"""Pydantic model configuration."""
|
|
||||||
|
|
||||||
validate_all = True
|
|
||||||
extra = "forbid"
|
|
||||||
validate_assignment = True
|
|
||||||
|
|
||||||
def __init__(self, *items: t.Union[MultiModelT, t.Dict[str, t.Any]]) -> None:
|
def __init__(self, *items: t.Union[MultiModelT, t.Dict[str, t.Any]]) -> None:
|
||||||
"""Validate items."""
|
"""Validate items."""
|
||||||
for cls_var in ("model", "unique_by"):
|
for cls_var in ("model", "unique_by"):
|
||||||
if getattr(self, cls_var, None) is None:
|
if getattr(self, cls_var, None) is None:
|
||||||
raise AttributeError(f"MultiModel is missing class variable '{cls_var}'")
|
raise AttributeError(f"MultiModel is missing class variable '{cls_var}'")
|
||||||
valid = self._valid_items(*items)
|
valid = self._valid_items(*items)
|
||||||
super().__init__(__root__=valid)
|
super().__init__(root=valid)
|
||||||
self._count = len(self.__root__)
|
self._count = len(self.root)
|
||||||
|
|
||||||
def __init_subclass__(cls, **kw: t.Any) -> None:
|
def __init_subclass__(cls, **kw: t.Any) -> None:
|
||||||
"""Add class variables from keyword arguments."""
|
"""Add class variables from keyword arguments."""
|
||||||
model = kw.pop("model", None)
|
model = kw.pop("model", None)
|
||||||
cls.model = model
|
cls.model = model
|
||||||
cls.unique_by = kw.pop("unique_by", None)
|
cls.unique_by = kw.pop("unique_by", None)
|
||||||
cls.model_name = getattr(model, "__name__", "MultiModel")
|
cls._model_name = getattr(model, "__name__", "MultiModel")
|
||||||
super().__init_subclass__()
|
super().__init_subclass__()
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
"""Represent model."""
|
"""Represent model."""
|
||||||
return repr_from_attrs(self, ["_count", "unique_by", "model_name"], strip="_")
|
return repr_from_attrs(self, ["_count", "unique_by", "_model_name"], strip="_")
|
||||||
|
|
||||||
def __iter__(self) -> t.Iterator[MultiModelT]:
|
def __iter__(self) -> t.Iterator[MultiModelT]:
|
||||||
"""Iterate items."""
|
"""Iterate items."""
|
||||||
return iter(self.__root__)
|
return iter(self.root)
|
||||||
|
|
||||||
def __getitem__(self, value: t.Union[int, str]) -> MultiModelT:
|
def __getitem__(self, value: t.Union[int, str]) -> MultiModelT:
|
||||||
"""Get an item by its `unique_by` property."""
|
"""Get an item by its `unique_by` property."""
|
||||||
|
|
@ -228,7 +217,7 @@ class MultiModel(GenericModel, t.Generic[MultiModelT]):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if isinstance(value, int):
|
if isinstance(value, int):
|
||||||
return self.__root__[value]
|
return self.root[value]
|
||||||
|
|
||||||
for item in self:
|
for item in self:
|
||||||
if hasattr(item, self.unique_by) and getattr(item, self.unique_by) == value:
|
if hasattr(item, self.unique_by) and getattr(item, self.unique_by) == value:
|
||||||
|
|
@ -265,7 +254,7 @@ class MultiModel(GenericModel, t.Generic[MultiModelT]):
|
||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
"""Get number of items."""
|
"""Get number of items."""
|
||||||
return len(self.__root__)
|
return len(self.root)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ids(self) -> t.Tuple[t.Any, ...]:
|
def ids(self) -> t.Tuple[t.Any, ...]:
|
||||||
|
|
@ -283,7 +272,7 @@ class MultiModel(GenericModel, t.Generic[MultiModelT]):
|
||||||
new = type(name, (cls,), cls.__dict__)
|
new = type(name, (cls,), cls.__dict__)
|
||||||
new.model = model
|
new.model = model
|
||||||
new.unique_by = unique_by
|
new.unique_by = unique_by
|
||||||
new.model_name = getattr(model, "__name__", "MultiModel")
|
new._model_name = getattr(model, "__name__", "MultiModel")
|
||||||
return new
|
return new
|
||||||
|
|
||||||
def _valid_items(
|
def _valid_items(
|
||||||
|
|
@ -317,7 +306,7 @@ class MultiModel(GenericModel, t.Generic[MultiModelT]):
|
||||||
if getattr(o, unique_by) == v
|
if getattr(o, unique_by) == v
|
||||||
}
|
}
|
||||||
return tuple(unique_by_objects.values())
|
return tuple(unique_by_objects.values())
|
||||||
return (*self.__root__, *to_add)
|
return (*self.root, *to_add)
|
||||||
|
|
||||||
def filter(self, *properties: str) -> MultiModelT:
|
def filter(self, *properties: str) -> MultiModelT:
|
||||||
"""Get only items with `unique_by` properties matching values in `properties`."""
|
"""Get only items with `unique_by` properties matching values in `properties`."""
|
||||||
|
|
@ -345,8 +334,8 @@ class MultiModel(GenericModel, t.Generic[MultiModelT]):
|
||||||
def add(self, *items, unique_by: t.Optional[str] = None) -> None:
|
def add(self, *items, unique_by: t.Optional[str] = None) -> None:
|
||||||
"""Add an item to the model."""
|
"""Add an item to the model."""
|
||||||
new = self._merge_with(*items, unique_by=unique_by)
|
new = self._merge_with(*items, unique_by=unique_by)
|
||||||
self.__root__ = new
|
self.root = new
|
||||||
self._count = len(self.__root__)
|
self._count = len(self.root)
|
||||||
for item in new:
|
for item in new:
|
||||||
log.debug(
|
log.debug(
|
||||||
"Added {} '{!s}' to {}",
|
"Added {} '{!s}' to {}",
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
"""Data Models for Parsing Arista JSON Response."""
|
"""Data Models for Parsing Arista JSON Response."""
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
from typing import Dict, List, Optional
|
import typing as t
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import ConfigDict
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
from hyperglass.models.data import BGPRouteTable
|
from hyperglass.models.data import BGPRouteTable
|
||||||
|
|
@ -29,16 +31,14 @@ def _alias_generator(field: str) -> str:
|
||||||
class _AristaBase(HyperglassModel):
|
class _AristaBase(HyperglassModel):
|
||||||
"""Base Model for Arista validation."""
|
"""Base Model for Arista validation."""
|
||||||
|
|
||||||
class Config:
|
model_config = ConfigDict(extra="ignore", alias_generator=_alias_generator)
|
||||||
extra = "ignore"
|
|
||||||
alias_generator = _alias_generator
|
|
||||||
|
|
||||||
|
|
||||||
class AristaAsPathEntry(_AristaBase):
|
class AristaAsPathEntry(_AristaBase):
|
||||||
"""Validation model for Arista asPathEntry."""
|
"""Validation model for Arista asPathEntry."""
|
||||||
|
|
||||||
as_path_type: str = "External"
|
as_path_type: str = "External"
|
||||||
as_path: Optional[str] = ""
|
as_path: t.Optional[str] = ""
|
||||||
|
|
||||||
|
|
||||||
class AristaPeerEntry(_AristaBase):
|
class AristaPeerEntry(_AristaBase):
|
||||||
|
|
@ -55,18 +55,18 @@ class AristaRouteType(_AristaBase):
|
||||||
suppressed: bool
|
suppressed: bool
|
||||||
valid: bool
|
valid: bool
|
||||||
active: bool
|
active: bool
|
||||||
origin_validity: Optional[str] = "notVerified"
|
origin_validity: t.Optional[str] = "notVerified"
|
||||||
|
|
||||||
|
|
||||||
class AristaRouteDetail(_AristaBase):
|
class AristaRouteDetail(_AristaBase):
|
||||||
"""Validation for Arista routeDetail."""
|
"""Validation for Arista routeDetail."""
|
||||||
|
|
||||||
origin: str
|
origin: str
|
||||||
label_stack: List = []
|
label_stack: t.List = []
|
||||||
ext_community_list: List[str] = []
|
ext_community_list: t.List[str] = []
|
||||||
ext_community_list_raw: List[str] = []
|
ext_community_list_raw: t.List[str] = []
|
||||||
community_list: List[str] = []
|
community_list: t.List[str] = []
|
||||||
large_community_list: List[str] = []
|
large_community_list: t.List[str] = []
|
||||||
|
|
||||||
|
|
||||||
class AristaRoutePath(_AristaBase):
|
class AristaRoutePath(_AristaBase):
|
||||||
|
|
@ -81,16 +81,16 @@ class AristaRoutePath(_AristaBase):
|
||||||
timestamp: int = int(datetime.utcnow().timestamp())
|
timestamp: int = int(datetime.utcnow().timestamp())
|
||||||
next_hop: str
|
next_hop: str
|
||||||
route_type: AristaRouteType
|
route_type: AristaRouteType
|
||||||
route_detail: Optional[AristaRouteDetail]
|
route_detail: t.Optional[AristaRouteDetail]
|
||||||
|
|
||||||
|
|
||||||
class AristaRouteEntry(_AristaBase):
|
class AristaRouteEntry(_AristaBase):
|
||||||
"""Validation model for Arista bgpRouteEntries."""
|
"""Validation model for Arista bgpRouteEntries."""
|
||||||
|
|
||||||
total_paths: int = 0
|
total_paths: int = 0
|
||||||
bgp_advertised_peer_groups: Dict = {}
|
bgp_advertised_peer_groups: t.Dict = {}
|
||||||
mask_length: int
|
mask_length: int
|
||||||
bgp_route_paths: List[AristaRoutePath] = []
|
bgp_route_paths: t.List[AristaRoutePath] = []
|
||||||
|
|
||||||
|
|
||||||
class AristaBGPTable(_AristaBase):
|
class AristaBGPTable(_AristaBase):
|
||||||
|
|
@ -98,7 +98,7 @@ class AristaBGPTable(_AristaBase):
|
||||||
|
|
||||||
router_id: str
|
router_id: str
|
||||||
vrf: str
|
vrf: str
|
||||||
bgp_route_entries: Dict[str, AristaRouteEntry]
|
bgp_route_entries: t.Dict[str, AristaRouteEntry]
|
||||||
# The raw value is really a string, but `int` will convert it.
|
# The raw value is really a string, but `int` will convert it.
|
||||||
asn: int
|
asn: int
|
||||||
|
|
||||||
|
|
@ -109,7 +109,7 @@ class AristaBGPTable(_AristaBase):
|
||||||
return now_timestamp - timestamp
|
return now_timestamp - timestamp
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_as_path(as_path: str) -> List[str]:
|
def _get_as_path(as_path: str) -> t.List[str]:
|
||||||
if as_path == "":
|
if as_path == "":
|
||||||
return []
|
return []
|
||||||
return [int(p) for p in as_path.split() if p.isdecimal()]
|
return [int(p) for p in as_path.split() if p.isdecimal()]
|
||||||
|
|
@ -119,11 +119,9 @@ class AristaBGPTable(_AristaBase):
|
||||||
routes = []
|
routes = []
|
||||||
count = 0
|
count = 0
|
||||||
for prefix, entries in self.bgp_route_entries.items():
|
for prefix, entries in self.bgp_route_entries.items():
|
||||||
|
|
||||||
count += entries.total_paths
|
count += entries.total_paths
|
||||||
|
|
||||||
for route in entries.bgp_route_paths:
|
for route in entries.bgp_route_paths:
|
||||||
|
|
||||||
as_path = self._get_as_path(route.as_path_entry.as_path)
|
as_path = self._get_as_path(route.as_path_entry.as_path)
|
||||||
rpki_state = RPKI_STATE_MAP.get(route.route_type.origin_validity, 3)
|
rpki_state = RPKI_STATE_MAP.get(route.route_type.origin_validity, 3)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
"""Data Models for Parsing FRRouting JSON Response."""
|
"""Data Models for Parsing FRRouting JSON Response."""
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
from typing import List
|
import typing as t
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import StrictInt, StrictStr, StrictBool, constr, root_validator
|
from pydantic import model_validator, ConfigDict
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
|
|
@ -14,7 +14,7 @@ from hyperglass.models.data import BGPRouteTable
|
||||||
# Local
|
# Local
|
||||||
from ..main import HyperglassModel
|
from ..main import HyperglassModel
|
||||||
|
|
||||||
FRRPeerType = constr(regex=r"(internal|external)")
|
FRRPeerType = t.Literal["internal", "external"]
|
||||||
|
|
||||||
|
|
||||||
def _alias_generator(field):
|
def _alias_generator(field):
|
||||||
|
|
@ -23,46 +23,44 @@ def _alias_generator(field):
|
||||||
|
|
||||||
|
|
||||||
class _FRRBase(HyperglassModel):
|
class _FRRBase(HyperglassModel):
|
||||||
class Config:
|
model_config = ConfigDict(alias_generator=_alias_generator, extra="ignore")
|
||||||
alias_generator = _alias_generator
|
|
||||||
extra = "ignore"
|
|
||||||
|
|
||||||
|
|
||||||
class FRRNextHop(_FRRBase):
|
class FRRNextHop(_FRRBase):
|
||||||
"""FRR Next Hop Model."""
|
"""FRR Next Hop Model."""
|
||||||
|
|
||||||
ip: StrictStr
|
ip: str
|
||||||
afi: StrictStr
|
afi: str
|
||||||
metric: StrictInt
|
metric: int
|
||||||
accessible: StrictBool
|
accessible: bool
|
||||||
used: StrictBool
|
used: bool
|
||||||
|
|
||||||
|
|
||||||
class FRRPeer(_FRRBase):
|
class FRRPeer(_FRRBase):
|
||||||
"""FRR Peer Model."""
|
"""FRR Peer Model."""
|
||||||
|
|
||||||
peer_id: StrictStr
|
peer_id: str
|
||||||
router_id: StrictStr
|
router_id: str
|
||||||
type: FRRPeerType
|
type: FRRPeerType
|
||||||
|
|
||||||
|
|
||||||
class FRRPath(_FRRBase):
|
class FRRPath(_FRRBase):
|
||||||
"""FRR Path Model."""
|
"""FRR Path Model."""
|
||||||
|
|
||||||
aspath: List[StrictInt]
|
aspath: t.List[int]
|
||||||
aggregator_as: StrictInt
|
aggregator_as: int
|
||||||
aggregator_id: StrictStr
|
aggregator_id: str
|
||||||
med: StrictInt = 0
|
med: int = 0
|
||||||
localpref: StrictInt
|
localpref: int
|
||||||
weight: StrictInt
|
weight: int
|
||||||
valid: StrictBool
|
valid: bool
|
||||||
last_update: StrictInt
|
last_update: int
|
||||||
bestpath: StrictBool
|
bestpath: bool
|
||||||
community: List[StrictStr]
|
community: t.List[str]
|
||||||
nexthops: List[FRRNextHop]
|
nexthops: t.List[FRRNextHop]
|
||||||
peer: FRRPeer
|
peer: FRRPeer
|
||||||
|
|
||||||
@root_validator(pre=True)
|
@model_validator(pre=True)
|
||||||
def validate_path(cls, values):
|
def validate_path(cls, values):
|
||||||
"""Extract meaningful data from FRR response."""
|
"""Extract meaningful data from FRR response."""
|
||||||
new = values.copy()
|
new = values.copy()
|
||||||
|
|
@ -77,8 +75,8 @@ class FRRPath(_FRRBase):
|
||||||
class FRRRoute(_FRRBase):
|
class FRRRoute(_FRRBase):
|
||||||
"""FRR Route Model."""
|
"""FRR Route Model."""
|
||||||
|
|
||||||
prefix: StrictStr
|
prefix: str
|
||||||
paths: List[FRRPath] = []
|
paths: t.List[FRRPath] = []
|
||||||
|
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
"""Convert the FRR-specific fields to standard parsed data model."""
|
"""Convert the FRR-specific fields to standard parsed data model."""
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
"""Data Models for Parsing Juniper XML Response."""
|
"""Data Models for Parsing Juniper XML Response."""
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
from typing import Any, Dict, List
|
import typing as t
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import validator, root_validator
|
from pydantic import field_validator, model_validator, ConfigDict
|
||||||
from pydantic.types import StrictInt, StrictStr, StrictBool
|
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
|
|
@ -26,7 +25,7 @@ RPKI_STATE_MAP = {
|
||||||
class JuniperBase(HyperglassModel, extra="ignore"):
|
class JuniperBase(HyperglassModel, extra="ignore"):
|
||||||
"""Base Juniper model."""
|
"""Base Juniper model."""
|
||||||
|
|
||||||
def __init__(self, **kwargs: Any) -> None:
|
def __init__(self, **kwargs: t.Any) -> None:
|
||||||
"""Convert all `-` keys to `_`.
|
"""Convert all `-` keys to `_`.
|
||||||
|
|
||||||
Default camelCase alias generator will still be used.
|
Default camelCase alias generator will still be used.
|
||||||
|
|
@ -38,22 +37,24 @@ class JuniperBase(HyperglassModel, extra="ignore"):
|
||||||
class JuniperRouteTableEntry(JuniperBase):
|
class JuniperRouteTableEntry(JuniperBase):
|
||||||
"""Parse Juniper rt-entry data."""
|
"""Parse Juniper rt-entry data."""
|
||||||
|
|
||||||
active_tag: StrictBool
|
model_config = ConfigDict(validate_assignment=False)
|
||||||
|
|
||||||
|
active_tag: bool
|
||||||
preference: int
|
preference: int
|
||||||
age: StrictInt
|
age: int
|
||||||
local_preference: int
|
local_preference: int
|
||||||
metric: int = 0
|
metric: int = 0
|
||||||
as_path: List[StrictInt] = []
|
as_path: t.List[int] = []
|
||||||
validation_state: StrictInt = 3
|
validation_state: int = 3
|
||||||
next_hop: StrictStr
|
next_hop: str
|
||||||
peer_rid: StrictStr
|
peer_rid: str
|
||||||
peer_as: int
|
peer_as: int
|
||||||
source_as: int
|
source_as: int
|
||||||
source_rid: StrictStr
|
source_rid: str
|
||||||
communities: List[StrictStr] = None
|
communities: t.List[str] = None
|
||||||
|
|
||||||
@root_validator(pre=True)
|
@model_validator(mode="before")
|
||||||
def validate_optional_flags(cls, values):
|
def validate_optional_flags(cls, values: t.Dict[str, t.Any]):
|
||||||
"""Flatten & rename keys prior to validation."""
|
"""Flatten & rename keys prior to validation."""
|
||||||
next_hops = []
|
next_hops = []
|
||||||
nh = None
|
nh = None
|
||||||
|
|
@ -67,7 +68,7 @@ class JuniperRouteTableEntry(JuniperBase):
|
||||||
nh = values.pop("protocol_nh")
|
nh = values.pop("protocol_nh")
|
||||||
|
|
||||||
# Force the next hops to be a list
|
# Force the next hops to be a list
|
||||||
if isinstance(nh, Dict):
|
if isinstance(nh, t.Dict):
|
||||||
nh = [nh]
|
nh = [nh]
|
||||||
|
|
||||||
if nh is not None:
|
if nh is not None:
|
||||||
|
|
@ -94,12 +95,12 @@ class JuniperRouteTableEntry(JuniperBase):
|
||||||
|
|
||||||
return values
|
return values
|
||||||
|
|
||||||
@validator("validation_state", pre=True, always=True)
|
@field_validator("validation_state", mode="before")
|
||||||
def validate_rpki_state(cls, value):
|
def validate_rpki_state(cls, value):
|
||||||
"""Convert string RPKI state to standard integer mapping."""
|
"""Convert string RPKI state to standard integer mapping."""
|
||||||
return RPKI_STATE_MAP.get(value, 3)
|
return RPKI_STATE_MAP.get(value, 3)
|
||||||
|
|
||||||
@validator("active_tag", pre=True, always=True)
|
@field_validator("active_tag", mode="before")
|
||||||
def validate_active_tag(cls, value):
|
def validate_active_tag(cls, value):
|
||||||
"""Convert active-tag from string/null to boolean."""
|
"""Convert active-tag from string/null to boolean."""
|
||||||
if value == "*":
|
if value == "*":
|
||||||
|
|
@ -108,7 +109,7 @@ class JuniperRouteTableEntry(JuniperBase):
|
||||||
value = False
|
value = False
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@validator("age", pre=True, always=True)
|
@field_validator("age", mode="before")
|
||||||
def validate_age(cls, value):
|
def validate_age(cls, value):
|
||||||
"""Get age as seconds."""
|
"""Get age as seconds."""
|
||||||
if not isinstance(value, dict):
|
if not isinstance(value, dict):
|
||||||
|
|
@ -120,13 +121,13 @@ class JuniperRouteTableEntry(JuniperBase):
|
||||||
value = value.get("@junos:seconds", 0)
|
value = value.get("@junos:seconds", 0)
|
||||||
return int(value)
|
return int(value)
|
||||||
|
|
||||||
@validator("as_path", pre=True, always=True)
|
@field_validator("as_path", mode="before")
|
||||||
def validate_as_path(cls, value):
|
def validate_as_path(cls, value):
|
||||||
"""Remove origin flags from AS_PATH."""
|
"""Remove origin flags from AS_PATH."""
|
||||||
disallowed = ("E", "I", "?")
|
disallowed = ("E", "I", "?")
|
||||||
return [int(a) for a in value.split() if a not in disallowed]
|
return [int(a) for a in value.split() if a not in disallowed]
|
||||||
|
|
||||||
@validator("communities", pre=True, always=True)
|
@field_validator("communities", mode="before")
|
||||||
def validate_communities(cls, value):
|
def validate_communities(cls, value):
|
||||||
"""Flatten community list."""
|
"""Flatten community list."""
|
||||||
if value is not None:
|
if value is not None:
|
||||||
|
|
@ -139,13 +140,13 @@ class JuniperRouteTableEntry(JuniperBase):
|
||||||
class JuniperRouteTable(JuniperBase):
|
class JuniperRouteTable(JuniperBase):
|
||||||
"""Validation model for Juniper rt data."""
|
"""Validation model for Juniper rt data."""
|
||||||
|
|
||||||
rt_destination: StrictStr
|
rt_destination: str
|
||||||
rt_prefix_length: int
|
rt_prefix_length: int
|
||||||
rt_entry_count: int
|
rt_entry_count: int
|
||||||
rt_announced_count: int
|
rt_announced_count: int
|
||||||
rt_entry: List[JuniperRouteTableEntry]
|
rt_entry: t.List[JuniperRouteTableEntry]
|
||||||
|
|
||||||
@validator("rt_entry_count", pre=True, always=True)
|
@field_validator("rt_entry_count", mode="before")
|
||||||
def validate_entry_count(cls, value):
|
def validate_entry_count(cls, value):
|
||||||
"""Flatten & convert entry-count to integer."""
|
"""Flatten & convert entry-count to integer."""
|
||||||
return int(value.get("#text"))
|
return int(value.get("#text"))
|
||||||
|
|
@ -154,12 +155,12 @@ class JuniperRouteTable(JuniperBase):
|
||||||
class JuniperBGPTable(JuniperBase):
|
class JuniperBGPTable(JuniperBase):
|
||||||
"""Validation model for route-table data."""
|
"""Validation model for route-table data."""
|
||||||
|
|
||||||
table_name: StrictStr
|
table_name: str
|
||||||
destination_count: int
|
destination_count: int
|
||||||
total_route_count: int
|
total_route_count: int
|
||||||
active_route_count: int
|
active_route_count: int
|
||||||
hidden_route_count: int
|
hidden_route_count: int
|
||||||
rt: List[JuniperRouteTable]
|
rt: t.List[JuniperRouteTable]
|
||||||
|
|
||||||
def bgp_table(self: "JuniperBGPTable") -> "BGPRouteTable":
|
def bgp_table(self: "JuniperBGPTable") -> "BGPRouteTable":
|
||||||
"""Convert the Juniper-specific fields to standard parsed data model."""
|
"""Convert the Juniper-specific fields to standard parsed data model."""
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,14 @@ from pydantic import (
|
||||||
FilePath,
|
FilePath,
|
||||||
RedisDsn,
|
RedisDsn,
|
||||||
SecretStr,
|
SecretStr,
|
||||||
BaseSettings,
|
|
||||||
DirectoryPath,
|
DirectoryPath,
|
||||||
IPvAnyAddress,
|
IPvAnyAddress,
|
||||||
validator,
|
field_validator,
|
||||||
|
ValidationInfo,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.util import at_least, cpu_count
|
from hyperglass.util import at_least, cpu_count
|
||||||
|
|
||||||
|
|
@ -31,11 +33,7 @@ _default_app_path = Path("/etc/hyperglass")
|
||||||
class HyperglassSettings(BaseSettings):
|
class HyperglassSettings(BaseSettings):
|
||||||
"""hyperglass system settings, required to start hyperglass."""
|
"""hyperglass system settings, required to start hyperglass."""
|
||||||
|
|
||||||
class Config:
|
model_config = SettingsConfigDict(env_prefix="hyperglass_")
|
||||||
"""hyperglass system settings configuration."""
|
|
||||||
|
|
||||||
env_prefix = "hyperglass_"
|
|
||||||
underscore_attrs_are_private = True
|
|
||||||
|
|
||||||
config_file_names: t.ClassVar[t.Tuple[str, ...]] = ("config", "devices", "directives")
|
config_file_names: t.ClassVar[t.Tuple[str, ...]] = ("config", "devices", "directives")
|
||||||
default_app_path: t.ClassVar[Path] = _default_app_path
|
default_app_path: t.ClassVar[Path] = _default_app_path
|
||||||
|
|
@ -46,12 +44,12 @@ class HyperglassSettings(BaseSettings):
|
||||||
disable_ui: bool = False
|
disable_ui: bool = False
|
||||||
app_path: DirectoryPath = _default_app_path
|
app_path: DirectoryPath = _default_app_path
|
||||||
redis_host: str = "localhost"
|
redis_host: str = "localhost"
|
||||||
redis_password: t.Optional[SecretStr]
|
redis_password: t.Optional[SecretStr] = None
|
||||||
redis_db: int = 1
|
redis_db: int = 1
|
||||||
redis_dsn: RedisDsn = None
|
redis_dsn: RedisDsn = None
|
||||||
host: IPvAnyAddress = None
|
host: IPvAnyAddress = None
|
||||||
port: int = 8001
|
port: int = 8001
|
||||||
ca_cert: t.Optional[FilePath]
|
ca_cert: t.Optional[FilePath] = None
|
||||||
container: bool = False
|
container: bool = False
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
|
|
@ -88,16 +86,16 @@ class HyperglassSettings(BaseSettings):
|
||||||
|
|
||||||
yield Panel.fit(table, title="hyperglass settings", border_style="subtle")
|
yield Panel.fit(table, title="hyperglass settings", border_style="subtle")
|
||||||
|
|
||||||
@validator("host", pre=True, always=True)
|
@field_validator("host", mode="before")
|
||||||
def validate_host(
|
def validate_host(
|
||||||
cls: "HyperglassSettings", value: t.Any, values: t.Dict[str, t.Any]
|
cls: "HyperglassSettings", value: t.Any, info: ValidationInfo
|
||||||
) -> IPvAnyAddress:
|
) -> IPvAnyAddress:
|
||||||
"""Set default host based on debug mode."""
|
"""Set default host based on debug mode."""
|
||||||
|
|
||||||
if value is None:
|
if value is None:
|
||||||
if values["debug"] is False:
|
if info.data.get("debug") is False:
|
||||||
return ip_address("::1")
|
return ip_address("::1")
|
||||||
if values["debug"] is True:
|
if info.data.get("debug") is True:
|
||||||
return ip_address("::")
|
return ip_address("::")
|
||||||
|
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
|
|
@ -112,20 +110,16 @@ class HyperglassSettings(BaseSettings):
|
||||||
|
|
||||||
raise ValueError(str(value))
|
raise ValueError(str(value))
|
||||||
|
|
||||||
@validator("redis_dsn", always=True)
|
@field_validator("redis_dsn", mode="before")
|
||||||
def validate_redis_dsn(
|
def validate_redis_dsn(cls, value: t.Any, info: ValidationInfo) -> RedisDsn:
|
||||||
cls: "HyperglassSettings", value: t.Any, values: t.Dict[str, t.Any]
|
|
||||||
) -> RedisDsn:
|
|
||||||
"""Construct a Redis DSN if none is provided."""
|
"""Construct a Redis DSN if none is provided."""
|
||||||
if value is None:
|
if value is None:
|
||||||
dsn = "redis://{}/{!s}".format(values["redis_host"], values["redis_db"])
|
host = info.data.get("redis_host")
|
||||||
password = values.get("redis_password")
|
db = info.data.get("redis_db")
|
||||||
|
dsn = "redis://{}/{!s}".format(host, db)
|
||||||
|
password = info.data.get("redis_password")
|
||||||
if password is not None:
|
if password is not None:
|
||||||
dsn = "redis://:{}@{}/{!s}".format(
|
dsn = "redis://:{}@{}/{!s}".format(password.get_secret_value(), host, db)
|
||||||
password.get_secret_value(),
|
|
||||||
values["redis_host"],
|
|
||||||
values["redis_db"],
|
|
||||||
)
|
|
||||||
return dsn
|
return dsn
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
"""UI Configuration models."""
|
"""UI Configuration models."""
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
from typing import Any, Dict, List, Tuple, Union, Literal, Optional
|
import typing as t
|
||||||
|
|
||||||
# Third Party
|
|
||||||
from pydantic import StrictStr, StrictBool
|
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from .main import HyperglassModel
|
from .main import HyperglassModel
|
||||||
|
|
@ -13,45 +10,45 @@ from .config.cache import CachePublic
|
||||||
from .config.params import ParamsPublic
|
from .config.params import ParamsPublic
|
||||||
from .config.messages import Messages
|
from .config.messages import Messages
|
||||||
|
|
||||||
Alignment = Union[Literal["left"], Literal["center"], Literal["right"], None]
|
Alignment = t.Union[t.Literal["left"], t.Literal["center"], t.Literal["right"], None]
|
||||||
StructuredDataField = Tuple[str, str, Alignment]
|
StructuredDataField = t.Tuple[str, str, Alignment]
|
||||||
|
|
||||||
|
|
||||||
class UIDirective(HyperglassModel):
|
class UIDirective(HyperglassModel):
|
||||||
"""UI: Directive."""
|
"""UI: Directive."""
|
||||||
|
|
||||||
id: StrictStr
|
id: str
|
||||||
name: StrictStr
|
name: str
|
||||||
field_type: StrictStr
|
field_type: str
|
||||||
groups: List[StrictStr]
|
groups: t.List[str]
|
||||||
description: StrictStr
|
description: str
|
||||||
info: Optional[str] = None
|
info: t.Optional[str] = None
|
||||||
options: Optional[List[Dict[str, Any]]]
|
options: t.Optional[t.List[t.Dict[str, t.Any]]] = None
|
||||||
|
|
||||||
|
|
||||||
class UILocation(HyperglassModel):
|
class UILocation(HyperglassModel):
|
||||||
"""UI: Location (Device)."""
|
"""UI: Location (Device)."""
|
||||||
|
|
||||||
id: StrictStr
|
id: str
|
||||||
name: StrictStr
|
name: str
|
||||||
group: Optional[StrictStr]
|
group: t.Optional[str] = None
|
||||||
avatar: Optional[StrictStr]
|
avatar: t.Optional[str] = None
|
||||||
description: Optional[StrictStr]
|
description: t.Optional[str] = None
|
||||||
directives: List[UIDirective] = []
|
directives: t.List[UIDirective] = []
|
||||||
|
|
||||||
|
|
||||||
class UIDevices(HyperglassModel):
|
class UIDevices(HyperglassModel):
|
||||||
"""UI: Devices."""
|
"""UI: Devices."""
|
||||||
|
|
||||||
group: Optional[StrictStr]
|
group: t.Optional[str] = None
|
||||||
locations: List[UILocation] = []
|
locations: t.List[UILocation] = []
|
||||||
|
|
||||||
|
|
||||||
class UIContent(HyperglassModel):
|
class UIContent(HyperglassModel):
|
||||||
"""UI: Content."""
|
"""UI: Content."""
|
||||||
|
|
||||||
credit: StrictStr
|
credit: str
|
||||||
greeting: StrictStr
|
greeting: str
|
||||||
|
|
||||||
|
|
||||||
class UIParameters(ParamsPublic, HyperglassModel):
|
class UIParameters(ParamsPublic, HyperglassModel):
|
||||||
|
|
@ -60,8 +57,8 @@ class UIParameters(ParamsPublic, HyperglassModel):
|
||||||
cache: CachePublic
|
cache: CachePublic
|
||||||
web: WebPublic
|
web: WebPublic
|
||||||
messages: Messages
|
messages: Messages
|
||||||
version: StrictStr
|
version: str
|
||||||
devices: List[UIDevices] = []
|
devices: t.List[UIDevices] = []
|
||||||
parsed_data_fields: Tuple[StructuredDataField, ...]
|
parsed_data_fields: t.Tuple[StructuredDataField, ...]
|
||||||
content: UIContent
|
content: UIContent
|
||||||
developer_mode: StrictBool
|
developer_mode: bool
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""Model utilities."""
|
"""Model utilities."""
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
from typing import Any, Dict, Tuple
|
import typing as t
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
@ -34,7 +34,7 @@ class LegacyField(BaseModel):
|
||||||
required: bool = True
|
required: bool = True
|
||||||
|
|
||||||
|
|
||||||
LEGACY_FIELDS: Dict[str, Tuple[LegacyField, ...]] = {
|
LEGACY_FIELDS: t.Dict[str, t.Tuple[LegacyField, ...]] = {
|
||||||
"Device": (
|
"Device": (
|
||||||
LegacyField(old="nos", new="platform", overwrite=True),
|
LegacyField(old="nos", new="platform", overwrite=True),
|
||||||
LegacyField(old="network", new="group", overwrite=False, required=False),
|
LegacyField(old="network", new="group", overwrite=False, required=False),
|
||||||
|
|
@ -43,7 +43,7 @@ LEGACY_FIELDS: Dict[str, Tuple[LegacyField, ...]] = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def check_legacy_fields(*, model: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
def check_legacy_fields(*, model: str, data: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
|
||||||
"""Check for legacy fields prior to model initialization."""
|
"""Check for legacy fields prior to model initialization."""
|
||||||
if model in LEGACY_FIELDS:
|
if model in LEGACY_FIELDS:
|
||||||
for field in LEGACY_FIELDS[model]:
|
for field in LEGACY_FIELDS[model]:
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import typing as t
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import StrictStr, root_validator
|
from pydantic import model_validator, ConfigDict
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
|
|
@ -17,35 +17,33 @@ _WEBHOOK_TITLE = "hyperglass received a valid query with the following data"
|
||||||
_ICON_URL = "https://res.cloudinary.com/hyperglass/image/upload/v1593192484/icon.png"
|
_ICON_URL = "https://res.cloudinary.com/hyperglass/image/upload/v1593192484/icon.png"
|
||||||
|
|
||||||
|
|
||||||
|
def to_snake_case(value: str) -> str:
|
||||||
|
"""Convert string to snake case."""
|
||||||
|
return value.replace("_", "-")
|
||||||
|
|
||||||
|
|
||||||
class WebhookHeaders(HyperglassModel):
|
class WebhookHeaders(HyperglassModel):
|
||||||
"""Webhook data model."""
|
"""Webhook data model."""
|
||||||
|
|
||||||
user_agent: t.Optional[StrictStr]
|
model_config = ConfigDict(alias_generator=to_snake_case)
|
||||||
referer: t.Optional[StrictStr]
|
|
||||||
accept_encoding: t.Optional[StrictStr]
|
|
||||||
accept_language: t.Optional[StrictStr]
|
|
||||||
x_real_ip: t.Optional[StrictStr]
|
|
||||||
x_forwarded_for: t.Optional[StrictStr]
|
|
||||||
|
|
||||||
class Config:
|
user_agent: t.Optional[str] = None
|
||||||
"""Pydantic model config."""
|
referer: t.Optional[str] = None
|
||||||
|
accept_encoding: t.Optional[str] = None
|
||||||
fields = {
|
accept_language: t.Optional[str] = None
|
||||||
"user_agent": "user-agent",
|
x_real_ip: t.Optional[str] = None
|
||||||
"accept_encoding": "accept-encoding",
|
x_forwarded_for: t.Optional[str] = None
|
||||||
"accept_language": "accept-language",
|
|
||||||
"x_real_ip": "x-real-ip",
|
|
||||||
"x_forwarded_for": "x-forwarded-for",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class WebhookNetwork(HyperglassModel, extra="allow"):
|
class WebhookNetwork(HyperglassModel):
|
||||||
"""Webhook data model."""
|
"""Webhook data model."""
|
||||||
|
|
||||||
prefix: StrictStr = "Unknown"
|
model_config = ConfigDict(extra="allow")
|
||||||
asn: StrictStr = "Unknown"
|
|
||||||
org: StrictStr = "Unknown"
|
prefix: str = "Unknown"
|
||||||
country: StrictStr = "Unknown"
|
asn: str = "Unknown"
|
||||||
|
org: str = "Unknown"
|
||||||
|
country: str = "Unknown"
|
||||||
|
|
||||||
|
|
||||||
class Webhook(HyperglassModel):
|
class Webhook(HyperglassModel):
|
||||||
|
|
@ -55,26 +53,26 @@ class Webhook(HyperglassModel):
|
||||||
query_type: str
|
query_type: str
|
||||||
query_target: t.Union[t.List[str], str]
|
query_target: t.Union[t.List[str], str]
|
||||||
headers: WebhookHeaders
|
headers: WebhookHeaders
|
||||||
source: StrictStr = "Unknown"
|
source: str = "Unknown"
|
||||||
network: WebhookNetwork
|
network: WebhookNetwork
|
||||||
timestamp: datetime
|
timestamp: datetime
|
||||||
|
|
||||||
@root_validator(pre=True)
|
@model_validator(mode="before")
|
||||||
def validate_webhook(cls, values):
|
def validate_webhook(cls, model: "Webhook") -> "Webhook":
|
||||||
"""Reset network attributes if the source is localhost."""
|
"""Reset network attributes if the source is localhost."""
|
||||||
if values.get("source") in ("127.0.0.1", "::1"):
|
if model.source in ("127.0.0.1", "::1"):
|
||||||
values["network"] = {}
|
model.network = {}
|
||||||
return values
|
return model
|
||||||
|
|
||||||
def msteams(self):
|
def msteams(self) -> t.Dict[str, t.Any]:
|
||||||
"""Format the webhook data as a Microsoft Teams card."""
|
"""Format the webhook data as a Microsoft Teams card."""
|
||||||
|
|
||||||
def code(value: t.Any):
|
def code(value: t.Any) -> str:
|
||||||
"""Wrap argument in backticks for markdown inline code formatting."""
|
"""Wrap argument in backticks for markdown inline code formatting."""
|
||||||
return f"`{str(value)}`"
|
return f"`{str(value)}`"
|
||||||
|
|
||||||
header_data = [
|
header_data = [
|
||||||
{"name": k, "value": code(v)} for k, v in self.headers.dict(by_alias=True).items()
|
{"name": k, "value": code(v)} for k, v in self.headers.model_dump(by_alias=True).items()
|
||||||
]
|
]
|
||||||
time_fmt = self.timestamp.strftime("%Y %m %d %H:%M:%S")
|
time_fmt = self.timestamp.strftime("%Y %m %d %H:%M:%S")
|
||||||
payload = {
|
payload = {
|
||||||
|
|
@ -114,7 +112,7 @@ class Webhook(HyperglassModel):
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
def slack(self):
|
def slack(self) -> t.Dict[str, t.Any]:
|
||||||
"""Format the webhook data as a Slack message."""
|
"""Format the webhook data as a Slack message."""
|
||||||
|
|
||||||
def make_field(key, value, code=False):
|
def make_field(key, value, code=False):
|
||||||
|
|
@ -123,7 +121,7 @@ class Webhook(HyperglassModel):
|
||||||
return f"*{key}*\n{value}"
|
return f"*{key}*\n{value}"
|
||||||
|
|
||||||
header_data = []
|
header_data = []
|
||||||
for k, v in self.headers.dict(by_alias=True).items():
|
for k, v in self.headers.model_dump(by_alias=True).items():
|
||||||
field = make_field(k, v, code=True)
|
field = make_field(k, v, code=True)
|
||||||
header_data.append(field)
|
header_data.append(field)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ SupportedMethod = t.TypeVar("SupportedMethod")
|
||||||
class HyperglassPlugin(BaseModel, ABC):
|
class HyperglassPlugin(BaseModel, ABC):
|
||||||
"""Plugin to interact with device command output."""
|
"""Plugin to interact with device command output."""
|
||||||
|
|
||||||
__hyperglass_builtin__: bool = PrivateAttr(False)
|
_hyperglass_builtin: bool = PrivateAttr(False)
|
||||||
_type: t.ClassVar[str]
|
_type: t.ClassVar[str]
|
||||||
name: str
|
name: str
|
||||||
common: bool = False
|
common: bool = False
|
||||||
|
|
@ -76,7 +76,7 @@ class HyperglassPlugin(BaseModel, ABC):
|
||||||
table = Table.grid(padding=(0, 1), expand=False)
|
table = Table.grid(padding=(0, 1), expand=False)
|
||||||
table.add_column(justify="right")
|
table.add_column(justify="right")
|
||||||
|
|
||||||
data = {"builtin": True if self.__hyperglass_builtin__ else False}
|
data = {"builtin": True if self._hyperglass_builtin else False}
|
||||||
data.update(
|
data.update(
|
||||||
{
|
{
|
||||||
attr: getattr(self, attr)
|
attr: getattr(self, attr)
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ def validate_large_community(value: str) -> bool:
|
||||||
class ValidateBGPCommunity(InputPlugin):
|
class ValidateBGPCommunity(InputPlugin):
|
||||||
"""Validate a BGP community string."""
|
"""Validate a BGP community string."""
|
||||||
|
|
||||||
__hyperglass_builtin__: bool = PrivateAttr(True)
|
_hyperglass_builtin: bool = PrivateAttr(True)
|
||||||
|
|
||||||
def validate(self, query: "Query") -> "InputPluginValidationReturn":
|
def validate(self, query: "Query") -> "InputPluginValidationReturn":
|
||||||
"""Ensure an input query target is a valid BGP community."""
|
"""Ensure an input query target is a valid BGP community."""
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ def parse_arista(output: t.Sequence[str]) -> "OutputDataModel":
|
||||||
result = None
|
result = None
|
||||||
|
|
||||||
for response in output:
|
for response in output:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
parsed: t.Dict = json.loads(response)
|
parsed: t.Dict = json.loads(response)
|
||||||
|
|
||||||
|
|
@ -68,7 +67,7 @@ def parse_arista(output: t.Sequence[str]) -> "OutputDataModel":
|
||||||
class BGPRoutePluginArista(OutputPlugin):
|
class BGPRoutePluginArista(OutputPlugin):
|
||||||
"""Coerce a Arista route table in JSON format to a standard BGP Table structure."""
|
"""Coerce a Arista route table in JSON format to a standard BGP Table structure."""
|
||||||
|
|
||||||
__hyperglass_builtin__: bool = PrivateAttr(True)
|
_hyperglass_builtin: bool = PrivateAttr(True)
|
||||||
platforms: t.Sequence[str] = ("arista_eos",)
|
platforms: t.Sequence[str] = ("arista_eos",)
|
||||||
directives: t.Sequence[str] = (
|
directives: t.Sequence[str] = (
|
||||||
"__hyperglass_arista_eos_bgp_route_table__",
|
"__hyperglass_arista_eos_bgp_route_table__",
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ def parse_juniper(output: Sequence[str]) -> "OutputDataModel": # noqa: C901
|
||||||
class BGPRoutePluginJuniper(OutputPlugin):
|
class BGPRoutePluginJuniper(OutputPlugin):
|
||||||
"""Coerce a Juniper route table in XML format to a standard BGP Table structure."""
|
"""Coerce a Juniper route table in XML format to a standard BGP Table structure."""
|
||||||
|
|
||||||
__hyperglass_builtin__: bool = PrivateAttr(True)
|
_hyperglass_builtin: bool = PrivateAttr(True)
|
||||||
platforms: Sequence[str] = ("juniper",)
|
platforms: Sequence[str] = ("juniper",)
|
||||||
directives: Sequence[str] = (
|
directives: Sequence[str] = (
|
||||||
"__hyperglass_juniper_bgp_route_table__",
|
"__hyperglass_juniper_bgp_route_table__",
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ if t.TYPE_CHECKING:
|
||||||
class MikrotikGarbageOutput(OutputPlugin):
|
class MikrotikGarbageOutput(OutputPlugin):
|
||||||
"""Parse Mikrotik output to remove garbage."""
|
"""Parse Mikrotik output to remove garbage."""
|
||||||
|
|
||||||
__hyperglass_builtin__: bool = PrivateAttr(True)
|
_hyperglass_builtin: bool = PrivateAttr(True)
|
||||||
platforms: t.Sequence[str] = ("mikrotik_routeros", "mikrotik_switchos")
|
platforms: t.Sequence[str] = ("mikrotik_routeros", "mikrotik_switchos")
|
||||||
directives: t.Sequence[str] = (
|
directives: t.Sequence[str] = (
|
||||||
"__hyperglass_mikrotik_bgp_aspath__",
|
"__hyperglass_mikrotik_bgp_aspath__",
|
||||||
|
|
@ -37,7 +37,6 @@ class MikrotikGarbageOutput(OutputPlugin):
|
||||||
result = ()
|
result = ()
|
||||||
|
|
||||||
for each_output in output:
|
for each_output in output:
|
||||||
|
|
||||||
if each_output.split()[-1] in ("DISTANCE", "STATUS"):
|
if each_output.split()[-1] in ("DISTANCE", "STATUS"):
|
||||||
# Mikrotik shows the columns with no rows if there is no data.
|
# Mikrotik shows the columns with no rows if there is no data.
|
||||||
# Rather than send back an empty table, send back an empty
|
# Rather than send back an empty table, send back an empty
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ if TYPE_CHECKING:
|
||||||
class RemoveCommand(OutputPlugin):
|
class RemoveCommand(OutputPlugin):
|
||||||
"""Remove anything before the command if found in output."""
|
"""Remove anything before the command if found in output."""
|
||||||
|
|
||||||
__hyperglass_builtin__: bool = PrivateAttr(True)
|
_hyperglass_builtin: bool = PrivateAttr(True)
|
||||||
|
|
||||||
def process(self, *, output: OutputType, query: "Query") -> Sequence[str]:
|
def process(self, *, output: OutputType, query: "Query") -> Sequence[str]:
|
||||||
"""Remove anything before the command if found in output."""
|
"""Remove anything before the command if found in output."""
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ class PluginManager(t.Generic[PluginT]):
|
||||||
plugins = self._state.plugins(self._type)
|
plugins = self._state.plugins(self._type)
|
||||||
|
|
||||||
if builtins is False:
|
if builtins is False:
|
||||||
plugins = [p for p in plugins if p.__hyperglass_builtin__ is False]
|
plugins = [p for p in plugins if p._hyperglass_builtin is False]
|
||||||
|
|
||||||
# Sort plugins by their name attribute, which is the name of the class by default.
|
# Sort plugins by their name attribute, which is the name of the class by default.
|
||||||
sorted_by_name = sorted(plugins, key=lambda p: str(p))
|
sorted_by_name = sorted(plugins, key=lambda p: str(p))
|
||||||
|
|
@ -69,7 +69,7 @@ class PluginManager(t.Generic[PluginT]):
|
||||||
# Sort with built-in plugins last.
|
# Sort with built-in plugins last.
|
||||||
return sorted(
|
return sorted(
|
||||||
sorted_by_name,
|
sorted_by_name,
|
||||||
key=lambda p: -1 if p.__hyperglass_builtin__ else 1,
|
key=lambda p: -1 if p._hyperglass_builtin else 1,
|
||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -111,7 +111,7 @@ class PluginManager(t.Generic[PluginT]):
|
||||||
if issubclass(plugin, HyperglassPlugin):
|
if issubclass(plugin, HyperglassPlugin):
|
||||||
instance = plugin(*args, **kwargs)
|
instance = plugin(*args, **kwargs)
|
||||||
self._state.add_plugin(self._type, instance)
|
self._state.add_plugin(self._type, instance)
|
||||||
if instance.__hyperglass_builtin__ is True:
|
if instance._hyperglass_builtin is True:
|
||||||
log.debug("Registered {} built-in plugin {!r}", self._type, instance.name)
|
log.debug("Registered {} built-in plugin {!r}", self._type, instance.name)
|
||||||
else:
|
else:
|
||||||
log.success("Registered {} plugin {!r}", self._type, instance.name)
|
log.success("Registered {} plugin {!r}", self._type, instance.name)
|
||||||
|
|
@ -182,7 +182,6 @@ class OutputPluginManager(PluginManager[OutputPlugin], type="output"):
|
||||||
)
|
)
|
||||||
common = (plugin for plugin in self.plugins() if plugin.common is True)
|
common = (plugin for plugin in self.plugins() if plugin.common is True)
|
||||||
for plugin in (*directives, *common):
|
for plugin in (*directives, *common):
|
||||||
|
|
||||||
log.debug("Output Plugin {!r} starting with\n{!r}", plugin.name, result)
|
log.debug("Output Plugin {!r} starting with\n{!r}", plugin.name, result)
|
||||||
result = plugin.process(output=result, query=query)
|
result = plugin.process(output=result, query=query)
|
||||||
log.debug("Output Plugin {!r} completed with\n{!r}", plugin.name, result)
|
log.debug("Output Plugin {!r} completed with\n{!r}", plugin.name, result)
|
||||||
|
|
|
||||||
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
|
# Local
|
||||||
from .._builtin.bgp_route_arista import BGPRoutePluginArista
|
from .._builtin.bgp_route_arista import BGPRoutePluginArista
|
||||||
|
from ._fixtures import MockDevice
|
||||||
|
|
||||||
DEPENDS_KWARGS = {
|
DEPENDS_KWARGS = {
|
||||||
"depends": [
|
"depends": [
|
||||||
|
|
@ -28,20 +29,17 @@ SAMPLE = Path(__file__).parent.parent.parent.parent / ".samples" / "arista_route
|
||||||
def _tester(sample: str):
|
def _tester(sample: str):
|
||||||
plugin = BGPRoutePluginArista()
|
plugin = BGPRoutePluginArista()
|
||||||
|
|
||||||
device = Device(
|
device = MockDevice(
|
||||||
name="Test Device",
|
name="Test Device",
|
||||||
address="127.0.0.1",
|
address="127.0.0.1",
|
||||||
group="Test Network",
|
group="Test Network",
|
||||||
credential={"username": "", "password": ""},
|
credential={"username": "", "password": ""},
|
||||||
platform="arista",
|
platform="arista",
|
||||||
structured_output=True,
|
structured_output=True,
|
||||||
directives=[],
|
directives=["__hyperglass_arista_eos_bgp_route_table__"],
|
||||||
attrs={"source4": "192.0.2.1", "source6": "2001:db8::1"},
|
attrs={"source4": "192.0.2.1", "source6": "2001:db8::1"},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Override has_directives method for testing.
|
|
||||||
device.has_directives = lambda *x: True
|
|
||||||
|
|
||||||
query = type("Query", (), {"device": device})
|
query = type("Query", (), {"device": device})
|
||||||
|
|
||||||
result = plugin.process(output=(sample,), query=query)
|
result = plugin.process(output=(sample,), query=query)
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,11 @@ import pytest
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
from hyperglass.models.api.query import Query
|
from hyperglass.models.api.query import Query
|
||||||
from hyperglass.models.config.devices import Device
|
|
||||||
from hyperglass.models.data.bgp_route import BGPRouteTable
|
from hyperglass.models.data.bgp_route import BGPRouteTable
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from .._builtin.bgp_route_juniper import BGPRoutePluginJuniper
|
from .._builtin.bgp_route_juniper import BGPRoutePluginJuniper
|
||||||
|
from ._fixtures import MockDevice
|
||||||
|
|
||||||
DEPENDS_KWARGS = {
|
DEPENDS_KWARGS = {
|
||||||
"depends": [
|
"depends": [
|
||||||
|
|
@ -32,7 +32,7 @@ AS_PATH = Path(__file__).parent.parent.parent.parent / ".samples" / "juniper_rou
|
||||||
def _tester(sample: str):
|
def _tester(sample: str):
|
||||||
plugin = BGPRoutePluginJuniper()
|
plugin = BGPRoutePluginJuniper()
|
||||||
|
|
||||||
device = Device(
|
device = MockDevice(
|
||||||
name="Test Device",
|
name="Test Device",
|
||||||
address="127.0.0.1",
|
address="127.0.0.1",
|
||||||
group="Test Network",
|
group="Test Network",
|
||||||
|
|
@ -43,9 +43,6 @@ def _tester(sample: str):
|
||||||
attrs={"source4": "192.0.2.1", "source6": "2001:db8::1"},
|
attrs={"source4": "192.0.2.1", "source6": "2001:db8::1"},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Override has_directives method for testing.
|
|
||||||
device.has_directives = lambda *x: True
|
|
||||||
|
|
||||||
query = type("Query", (), {"device": device})
|
query = type("Query", (), {"device": device})
|
||||||
|
|
||||||
result = plugin.process(output=(sample,), query=query)
|
result = plugin.process(output=(sample,), query=query)
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,6 @@ from .system_info import cpu_count, check_python, get_system_info, get_node_vers
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"at_least",
|
"at_least",
|
||||||
# "build_frontend",
|
|
||||||
# "build_ui",
|
|
||||||
"check_path",
|
"check_path",
|
||||||
"check_python",
|
"check_python",
|
||||||
"compare_dicts",
|
"compare_dicts",
|
||||||
|
|
|
||||||
|
|
@ -11,16 +11,16 @@ dependencies = [
|
||||||
"PyYAML>=6.0",
|
"PyYAML>=6.0",
|
||||||
"aiofiles>=23.2.1",
|
"aiofiles>=23.2.1",
|
||||||
"distro==1.8.0",
|
"distro==1.8.0",
|
||||||
"fastapi==0.95.1",
|
"fastapi>=0.110.0",
|
||||||
"favicons==0.2.2",
|
"favicons==0.2.2",
|
||||||
"gunicorn==20.1.0",
|
"gunicorn>=21.2.0",
|
||||||
"httpx==0.24.0",
|
"httpx==0.24.0",
|
||||||
"loguru==0.7.0",
|
"loguru==0.7.0",
|
||||||
"netmiko==4.1.2",
|
"netmiko==4.1.2",
|
||||||
"paramiko==3.4.0",
|
"paramiko==3.4.0",
|
||||||
"psutil==5.9.4",
|
"psutil==5.9.4",
|
||||||
"py-cpuinfo==9.0.0",
|
"py-cpuinfo==9.0.0",
|
||||||
"pydantic==1.10.14",
|
"pydantic>=2.6.3",
|
||||||
"redis==4.5.4",
|
"redis==4.5.4",
|
||||||
"rich>=13.7.0",
|
"rich>=13.7.0",
|
||||||
"typer>=0.9.0",
|
"typer>=0.9.0",
|
||||||
|
|
@ -28,6 +28,8 @@ dependencies = [
|
||||||
"uvloop>=0.17.0",
|
"uvloop>=0.17.0",
|
||||||
"xmltodict==0.13.0",
|
"xmltodict==0.13.0",
|
||||||
"toml>=0.10.2",
|
"toml>=0.10.2",
|
||||||
|
"pydantic-settings>=2.2.1",
|
||||||
|
"pydantic-extra-types>=2.6.0",
|
||||||
]
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">= 3.11"
|
requires-python = ">= 3.11"
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@
|
||||||
-e file:.
|
-e file:.
|
||||||
aiofiles==23.2.1
|
aiofiles==23.2.1
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
|
annotated-types==0.6.0
|
||||||
|
# via pydantic
|
||||||
anyio==4.3.0
|
anyio==4.3.0
|
||||||
# via httpcore
|
# via httpcore
|
||||||
# via starlette
|
# via starlette
|
||||||
|
|
@ -41,7 +43,7 @@ distlib==0.3.8
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
distro==1.8.0
|
distro==1.8.0
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
fastapi==0.95.1
|
fastapi==0.110.0
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
favicons==0.2.2
|
favicons==0.2.2
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
|
|
@ -53,7 +55,7 @@ freetype-py==2.4.0
|
||||||
# via rlpycairo
|
# via rlpycairo
|
||||||
future==0.18.3
|
future==0.18.3
|
||||||
# via textfsm
|
# via textfsm
|
||||||
gunicorn==20.1.0
|
gunicorn==21.2.0
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
h11==0.14.0
|
h11==0.14.0
|
||||||
# via httpcore
|
# via httpcore
|
||||||
|
|
@ -90,6 +92,7 @@ ntc-templates==4.3.0
|
||||||
# via netmiko
|
# via netmiko
|
||||||
packaging==23.2
|
packaging==23.2
|
||||||
# via black
|
# via black
|
||||||
|
# via gunicorn
|
||||||
# via pytest
|
# via pytest
|
||||||
paramiko==3.4.0
|
paramiko==3.4.0
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
|
|
@ -121,9 +124,17 @@ pycodestyle==2.11.1
|
||||||
# via flake8
|
# via flake8
|
||||||
pycparser==2.21
|
pycparser==2.21
|
||||||
# via cffi
|
# via cffi
|
||||||
pydantic==1.10.14
|
pydantic==2.6.3
|
||||||
# via fastapi
|
# via fastapi
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
|
# via pydantic-extra-types
|
||||||
|
# via pydantic-settings
|
||||||
|
pydantic-core==2.16.3
|
||||||
|
# via pydantic
|
||||||
|
pydantic-extra-types==2.6.0
|
||||||
|
# via hyperglass
|
||||||
|
pydantic-settings==2.2.1
|
||||||
|
# via hyperglass
|
||||||
pyflakes==3.2.0
|
pyflakes==3.2.0
|
||||||
# via flake8
|
# via flake8
|
||||||
pygments==2.17.2
|
pygments==2.17.2
|
||||||
|
|
@ -139,6 +150,8 @@ pytest==8.0.1
|
||||||
# via pytest-dependency
|
# via pytest-dependency
|
||||||
pytest-asyncio==0.23.5
|
pytest-asyncio==0.23.5
|
||||||
pytest-dependency==0.6.0
|
pytest-dependency==0.6.0
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
# via pydantic-settings
|
||||||
pyyaml==6.0.1
|
pyyaml==6.0.1
|
||||||
# via bandit
|
# via bandit
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
|
|
@ -159,7 +172,6 @@ ruff==0.2.2
|
||||||
scp==0.14.5
|
scp==0.14.5
|
||||||
# via netmiko
|
# via netmiko
|
||||||
setuptools==69.1.0
|
setuptools==69.1.0
|
||||||
# via gunicorn
|
|
||||||
# via netmiko
|
# via netmiko
|
||||||
# via nodeenv
|
# via nodeenv
|
||||||
# via pytest-dependency
|
# via pytest-dependency
|
||||||
|
|
@ -170,7 +182,7 @@ sniffio==1.3.0
|
||||||
# via httpcore
|
# via httpcore
|
||||||
# via httpx
|
# via httpx
|
||||||
stackprinter==0.2.11
|
stackprinter==0.2.11
|
||||||
starlette==0.26.1
|
starlette==0.36.3
|
||||||
# via fastapi
|
# via fastapi
|
||||||
stevedore==5.1.0
|
stevedore==5.1.0
|
||||||
# via bandit
|
# via bandit
|
||||||
|
|
@ -193,7 +205,9 @@ typer==0.9.0
|
||||||
# via favicons
|
# via favicons
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
typing-extensions==4.9.0
|
typing-extensions==4.9.0
|
||||||
|
# via fastapi
|
||||||
# via pydantic
|
# via pydantic
|
||||||
|
# via pydantic-core
|
||||||
# via typer
|
# via typer
|
||||||
uvicorn==0.21.1
|
uvicorn==0.21.1
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@
|
||||||
-e file:.
|
-e file:.
|
||||||
aiofiles==23.2.1
|
aiofiles==23.2.1
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
|
annotated-types==0.6.0
|
||||||
|
# via pydantic
|
||||||
anyio==4.3.0
|
anyio==4.3.0
|
||||||
# via httpcore
|
# via httpcore
|
||||||
# via starlette
|
# via starlette
|
||||||
|
|
@ -32,7 +34,7 @@ cssselect2==0.7.0
|
||||||
# via svglib
|
# via svglib
|
||||||
distro==1.8.0
|
distro==1.8.0
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
fastapi==0.95.1
|
fastapi==0.110.0
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
favicons==0.2.2
|
favicons==0.2.2
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
|
|
@ -40,7 +42,7 @@ freetype-py==2.4.0
|
||||||
# via rlpycairo
|
# via rlpycairo
|
||||||
future==0.18.3
|
future==0.18.3
|
||||||
# via textfsm
|
# via textfsm
|
||||||
gunicorn==20.1.0
|
gunicorn==21.2.0
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
h11==0.14.0
|
h11==0.14.0
|
||||||
# via httpcore
|
# via httpcore
|
||||||
|
|
@ -64,6 +66,8 @@ netmiko==4.1.2
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
ntc-templates==4.3.0
|
ntc-templates==4.3.0
|
||||||
# via netmiko
|
# via netmiko
|
||||||
|
packaging==24.0
|
||||||
|
# via gunicorn
|
||||||
paramiko==3.4.0
|
paramiko==3.4.0
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
# via netmiko
|
# via netmiko
|
||||||
|
|
@ -80,9 +84,17 @@ pycairo==1.26.0
|
||||||
# via rlpycairo
|
# via rlpycairo
|
||||||
pycparser==2.21
|
pycparser==2.21
|
||||||
# via cffi
|
# via cffi
|
||||||
pydantic==1.10.14
|
pydantic==2.6.3
|
||||||
# via fastapi
|
# via fastapi
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
|
# via pydantic-extra-types
|
||||||
|
# via pydantic-settings
|
||||||
|
pydantic-core==2.16.3
|
||||||
|
# via pydantic
|
||||||
|
pydantic-extra-types==2.6.0
|
||||||
|
# via hyperglass
|
||||||
|
pydantic-settings==2.2.1
|
||||||
|
# via hyperglass
|
||||||
pygments==2.17.2
|
pygments==2.17.2
|
||||||
# via rich
|
# via rich
|
||||||
pyjwt==2.6.0
|
pyjwt==2.6.0
|
||||||
|
|
@ -91,6 +103,8 @@ pynacl==1.5.0
|
||||||
# via paramiko
|
# via paramiko
|
||||||
pyserial==3.5
|
pyserial==3.5
|
||||||
# via netmiko
|
# via netmiko
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
# via pydantic-settings
|
||||||
pyyaml==6.0.1
|
pyyaml==6.0.1
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
# via netmiko
|
# via netmiko
|
||||||
|
|
@ -107,7 +121,6 @@ rlpycairo==0.3.0
|
||||||
scp==0.14.5
|
scp==0.14.5
|
||||||
# via netmiko
|
# via netmiko
|
||||||
setuptools==69.1.0
|
setuptools==69.1.0
|
||||||
# via gunicorn
|
|
||||||
# via netmiko
|
# via netmiko
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
# via textfsm
|
# via textfsm
|
||||||
|
|
@ -115,7 +128,7 @@ sniffio==1.3.0
|
||||||
# via anyio
|
# via anyio
|
||||||
# via httpcore
|
# via httpcore
|
||||||
# via httpx
|
# via httpx
|
||||||
starlette==0.26.1
|
starlette==0.36.3
|
||||||
# via fastapi
|
# via fastapi
|
||||||
svglib==1.5.1
|
svglib==1.5.1
|
||||||
# via favicons
|
# via favicons
|
||||||
|
|
@ -133,7 +146,9 @@ typer==0.9.0
|
||||||
# via favicons
|
# via favicons
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
typing-extensions==4.9.0
|
typing-extensions==4.9.0
|
||||||
|
# via fastapi
|
||||||
# via pydantic
|
# via pydantic
|
||||||
|
# via pydantic-core
|
||||||
# via typer
|
# via typer
|
||||||
uvicorn==0.21.1
|
uvicorn==0.21.1
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue