mirror of
https://github.com/checktheroads/hyperglass
synced 2024-05-11 05:55:08 +00:00
add structured output support
This commit is contained in:
@@ -121,6 +121,10 @@ class Query(BaseModel):
|
||||
)
|
||||
return f'Query({", ".join(items)})'
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
return getattr(devices, self.query_location)
|
||||
|
||||
def export_dict(self):
|
||||
"""Create dictionary representation of instance."""
|
||||
return {
|
||||
|
@@ -1,12 +1,14 @@
|
||||
"""Response model."""
|
||||
|
||||
# Standard Library
|
||||
from typing import List, Optional
|
||||
from typing import List, Union, Optional
|
||||
|
||||
# Third Party
|
||||
from pydantic import BaseModel, StrictInt, StrictStr, StrictBool, constr
|
||||
|
||||
# Project
|
||||
from hyperglass.configuration import params
|
||||
from hyperglass.parsing.models.serialized import ParsedRoutes
|
||||
|
||||
|
||||
class QueryError(BaseModel):
|
||||
@@ -55,14 +57,14 @@ class QueryError(BaseModel):
|
||||
class QueryResponse(BaseModel):
|
||||
"""Query response model."""
|
||||
|
||||
output: StrictStr
|
||||
output: Union[ParsedRoutes, StrictStr]
|
||||
level: constr(regex=r"success") = "success"
|
||||
random: StrictStr
|
||||
cached: StrictBool
|
||||
runtime: StrictInt
|
||||
keywords: List[StrictStr] = []
|
||||
timestamp: StrictStr
|
||||
_format: constr(regex=r"(application\/json|text\/plain)") = "text/plain"
|
||||
format: constr(regex=r"(application\/json|text\/plain)") = "text/plain"
|
||||
|
||||
class Config:
|
||||
"""Pydantic model configuration."""
|
||||
@@ -91,7 +93,7 @@ class QueryResponse(BaseModel):
|
||||
"example": "2020-04-18 14:45:37",
|
||||
},
|
||||
"format": {
|
||||
"alias": "format",
|
||||
# "alias": "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",
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
# Standard Library
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
|
||||
# Third Party
|
||||
@@ -53,8 +54,8 @@ async def query(query_data: Query, request: Request):
|
||||
|
||||
# Define cache entry expiry time
|
||||
cache_timeout = params.cache.timeout
|
||||
log.debug(f"Cache Timeout: {cache_timeout}")
|
||||
|
||||
log.debug(f"Cache Timeout: {cache_timeout}")
|
||||
log.info(f"Starting query execution for query {query_data.summary}")
|
||||
|
||||
cache_response = await cache.get_dict(cache_key, "output")
|
||||
@@ -88,7 +89,11 @@ async def query(query_data: Query, request: Request):
|
||||
raise HyperglassError(message=params.messages.general, alert="danger")
|
||||
|
||||
# Create a cache entry
|
||||
await cache.set_dict(cache_key, "output", str(cache_output))
|
||||
if query_data.device.structured_output:
|
||||
raw_output = json.dumps(cache_output)
|
||||
else:
|
||||
raw_output = str(cache_output)
|
||||
await cache.set_dict(cache_key, "output", raw_output)
|
||||
await cache.set_dict(cache_key, "timestamp", timestamp)
|
||||
await cache.expire(cache_key, seconds=cache_timeout)
|
||||
|
||||
@@ -99,6 +104,12 @@ async def query(query_data: Query, request: Request):
|
||||
# If it does, return the cached entry
|
||||
cache_response = await cache.get_dict(cache_key, "output")
|
||||
|
||||
if query_data.device.structured_output:
|
||||
response_format = "application/json"
|
||||
cache_response = json.loads(cache_response)
|
||||
else:
|
||||
response_format = "text/plain"
|
||||
|
||||
log.debug(f"Cache match for {cache_key}:\n {cache_response}")
|
||||
log.success(f"Completed query execution for {query_data.summary}")
|
||||
|
||||
@@ -108,6 +119,7 @@ async def query(query_data: Query, request: Request):
|
||||
"cached": cached,
|
||||
"runtime": runtime,
|
||||
"timestamp": timestamp,
|
||||
"format": response_format,
|
||||
"random": query_data.random(),
|
||||
"level": "success",
|
||||
"keywords": [],
|
||||
|
@@ -1,42 +1,56 @@
|
||||
"""Arista Command Model."""
|
||||
|
||||
# Third Party
|
||||
from pydantic import StrictStr
|
||||
|
||||
# Project
|
||||
from hyperglass.configuration.models.commands.common import CommandSet, CommandGroup
|
||||
|
||||
_ipv4_default = {
|
||||
"bgp_route": "show ip bgp {target}",
|
||||
"bgp_aspath": "show ip bgp regexp {target}",
|
||||
"bgp_community": "show ip bgp community {target}",
|
||||
"ping": "ping ip {target} source {source}",
|
||||
"traceroute": "traceroute ip {target} source {source}",
|
||||
}
|
||||
_ipv6_default = {
|
||||
"bgp_route": "show ipv6 bgp {target}",
|
||||
"bgp_aspath": "show ipv6 bgp regexp {target}",
|
||||
"bgp_community": "show ipv6 bgp community {target}",
|
||||
"ping": "ping ipv6 {target} source {source}",
|
||||
"traceroute": "traceroute ipv6 {target} source {source}",
|
||||
}
|
||||
_ipv4_vpn = {
|
||||
"bgp_route": "show ip bgp {target} vrf {vrf}",
|
||||
"bgp_aspath": "show ip bgp regexp {target} vrf {vrf}",
|
||||
"bgp_community": "show ip bgp community {target} vrf {vrf}",
|
||||
"ping": "ping vrf {vrf} ip {target} source {source}",
|
||||
"traceroute": "traceroute vrf {vrf} ip {target} source {source}",
|
||||
}
|
||||
_ipv6_vpn = {
|
||||
"bgp_route": "show ipv6 bgp {target} vrf {vrf}",
|
||||
"bgp_aspath": "show ipv6 bgp regexp {target} vrf {vrf}",
|
||||
"bgp_community": "show ipv6 bgp community {target} vrf {vrf}",
|
||||
"ping": "ping vrf {vrf} ipv6 {target} source {source}",
|
||||
"traceroute": "traceroute vrf {vrf} ipv6 {target} source {source}",
|
||||
}
|
||||
|
||||
class _IPv4(CommandSet):
|
||||
"""Validation model for non-default dual afi commands."""
|
||||
|
||||
bgp_route: StrictStr = "show ip bgp {target}"
|
||||
bgp_aspath: StrictStr = "show ip bgp regexp {target}"
|
||||
bgp_community: StrictStr = "show ip bgp community {target}"
|
||||
ping: StrictStr = "ping ip {target} source {source}"
|
||||
traceroute: StrictStr = "traceroute ip {target} source {source}"
|
||||
|
||||
|
||||
class _IPv6(CommandSet):
|
||||
"""Validation model for non-default ipv4 commands."""
|
||||
|
||||
bgp_route: StrictStr = "show ipv6 bgp {target}"
|
||||
bgp_aspath: StrictStr = "show ipv6 bgp regexp {target}"
|
||||
bgp_community: StrictStr = "show ipv6 bgp community {target}"
|
||||
ping: StrictStr = "ping ipv6 {target} source {source}"
|
||||
traceroute: StrictStr = "traceroute ipv6 {target} source {source}"
|
||||
|
||||
|
||||
class _VPNIPv4(CommandSet):
|
||||
"""Validation model for non-default ipv6 commands."""
|
||||
|
||||
bgp_route: StrictStr = "show ip bgp {target} vrf {vrf}"
|
||||
bgp_aspath: StrictStr = "show ip bgp regexp {target} vrf {vrf}"
|
||||
bgp_community: StrictStr = "show ip bgp community {target} vrf {vrf}"
|
||||
ping: StrictStr = "ping vrf {vrf} ip {target} source {source}"
|
||||
traceroute: StrictStr = "traceroute vrf {vrf} ip {target} source {source}"
|
||||
|
||||
|
||||
class _VPNIPv6(CommandSet):
|
||||
"""Validation model for non-default ipv6 commands."""
|
||||
|
||||
bgp_route: StrictStr = "show ipv6 bgp {target} vrf {vrf}"
|
||||
bgp_aspath: StrictStr = "show ipv6 bgp regexp {target} vrf {vrf}"
|
||||
bgp_community: StrictStr = "show ipv6 bgp community {target} vrf {vrf}"
|
||||
ping: StrictStr = "ping vrf {vrf} ipv6 {target} source {source}"
|
||||
traceroute: StrictStr = "traceroute vrf {vrf} ipv6 {target} source {source}"
|
||||
|
||||
|
||||
class AristaCommands(CommandGroup):
|
||||
"""Validation model for default arista commands."""
|
||||
|
||||
ipv4_default: CommandSet = CommandSet(**_ipv4_default)
|
||||
ipv6_default: CommandSet = CommandSet(**_ipv6_default)
|
||||
ipv4_vpn: CommandSet = CommandSet(**_ipv4_vpn)
|
||||
ipv6_vpn: CommandSet = CommandSet(**_ipv6_vpn)
|
||||
ipv4_default: _IPv4 = _IPv4()
|
||||
ipv6_default: _IPv6 = _IPv6()
|
||||
ipv4_vpn: _VPNIPv4 = _VPNIPv4()
|
||||
ipv6_vpn: _VPNIPv6 = _VPNIPv6()
|
||||
|
@@ -1,42 +1,62 @@
|
||||
"""Cisco IOS Command Model."""
|
||||
|
||||
# Third Party
|
||||
from pydantic import StrictStr
|
||||
|
||||
# Project
|
||||
from hyperglass.configuration.models.commands.common import CommandSet, CommandGroup
|
||||
|
||||
_ipv4_default = {
|
||||
"bgp_route": "show bgp ipv4 unicast {target} | exclude pathid:|Epoch",
|
||||
"bgp_aspath": 'show bgp ipv4 unicast quote-regexp "{target}"',
|
||||
"bgp_community": "show bgp ipv4 unicast community {target}",
|
||||
"ping": "ping {target} repeat 5 source {source}",
|
||||
"traceroute": "traceroute {target} timeout 1 probe 2 source {source}",
|
||||
}
|
||||
_ipv6_default = {
|
||||
"bgp_route": "show bgp ipv6 unicast {target} | exclude pathid:|Epoch",
|
||||
"bgp_aspath": 'show bgp ipv6 unicast quote-regexp "{target}"',
|
||||
"bgp_community": "show bgp ipv6 unicast community {target}",
|
||||
"ping": "ping ipv6 {target} repeat 5 source {source}",
|
||||
"traceroute": "traceroute ipv6 {target} timeout 1 probe 2 source {source}",
|
||||
}
|
||||
_ipv4_vpn = {
|
||||
"bgp_route": "show bgp vpnv4 unicast vrf {vrf} {target}",
|
||||
"bgp_aspath": 'show bgp vpnv4 unicast vrf {vrf} quote-regexp "{target}"',
|
||||
"bgp_community": "show bgp vpnv4 unicast vrf {vrf} community {target}",
|
||||
"ping": "ping vrf {vrf} {target} repeat 5 source {source}",
|
||||
"traceroute": "traceroute vrf {vrf} {target} timeout 1 probe 2 source {source}",
|
||||
}
|
||||
_ipv6_vpn = {
|
||||
"bgp_route": "show bgp vpnv6 unicast vrf {vrf} {target}",
|
||||
"bgp_aspath": 'show bgp vpnv6 unicast vrf {vrf} quote-regexp "{target}"',
|
||||
"bgp_community": "show bgp vpnv6 unicast vrf {vrf} community {target}",
|
||||
"ping": "ping vrf {vrf} {target} repeat 5 source {source}",
|
||||
"traceroute": "traceroute vrf {vrf} {target} timeout 1 probe 2 source {source}",
|
||||
}
|
||||
|
||||
class _IPv4(CommandSet):
|
||||
"""Default commands for ipv4 commands."""
|
||||
|
||||
bgp_community: StrictStr = "show bgp ipv4 unicast community {target}"
|
||||
bgp_aspath: StrictStr = 'show bgp ipv4 unicast quote-regexp "{target}"'
|
||||
bgp_route: StrictStr = "show bgp ipv4 unicast {target} | exclude pathid:|Epoch"
|
||||
ping: StrictStr = "ping {target} repeat 5 source {source}"
|
||||
traceroute: StrictStr = "traceroute {target} timeout 1 probe 2 source {source}"
|
||||
|
||||
|
||||
class _IPv6(CommandSet):
|
||||
"""Default commands for ipv6 commands."""
|
||||
|
||||
bgp_community: StrictStr = "show bgp ipv6 unicast community {target}"
|
||||
bgp_aspath: StrictStr = 'show bgp ipv6 unicast quote-regexp "{target}"'
|
||||
bgp_route: StrictStr = "show bgp ipv6 unicast {target} | exclude pathid:|Epoch"
|
||||
ping: StrictStr = ("ping ipv6 {target} repeat 5 source {source}")
|
||||
traceroute: StrictStr = (
|
||||
"traceroute ipv6 {target} timeout 1 probe 2 source {source}"
|
||||
)
|
||||
|
||||
|
||||
class _VPNIPv4(CommandSet):
|
||||
"""Default commands for dual afi commands."""
|
||||
|
||||
bgp_community: StrictStr = "show bgp vpnv4 unicast vrf {vrf} community {target}"
|
||||
bgp_aspath: StrictStr = 'show bgp vpnv4 unicast vrf {vrf} quote-regexp "{target}"'
|
||||
bgp_route: StrictStr = "show bgp vpnv4 unicast vrf {vrf} {target}"
|
||||
ping: StrictStr = "ping vrf {vrf} {target} repeat 5 source {source}"
|
||||
traceroute: StrictStr = (
|
||||
"traceroute vrf {vrf} {target} timeout 1 probe 2 source {source}"
|
||||
)
|
||||
|
||||
|
||||
class _VPNIPv6(CommandSet):
|
||||
"""Default commands for dual afi commands."""
|
||||
|
||||
bgp_community: StrictStr = "show bgp vpnv6 unicast vrf {vrf} community {target}"
|
||||
bgp_aspath: StrictStr = 'show bgp vpnv6 unicast vrf {vrf} quote-regexp "{target}"'
|
||||
bgp_route: StrictStr = "show bgp vpnv6 unicast vrf {vrf} {target}"
|
||||
ping: StrictStr = "ping vrf {vrf} {target} repeat 5 source {source}"
|
||||
traceroute: StrictStr = (
|
||||
"traceroute vrf {vrf} {target} timeout 1 probe 2 source {source}"
|
||||
)
|
||||
|
||||
|
||||
class CiscoIOSCommands(CommandGroup):
|
||||
"""Validation model for default cisco_ios commands."""
|
||||
|
||||
ipv4_default: CommandSet = CommandSet(**_ipv4_default)
|
||||
ipv6_default: CommandSet = CommandSet(**_ipv6_default)
|
||||
ipv4_vpn: CommandSet = CommandSet(**_ipv4_vpn)
|
||||
ipv6_vpn: CommandSet = CommandSet(**_ipv6_vpn)
|
||||
ipv4_default: _IPv4 = _IPv4()
|
||||
ipv6_default: _IPv6 = _IPv6()
|
||||
ipv4_vpn: _VPNIPv4 = _VPNIPv4()
|
||||
ipv6_vpn: _VPNIPv6 = _VPNIPv6()
|
||||
|
@@ -1,42 +1,56 @@
|
||||
"""Cisco NX-OS Command Model."""
|
||||
|
||||
# Third Party
|
||||
from pydantic import StrictStr
|
||||
|
||||
# Project
|
||||
from hyperglass.configuration.models.commands.common import CommandSet, CommandGroup
|
||||
|
||||
_ipv4_default = {
|
||||
"bgp_route": "show bgp ipv4 unicast {target}",
|
||||
"bgp_aspath": 'show bgp ipv4 unicast regexp "{target}"',
|
||||
"bgp_community": "show bgp ipv4 unicast community {target}",
|
||||
"ping": "ping {target} source {source}",
|
||||
"traceroute": "traceroute {target} source {source}",
|
||||
}
|
||||
_ipv6_default = {
|
||||
"bgp_route": "show bgp ipv6 unicast {target}",
|
||||
"bgp_aspath": 'show bgp ipv6 unicast regexp "{target}"',
|
||||
"bgp_community": "show bgp ipv6 unicast community {target}",
|
||||
"ping": "ping6 {target} source {source}",
|
||||
"traceroute": "traceroute6 {target} source {source}",
|
||||
}
|
||||
_ipv4_vpn = {
|
||||
"bgp_route": "show bgp ipv4 unicast {target} vrf {vrf}",
|
||||
"bgp_aspath": 'show bgp ipv4 unicast regexp "{target}" vrf {vrf}',
|
||||
"bgp_community": "show bgp ipv4 unicast community {target} vrf {vrf}",
|
||||
"ping": "ping {target} source {source} vrf {vrf}",
|
||||
"traceroute": "traceroute {target} source {source} vrf {vrf}",
|
||||
}
|
||||
_ipv6_vpn = {
|
||||
"bgp_route": "show bgp ipv6 unicast {target} vrf {vrf}",
|
||||
"bgp_aspath": 'show bgp ipv6 unicast regexp "{target}" vrf {vrf}',
|
||||
"bgp_community": "show bgp ipv6 unicast community {target} vrf {vrf}",
|
||||
"ping": "ping6 {target} source {source} vrf {vrf}",
|
||||
"traceroute": "traceroute6 {target} source {source} vrf {vrf}",
|
||||
}
|
||||
|
||||
class _IPv4(CommandSet):
|
||||
"""Validation model for non-default dual afi commands."""
|
||||
|
||||
bgp_route: StrictStr = "show bgp ipv4 unicast {target}"
|
||||
bgp_aspath: StrictStr = 'show bgp ipv4 unicast regexp "{target}"'
|
||||
bgp_community: StrictStr = "show bgp ipv4 unicast community {target}"
|
||||
ping: StrictStr = "ping {target} source {source}"
|
||||
traceroute: StrictStr = "traceroute {target} source {source}"
|
||||
|
||||
|
||||
class _IPv6(CommandSet):
|
||||
"""Validation model for non-default ipv4 commands."""
|
||||
|
||||
bgp_route: StrictStr = "show bgp ipv6 unicast {target}"
|
||||
bgp_aspath: StrictStr = 'show bgp ipv6 unicast regexp "{target}"'
|
||||
bgp_community: StrictStr = "show bgp ipv6 unicast community {target}"
|
||||
ping: StrictStr = "ping6 {target} source {source}"
|
||||
traceroute: StrictStr = "traceroute6 {target} source {source}"
|
||||
|
||||
|
||||
class _VPNIPv4(CommandSet):
|
||||
"""Validation model for non-default ipv6 commands."""
|
||||
|
||||
bgp_route: StrictStr = "show bgp ipv4 unicast {target} vrf {vrf}"
|
||||
bgp_aspath: StrictStr = 'show bgp ipv4 unicast regexp "{target}" vrf {vrf}'
|
||||
bgp_community: StrictStr = "show bgp ipv4 unicast community {target} vrf {vrf}"
|
||||
ping: StrictStr = "ping {target} source {source} vrf {vrf}"
|
||||
traceroute: StrictStr = "traceroute {target} source {source} vrf {vrf}"
|
||||
|
||||
|
||||
class _VPNIPv6(CommandSet):
|
||||
"""Validation model for non-default ipv6 commands."""
|
||||
|
||||
bgp_route: StrictStr = "show bgp ipv6 unicast {target} vrf {vrf}"
|
||||
bgp_aspath: StrictStr = 'show bgp ipv6 unicast regexp "{target}" vrf {vrf}'
|
||||
bgp_community: StrictStr = "show bgp ipv6 unicast community {target} vrf {vrf}"
|
||||
ping: StrictStr = "ping6 {target} source {source} vrf {vrf}"
|
||||
traceroute: StrictStr = "traceroute6 {target} source {source} vrf {vrf}"
|
||||
|
||||
|
||||
class CiscoNXOSCommands(CommandGroup):
|
||||
"""Validation model for default cisco_nxos commands."""
|
||||
|
||||
ipv4_default: CommandSet = CommandSet(**_ipv4_default)
|
||||
ipv6_default: CommandSet = CommandSet(**_ipv6_default)
|
||||
ipv4_vpn: CommandSet = CommandSet(**_ipv4_vpn)
|
||||
ipv6_vpn: CommandSet = CommandSet(**_ipv6_vpn)
|
||||
ipv4_default: _IPv4 = _IPv4()
|
||||
ipv6_default: _IPv6 = _IPv6()
|
||||
ipv4_vpn: _VPNIPv4 = _VPNIPv4()
|
||||
ipv6_vpn: _VPNIPv6 = _VPNIPv6()
|
||||
|
@@ -1,42 +1,56 @@
|
||||
"""Cisco IOS XR Command Model."""
|
||||
|
||||
# Third Party
|
||||
from pydantic import StrictStr
|
||||
|
||||
# Project
|
||||
from hyperglass.configuration.models.commands.common import CommandSet, CommandGroup
|
||||
|
||||
_ipv4_default = {
|
||||
"bgp_route": "show bgp ipv4 unicast {target}",
|
||||
"bgp_aspath": "show bgp ipv4 unicast regexp {target}",
|
||||
"bgp_community": "show bgp ipv4 unicast community {target}",
|
||||
"ping": "ping ipv4 {target} count 5 source {source}",
|
||||
"traceroute": "traceroute ipv4 {target} timeout 1 probe 2 source {source}",
|
||||
}
|
||||
_ipv6_default = {
|
||||
"bgp_route": "show bgp ipv6 unicast {target}",
|
||||
"bgp_aspath": "show bgp ipv6 unicast regexp {target}",
|
||||
"bgp_community": "show bgp ipv6 unicast community {target}",
|
||||
"ping": "ping ipv6 {target} count 5 source {source}",
|
||||
"traceroute": "traceroute ipv6 {target} timeout 1 probe 2 source {source}",
|
||||
}
|
||||
_ipv4_vpn = {
|
||||
"bgp_route": "show bgp vpnv4 unicast vrf {vrf} {target}",
|
||||
"bgp_aspath": "show bgp vpnv4 unicast vrf {vrf} regexp {target}",
|
||||
"bgp_community": "show bgp vpnv4 unicast vrf {vrf} community {target}",
|
||||
"ping": "ping vrf {vrf} {target} count 5 source {source}",
|
||||
"traceroute": "traceroute vrf {vrf} {target} timeout 1 probe 2 source {source}",
|
||||
}
|
||||
_ipv6_vpn = {
|
||||
"bgp_route": "show bgp vpnv6 unicast vrf {vrf} {target}",
|
||||
"bgp_aspath": "show bgp vpnv6 unicast vrf {vrf} regexp {target}",
|
||||
"bgp_community": "show bgp vpnv6 unicast vrf {vrf} community {target}",
|
||||
"ping": "ping vrf {vrf} {target} count 5 source {source}",
|
||||
"traceroute": "traceroute vrf {vrf} {target} timeout 1 probe 2 source {source}",
|
||||
}
|
||||
|
||||
class _IPv4(CommandSet):
|
||||
"""Validation model for non-default dual afi commands."""
|
||||
|
||||
bgp_route: StrictStr = "show bgp ipv4 unicast {target}"
|
||||
bgp_aspath: StrictStr = "show bgp ipv4 unicast regexp {target}"
|
||||
bgp_community: StrictStr = "show bgp ipv4 unicast community {target}"
|
||||
ping: StrictStr = "ping ipv4 {target} count 5 source {source}"
|
||||
traceroute: StrictStr = "traceroute ipv4 {target} timeout 1 probe 2 source {source}"
|
||||
|
||||
|
||||
class _IPv6(CommandSet):
|
||||
"""Validation model for non-default ipv4 commands."""
|
||||
|
||||
bgp_route: StrictStr = "show bgp ipv6 unicast {target}"
|
||||
bgp_aspath: StrictStr = "show bgp ipv6 unicast regexp {target}"
|
||||
bgp_community: StrictStr = "show bgp ipv6 unicast community {target}"
|
||||
ping: StrictStr = "ping ipv6 {target} count 5 source {source}"
|
||||
traceroute: StrictStr = "traceroute ipv6 {target} timeout 1 probe 2 source {source}"
|
||||
|
||||
|
||||
class _VPNIPv4(CommandSet):
|
||||
"""Validation model for non-default ipv6 commands."""
|
||||
|
||||
bgp_route: StrictStr = "show bgp vpnv4 unicast vrf {vrf} {target}"
|
||||
bgp_aspath: StrictStr = "show bgp vpnv4 unicast vrf {vrf} regexp {target}"
|
||||
bgp_community: StrictStr = "show bgp vpnv4 unicast vrf {vrf} community {target}"
|
||||
ping: StrictStr = "ping vrf {vrf} {target} count 5 source {source}"
|
||||
traceroute: StrictStr = "traceroute vrf {vrf} {target} timeout 1 probe 2 source {source}"
|
||||
|
||||
|
||||
class _VPNIPv6(CommandSet):
|
||||
"""Validation model for non-default ipv6 commands."""
|
||||
|
||||
bgp_route: StrictStr = "show bgp vpnv6 unicast vrf {vrf} {target}"
|
||||
bgp_aspath: StrictStr = "show bgp vpnv6 unicast vrf {vrf} regexp {target}"
|
||||
bgp_community: StrictStr = "show bgp vpnv6 unicast vrf {vrf} community {target}"
|
||||
ping: StrictStr = "ping vrf {vrf} {target} count 5 source {source}"
|
||||
traceroute: StrictStr = "traceroute vrf {vrf} {target} timeout 1 probe 2 source {source}"
|
||||
|
||||
|
||||
class CiscoXRCommands(CommandGroup):
|
||||
"""Validation model for default cisco_xr commands."""
|
||||
|
||||
ipv4_default: CommandSet = CommandSet(**_ipv4_default)
|
||||
ipv6_default: CommandSet = CommandSet(**_ipv6_default)
|
||||
ipv4_vpn: CommandSet = CommandSet(**_ipv4_vpn)
|
||||
ipv6_vpn: CommandSet = CommandSet(**_ipv6_vpn)
|
||||
ipv4_default: _IPv4 = _IPv4()
|
||||
ipv6_default: _IPv6 = _IPv6()
|
||||
ipv4_vpn: _VPNIPv4 = _VPNIPv4()
|
||||
ipv6_vpn: _VPNIPv6 = _VPNIPv6()
|
||||
|
@@ -1,12 +1,10 @@
|
||||
"""Models common to entire commands module."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
# Third Party
|
||||
from pydantic import StrictStr
|
||||
|
||||
# Project
|
||||
from hyperglass.models import HyperglassModel
|
||||
from hyperglass.models import HyperglassModel, HyperglassModelExtra
|
||||
|
||||
|
||||
class CommandSet(HyperglassModel):
|
||||
@@ -19,14 +17,10 @@ class CommandSet(HyperglassModel):
|
||||
traceroute: StrictStr
|
||||
|
||||
|
||||
class CommandGroup(HyperglassModel):
|
||||
class CommandGroup(HyperglassModelExtra):
|
||||
"""Validation model for all commands."""
|
||||
|
||||
ipv4_default: CommandSet
|
||||
ipv6_default: CommandSet
|
||||
ipv4_vpn: CommandSet
|
||||
ipv6_vpn: CommandSet
|
||||
structured: Optional["CommandGroup"]
|
||||
|
||||
|
||||
CommandGroup.update_forward_refs()
|
||||
|
@@ -1,42 +1,56 @@
|
||||
"""Huawei Command Model."""
|
||||
|
||||
# Third Party
|
||||
from pydantic import StrictStr
|
||||
|
||||
# Project
|
||||
from hyperglass.configuration.models.commands.common import CommandSet, CommandGroup
|
||||
|
||||
_ipv4_default = {
|
||||
"bgp_route": "display bgp routing-table {target}",
|
||||
"bgp_aspath": "display bgp routing-table regular-expression {target}",
|
||||
"bgp_community": "display bgp routing-table regular-expression {target}",
|
||||
"ping": "ping -c 5 -a {source} {target}",
|
||||
"traceroute": "tracert -q 2 -f 1 -a {source} {target}",
|
||||
}
|
||||
_ipv6_default = {
|
||||
"bgp_route": "display bgp ipv6 routing-table {target}",
|
||||
"bgp_aspath": "display bgp ipv6 routing-table regular-expression {target}",
|
||||
"bgp_community": "display bgp ipv6 routing-table community {target}",
|
||||
"ping": "ping ipv6 -c 5 -a {source} {target}",
|
||||
"traceroute": "tracert ipv6 -q 2 -f 1 -a {source} {target}",
|
||||
}
|
||||
_ipv4_vpn = {
|
||||
"bgp_route": "display bgp vpnv4 vpn-instance {vrf} routing-table {target}",
|
||||
"bgp_aspath": "display bgp vpnv4 vpn-instance {vrf} routing-table regular-expression {target}",
|
||||
"bgp_community": "display bgp vpnv4 vpn-instance {vrf} routing-table regular-expression {target}",
|
||||
"ping": "ping -vpn-instance {vrf} -c 5 -a {source} {target}",
|
||||
"traceroute": "tracert -q 2 -f 1 -vpn-instance {vrf} -a {source} {target}",
|
||||
}
|
||||
_ipv6_vpn = {
|
||||
"bgp_route": "display bgp vpnv6 vpn-instance {vrf} routing-table {target}",
|
||||
"bgp_aspath": "display bgp vpnv6 vpn-instance {vrf} routing-table regular-expression {target}",
|
||||
"bgp_community": "display bgp vpnv6 vpn-instance {vrf} routing-table regular-expression {target}",
|
||||
"ping": "ping vpnv6 vpn-instance {vrf} -c 5 -a {source} {target}",
|
||||
"traceroute": "tracert -q 2 -f 1 vpn-instance {vrf} -a {source} {target}",
|
||||
}
|
||||
|
||||
class _IPv4(CommandSet):
|
||||
"""Default commands for ipv4 commands."""
|
||||
|
||||
bgp_community: StrictStr = "display bgp routing-table regular-expression {target}"
|
||||
bgp_aspath: StrictStr = "display bgp routing-table regular-expression {target}"
|
||||
bgp_route: StrictStr = "display bgp routing-table {target}"
|
||||
ping: StrictStr = "ping -c 5 -a {source} {target}"
|
||||
traceroute: StrictStr = "tracert -q 2 -f 1 -a {source} {target}"
|
||||
|
||||
|
||||
class _IPv6(CommandSet):
|
||||
"""Default commands for ipv6 commands."""
|
||||
|
||||
bgp_community: StrictStr = "display bgp ipv6 routing-table community {target}"
|
||||
bgp_aspath: StrictStr = "display bgp ipv6 routing-table regular-expression {target}"
|
||||
bgp_route: StrictStr = "display bgp ipv6 routing-table {target}"
|
||||
ping: StrictStr = "ping ipv6 -c 5 -a {source} {target}"
|
||||
traceroute: StrictStr = "tracert ipv6 -q 2 -f 1 -a {source} {target}"
|
||||
|
||||
|
||||
class _VPNIPv4(CommandSet):
|
||||
"""Default commands for dual afi commands."""
|
||||
|
||||
bgp_community: StrictStr = "display bgp vpnv4 vpn-instance {vrf} routing-table regular-expression {target}"
|
||||
bgp_aspath: StrictStr = "display bgp vpnv4 vpn-instance {vrf} routing-table regular-expression {target}"
|
||||
bgp_route: StrictStr = "display bgp vpnv4 vpn-instance {vrf} routing-table {target}"
|
||||
ping: StrictStr = "ping -vpn-instance {vrf} -c 5 -a {source} {target}"
|
||||
traceroute: StrictStr = "tracert -q 2 -f 1 -vpn-instance {vrf} -a {source} {target}"
|
||||
|
||||
|
||||
class _VPNIPv6(CommandSet):
|
||||
"""Default commands for dual afi commands."""
|
||||
|
||||
bgp_community: StrictStr = "display bgp vpnv6 vpn-instance {vrf} routing-table regular-expression {target}"
|
||||
bgp_aspath: StrictStr = "display bgp vpnv6 vpn-instance {vrf} routing-table regular-expression {target}"
|
||||
bgp_route: StrictStr = "display bgp vpnv6 vpn-instance {vrf} routing-table {target}"
|
||||
ping: StrictStr = "ping vpnv6 vpn-instance {vrf} -c 5 -a {source} {target}"
|
||||
traceroute: StrictStr = "tracert -q 2 -f 1 vpn-instance {vrf} -a {source} {target}"
|
||||
|
||||
|
||||
class HuaweiCommands(CommandGroup):
|
||||
"""Validation model for default huawei commands."""
|
||||
|
||||
ipv4_default: CommandSet = CommandSet(**_ipv4_default)
|
||||
ipv6_default: CommandSet = CommandSet(**_ipv6_default)
|
||||
ipv4_vpn: CommandSet = CommandSet(**_ipv4_vpn)
|
||||
ipv6_vpn: CommandSet = CommandSet(**_ipv6_vpn)
|
||||
ipv4_default: _IPv4 = _IPv4()
|
||||
ipv6_default: _IPv6 = _IPv6()
|
||||
ipv4_vpn: _VPNIPv4 = _VPNIPv4()
|
||||
ipv6_vpn: _VPNIPv6 = _VPNIPv6()
|
||||
|
@@ -1,63 +1,78 @@
|
||||
"""Juniper Command Model."""
|
||||
|
||||
# Third Party
|
||||
from pydantic import StrictStr
|
||||
|
||||
# Project
|
||||
from hyperglass.configuration.models.commands.common import CommandSet, CommandGroup
|
||||
|
||||
_ipv4_default = {
|
||||
"bgp_route": 'show route protocol bgp table inet.0 {target} detail | except Label | except Label | except "Next hop type" | except Task | except Address | except "Session Id" | except State | except "Next-hop reference" | except destinations | except "Announcement bits"',
|
||||
"bgp_aspath": 'show route protocol bgp table inet.0 aspath-regex "{target}"',
|
||||
"bgp_community": "show route protocol bgp table inet.0 community {target}",
|
||||
"ping": "ping inet {target} count 5 source {source}",
|
||||
"traceroute": "traceroute inet {target} wait 1 source {source}",
|
||||
}
|
||||
_ipv6_default = {
|
||||
"bgp_route": 'show route protocol bgp table inet6.0 {target} detail | except Label | except Label | except "Next hop type" | except Task | except Address | except "Session Id" | except State | except "Next-hop reference" | except destinations | except "Announcement bits"',
|
||||
"bgp_aspath": 'show route protocol bgp table inet6.0 aspath-regex "{target}"',
|
||||
"bgp_community": "show route protocol bgp table inet6.0 community {target}",
|
||||
"ping": "ping inet6 {target} count 5 source {source}",
|
||||
"traceroute": "traceroute inet6 {target} wait 2 source {source}",
|
||||
}
|
||||
_ipv4_vpn = {
|
||||
"bgp_route": 'show route protocol bgp table {vrf}.inet.0 {target} detail | except Label | except Label | except "Next hop type" | except Task | except Address | except "Session Id" | except State | except "Next-hop reference" | except destinations | except "Announcement bits"',
|
||||
"bgp_aspath": 'show route protocol bgp table {vrf}.inet.0 aspath-regex "{target}"',
|
||||
"bgp_community": "show route protocol bgp table {vrf}.inet.0 community {target}",
|
||||
"ping": "ping inet routing-instance {vrf} {target} count 5 source {source}",
|
||||
"traceroute": "traceroute inet routing-instance {vrf} {target} wait 1 source {source}",
|
||||
}
|
||||
_ipv6_vpn = {
|
||||
"bgp_route": 'show route protocol bgp table {vrf}.inet6.0 {target} detail | except Label | except Label | except "Next hop type" | except Task | except Address | except "Session Id" | except State | except "Next-hop reference" | except destinations | except "Announcement bits"',
|
||||
"bgp_aspath": 'show route protocol bgp table {vrf}.inet6.0 aspath-regex "{target}"',
|
||||
"bgp_community": "show route protocol bgp table {vrf}.inet6.0 community {target}",
|
||||
"ping": "ping inet6 routing-instance {vrf} {target} count 5 source {source}",
|
||||
"traceroute": "traceroute inet6 routing-instance {vrf} {target} wait 2 source {source}",
|
||||
}
|
||||
|
||||
class _IPv4(CommandSet):
|
||||
"""Validation model for non-default dual afi commands."""
|
||||
|
||||
bgp_route: StrictStr = 'show route protocol bgp table inet.0 {target} detail | except Label | except Label | except "Next hop type" | except Task | except Address | except "Session Id" | except State | except "Next-hop reference" | except destinations | except "Announcement bits"'
|
||||
bgp_aspath: StrictStr = 'show route protocol bgp table inet.0 aspath-regex "{target}"'
|
||||
bgp_community: StrictStr = "show route protocol bgp table inet.0 community {target}"
|
||||
ping: StrictStr = "ping inet {target} count 5 source {source}"
|
||||
traceroute: StrictStr = "traceroute inet {target} wait 1 source {source}"
|
||||
|
||||
|
||||
class _IPv6(CommandSet):
|
||||
"""Validation model for non-default ipv4 commands."""
|
||||
|
||||
bgp_route: StrictStr = 'show route protocol bgp table inet6.0 {target} detail | except Label | except Label | except "Next hop type" | except Task | except Address | except "Session Id" | except State | except "Next-hop reference" | except destinations | except "Announcement bits"'
|
||||
bgp_aspath: StrictStr = 'show route protocol bgp table inet6.0 aspath-regex "{target}"'
|
||||
bgp_community: StrictStr = "show route protocol bgp table inet6.0 community {target}"
|
||||
ping: StrictStr = "ping inet6 {target} count 5 source {source}"
|
||||
traceroute: StrictStr = "traceroute inet6 {target} wait 2 source {source}"
|
||||
|
||||
|
||||
class _VPNIPv4(CommandSet):
|
||||
"""Validation model for non-default ipv6 commands."""
|
||||
|
||||
bgp_route: StrictStr = 'show route protocol bgp table {vrf}.inet.0 {target} detail | except Label | except Label | except "Next hop type" | except Task | except Address | except "Session Id" | except State | except "Next-hop reference" | except destinations | except "Announcement bits"'
|
||||
bgp_aspath: StrictStr = 'show route protocol bgp table {vrf}.inet.0 aspath-regex "{target}"'
|
||||
bgp_community: StrictStr = "show route protocol bgp table {vrf}.inet.0 community {target}"
|
||||
ping: StrictStr = "ping inet routing-instance {vrf} {target} count 5 source {source}"
|
||||
traceroute: StrictStr = "traceroute inet routing-instance {vrf} {target} wait 1 source {source}"
|
||||
|
||||
|
||||
class _VPNIPv6(CommandSet):
|
||||
"""Validation model for non-default ipv6 commands."""
|
||||
|
||||
bgp_route: StrictStr = 'show route protocol bgp table {vrf}.inet6.0 {target} detail | except Label | except Label | except "Next hop type" | except Task | except Address | except "Session Id" | except State | except "Next-hop reference" | except destinations | except "Announcement bits"'
|
||||
bgp_aspath: StrictStr = 'show route protocol bgp table {vrf}.inet6.0 aspath-regex "{target}"'
|
||||
bgp_community: StrictStr = "show route protocol bgp table {vrf}.inet6.0 community {target}"
|
||||
ping: StrictStr = "ping inet6 routing-instance {vrf} {target} count 5 source {source}"
|
||||
traceroute: StrictStr = "traceroute inet6 routing-instance {vrf} {target} wait 2 source {source}"
|
||||
|
||||
|
||||
_structured = CommandGroup(
|
||||
ipv4_default=CommandSet(
|
||||
bgp_route="show route protocol bgp table inet.0 {target} detail | display json",
|
||||
bgp_aspath='show route protocol bgp table inet.0 aspath-regex "{target}" | display json',
|
||||
bgp_community="show route protocol bgp table inet.0 community {target} | display json",
|
||||
bgp_route="show route protocol bgp table inet.0 {target} detail | display xml",
|
||||
bgp_aspath='show route protocol bgp table inet.0 aspath-regex "{target}" detail | display xml',
|
||||
bgp_community="show route protocol bgp table inet.0 community {target} detail | display xml",
|
||||
ping="ping inet {target} count 5 source {source}",
|
||||
traceroute="traceroute inet {target} wait 1 source {source}",
|
||||
),
|
||||
ipv6_default=CommandSet(
|
||||
bgp_route="show route protocol bgp table inet6.0 {target} detail | display json",
|
||||
bgp_aspath='show route protocol bgp table inet6.0 aspath-regex "{target}" | display json',
|
||||
bgp_community="show route protocol bgp table inet6.0 community {target} | display json",
|
||||
bgp_route="show route protocol bgp table inet6.0 {target} detail | display xml",
|
||||
bgp_aspath='show route protocol bgp table inet6.0 aspath-regex "{target}" detail | display xml',
|
||||
bgp_community="show route protocol bgp table inet6.0 community {target} detail | display xml",
|
||||
ping="ping inet6 {target} count 5 source {source}",
|
||||
traceroute="traceroute inet6 {target} wait 2 source {source}",
|
||||
),
|
||||
ipv4_vpn=CommandSet(
|
||||
bgp_route="show route protocol bgp table {vrf}.inet.0 {target} detail | display json",
|
||||
bgp_aspath='show route protocol bgp table {vrf}.inet.0 aspath-regex "{target}" | display json',
|
||||
bgp_community="show route protocol bgp table {vrf}.inet.0 community {target} | display json",
|
||||
bgp_route="show route protocol bgp table {vrf}.inet.0 {target} detail | display xml",
|
||||
bgp_aspath='show route protocol bgp table {vrf}.inet.0 aspath-regex "{target}" detail | display xml',
|
||||
bgp_community="show route protocol bgp table {vrf}.inet.0 community {target} detail | display xml",
|
||||
ping="ping inet routing-instance {vrf} {target} count 5 source {source}",
|
||||
traceroute="traceroute inet routing-instance {vrf} {target} wait 1 source {source}",
|
||||
),
|
||||
ipv6_vpn=CommandSet(
|
||||
bgp_route="show route protocol bgp table {vrf}.inet6.0 {target} detail | display json",
|
||||
bgp_aspath='show route protocol bgp table {vrf}.inet6.0 aspath-regex "{target}" | display json',
|
||||
bgp_community="show route protocol bgp table {vrf}.inet6.0 community {target} | display json",
|
||||
bgp_route="show route protocol bgp table {vrf}.inet6.0 {target} detail | display xml",
|
||||
bgp_aspath='show route protocol bgp table {vrf}.inet6.0 aspath-regex "{target}" detail | display xml',
|
||||
bgp_community="show route protocol bgp table {vrf}.inet6.0 community {target} detail | display xml",
|
||||
ping="ping inet6 routing-instance {vrf} {target} count 5 source {source}",
|
||||
traceroute="traceroute inet6 routing-instance {vrf} {target} wait 1 source {source}",
|
||||
),
|
||||
@@ -67,8 +82,12 @@ _structured = CommandGroup(
|
||||
class JuniperCommands(CommandGroup):
|
||||
"""Validation model for default juniper commands."""
|
||||
|
||||
ipv4_default: CommandSet = CommandSet(**_ipv4_default)
|
||||
ipv6_default: CommandSet = CommandSet(**_ipv6_default)
|
||||
ipv4_vpn: CommandSet = CommandSet(**_ipv4_vpn)
|
||||
ipv6_vpn: CommandSet = CommandSet(**_ipv6_vpn)
|
||||
structured: CommandGroup = _structured
|
||||
ipv4_default: _IPv4 = _IPv4()
|
||||
ipv6_default: _IPv6 = _IPv6()
|
||||
ipv4_vpn: _VPNIPv4 = _VPNIPv4()
|
||||
ipv6_vpn: _VPNIPv6 = _VPNIPv6()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize command group, ensure structured fields are not overridden."""
|
||||
super().__init__(**kwargs)
|
||||
self.structured = _structured
|
||||
|
@@ -75,7 +75,7 @@ class Messages(HyperglassModel):
|
||||
description="Displayed when a query VRF is not configured on any devices. The hyperglass UI only shows configured VRFs, so this error is most likely to appear when using the hyperglass API. `{vrf_name}` may be used to display the VRF in question.",
|
||||
)
|
||||
no_output: StrictStr = Field(
|
||||
"No output.",
|
||||
"The query completed, but no matching results were found.",
|
||||
title="No Output",
|
||||
description="Displayed when hyperglass can connect to a device and execute a query, but the response is empty.",
|
||||
)
|
||||
|
@@ -27,6 +27,7 @@ DNS_OVER_HTTPS = {
|
||||
}
|
||||
|
||||
PARSED_RESPONSE_FIELDS = (
|
||||
("Prefix", "prefix", "left"),
|
||||
("Active", "active", None),
|
||||
("RPKI State", "rpki_state", "center"),
|
||||
("AS Path", "as_path", "left"),
|
||||
|
@@ -8,7 +8,7 @@ hyperglass API modules.
|
||||
# Standard Library
|
||||
import re
|
||||
import json as _json
|
||||
import operator
|
||||
from operator import attrgetter
|
||||
|
||||
# Project
|
||||
from hyperglass.log import log
|
||||
@@ -120,9 +120,17 @@ class Construct:
|
||||
Returns:
|
||||
{str} -- Command string
|
||||
"""
|
||||
command = operator.attrgetter(
|
||||
f"{self.device.nos}.{afi.protocol}.{self.query_data.query_type}"
|
||||
)(commands)
|
||||
if self.device.structured_output:
|
||||
cmd_paths = (
|
||||
self.device.nos,
|
||||
"structured",
|
||||
afi.protocol,
|
||||
self.query_data.query_type,
|
||||
)
|
||||
else:
|
||||
cmd_paths = (self.device.nos, afi.protocol, self.query_data.query_type)
|
||||
|
||||
command = attrgetter(".".join(cmd_paths))(commands)
|
||||
return command.format(
|
||||
target=self.target,
|
||||
source=str(afi.source_address),
|
||||
|
@@ -34,6 +34,7 @@ from hyperglass.exceptions import (
|
||||
DeviceTimeout,
|
||||
ResponseEmpty,
|
||||
)
|
||||
from hyperglass.parsing.nos import nos_parsers
|
||||
from hyperglass.configuration import params, devices
|
||||
from hyperglass.parsing.common import parsers
|
||||
from hyperglass.execution.construct import Construct
|
||||
@@ -79,9 +80,23 @@ class Connect:
|
||||
Returns:
|
||||
{str} -- Parsed output
|
||||
"""
|
||||
log.debug(f"Pre-parsed responses:\n{output}")
|
||||
parsed = ()
|
||||
if not self.device.structured_output:
|
||||
for coro in parsers:
|
||||
output = await coro(commands=self.query, output=output)
|
||||
return output
|
||||
for response in output:
|
||||
_output = await coro(commands=self.query, output=response)
|
||||
parsed += (_output,)
|
||||
response = "\n\n".join(parsed)
|
||||
elif (
|
||||
self.device.structured_output
|
||||
and self.device.nos in nos_parsers.keys()
|
||||
and self.query_type in nos_parsers[self.device.nos].keys()
|
||||
):
|
||||
func = nos_parsers[self.device.nos][self.query_type]
|
||||
response = func(output)
|
||||
log.debug(f"Post-parsed responses:\n{response}")
|
||||
return response
|
||||
|
||||
async def scrape_proxied(self):
|
||||
"""Connect to a device via an SSH proxy.
|
||||
@@ -150,12 +165,9 @@ class Connect:
|
||||
raw = nm_connect_direct.send_command(query)
|
||||
responses += (raw,)
|
||||
log.debug(f'Raw response for command "{query}":\n{raw}')
|
||||
response = "\n\n".join(responses)
|
||||
|
||||
nm_connect_direct.disconnect()
|
||||
|
||||
log.debug(f"Response type:\n{type(response)}")
|
||||
|
||||
except (NetMikoTimeoutException, NetmikoTimeoutError) as scrape_error:
|
||||
log.error(
|
||||
"Timeout connecting to device {loc}: {e}",
|
||||
@@ -187,7 +199,7 @@ class Connect:
|
||||
proxy=self.device.proxy.name,
|
||||
error=params.messages.general,
|
||||
)
|
||||
if response is None:
|
||||
if not responses:
|
||||
raise ScrapeError(
|
||||
params.messages.connection_error,
|
||||
device_name=self.device.display_name,
|
||||
@@ -195,7 +207,7 @@ class Connect:
|
||||
error=params.messages.no_response,
|
||||
)
|
||||
signal.alarm(0)
|
||||
return await self.parsed_response(response)
|
||||
return await self.parsed_response(responses)
|
||||
|
||||
async def scrape_direct(self):
|
||||
"""Connect directly to a device.
|
||||
@@ -235,8 +247,6 @@ class Connect:
|
||||
responses += (raw,)
|
||||
log.debug(f'Raw response for command "{query}":\n{raw}')
|
||||
|
||||
response = "\n\n".join(responses)
|
||||
|
||||
nm_connect_direct.disconnect()
|
||||
|
||||
except (NetMikoTimeoutException, NetmikoTimeoutError) as scrape_error:
|
||||
@@ -260,7 +270,7 @@ class Connect:
|
||||
proxy=None,
|
||||
error=params.messages.authentication_error,
|
||||
)
|
||||
if response is None:
|
||||
if not responses:
|
||||
raise ScrapeError(
|
||||
params.messages.connection_error,
|
||||
device_name=self.device.display_name,
|
||||
@@ -268,7 +278,7 @@ class Connect:
|
||||
error=params.messages.no_response,
|
||||
)
|
||||
signal.alarm(0)
|
||||
return await self.parsed_response(response)
|
||||
return await self.parsed_response(responses)
|
||||
|
||||
async def rest(self): # noqa: C901
|
||||
"""Connect to a device running hyperglass-agent via HTTP."""
|
||||
@@ -305,7 +315,7 @@ class Connect:
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(**client_params) as http_client:
|
||||
responses = []
|
||||
responses = ()
|
||||
|
||||
for query in self.query:
|
||||
encoded_query = await jwt_encode(
|
||||
@@ -329,13 +339,11 @@ class Connect:
|
||||
secret=self.device.credential.password.get_secret_value(),
|
||||
)
|
||||
log.debug(f"Decoded Response: {decoded}")
|
||||
responses += (decoded,)
|
||||
|
||||
responses.append(decoded)
|
||||
else:
|
||||
log.error(raw_response.text)
|
||||
|
||||
response = "\n\n".join(responses)
|
||||
log.debug(f"Output for query {self.query}:\n{response}")
|
||||
except httpx.exceptions.HTTPError as rest_error:
|
||||
msg = parse_exception(rest_error)
|
||||
log.error(f"Error connecting to device {self.device.name}: {msg}")
|
||||
@@ -368,7 +376,7 @@ class Connect:
|
||||
error=params.messages.general,
|
||||
)
|
||||
|
||||
if not response:
|
||||
if not responses:
|
||||
log.error(f"No response from device {self.device.name}")
|
||||
raise RestError(
|
||||
params.messages.connection_error,
|
||||
@@ -376,7 +384,7 @@ class Connect:
|
||||
error=params.messages.no_response,
|
||||
)
|
||||
|
||||
return await self.parsed_response(response)
|
||||
return await self.parsed_response(responses)
|
||||
|
||||
|
||||
class Execute:
|
||||
|
@@ -5,21 +5,40 @@ import xmltodict
|
||||
|
||||
# Project
|
||||
from hyperglass.log import log
|
||||
from hyperglass.exceptions import ParsingError
|
||||
from hyperglass.exceptions import ParsingError, ResponseEmpty
|
||||
from hyperglass.configuration import params
|
||||
from hyperglass.parsing.models.juniper import JuniperRoute
|
||||
|
||||
|
||||
def parse_juniper(output):
|
||||
"""Parse a Juniper BGP XML response."""
|
||||
data = {}
|
||||
for i, response in enumerate(output):
|
||||
try:
|
||||
parsed = xmltodict.parse(output)["rpc-reply"]["route-information"][
|
||||
"route-table"
|
||||
]
|
||||
parsed = xmltodict.parse(response, force_list=("rt", "rt-entry"))
|
||||
|
||||
if "rpc-reply" in parsed.keys():
|
||||
parsed = parsed["rpc-reply"]["route-information"]["route-table"]
|
||||
elif "route-information" in parsed.keys():
|
||||
parsed = parsed["route-information"]["route-table"]
|
||||
|
||||
if "rt" not in parsed:
|
||||
raise ResponseEmpty(params.messages.no_output)
|
||||
|
||||
validated = JuniperRoute(**parsed)
|
||||
return validated.serialize().export_dict()
|
||||
serialized = validated.serialize().export_dict()
|
||||
|
||||
if i == 0:
|
||||
data.update(serialized)
|
||||
else:
|
||||
data["routes"].extend(serialized["routes"])
|
||||
|
||||
except xmltodict.expat.ExpatError as err:
|
||||
log.critical(str(err))
|
||||
raise ParsingError("Error parsing response data")
|
||||
|
||||
except KeyError as err:
|
||||
log.critical(f"'{str(err)}' was not found in the response")
|
||||
raise ParsingError("Error parsing response data")
|
||||
|
||||
return data
|
||||
|
81
hyperglass/parsing/linux.py
Normal file
81
hyperglass/parsing/linux.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# flake8: noqa
|
||||
# WORK IN PROGRESS
|
||||
|
||||
"""Linux-style parsers for ping & traceroute."""
|
||||
|
||||
# Standard Library
|
||||
import re
|
||||
|
||||
# Project
|
||||
from hyperglass.exceptions import ParsingError
|
||||
|
||||
|
||||
def _process_numbers(numbers):
|
||||
"""Convert string to float or int."""
|
||||
for num in numbers:
|
||||
num = float(num)
|
||||
if num.is_integer():
|
||||
num = int(num)
|
||||
yield num
|
||||
|
||||
|
||||
def parse_linux_ping(output):
|
||||
"""Parse standard Linux-style ping output to structured data.
|
||||
|
||||
Example:
|
||||
64 bytes from 1.1.1.1: icmp_seq=0 ttl=59 time=1.151 ms
|
||||
64 bytes from 1.1.1.1: icmp_seq=1 ttl=59 time=1.180 ms
|
||||
64 bytes from 1.1.1.1: icmp_seq=2 ttl=59 time=1.170 ms
|
||||
64 bytes from 1.1.1.1: icmp_seq=3 ttl=59 time=1.338 ms
|
||||
64 bytes from 1.1.1.1: icmp_seq=4 ttl=59 time=4.913 ms
|
||||
|
||||
--- 1.1.1.1 ping statistics ---
|
||||
5 packets transmitted, 5 packets received, 0% packet loss
|
||||
round-trip min/avg/max/stddev = 1.151/1.950/4.913/1.483 ms
|
||||
"""
|
||||
try:
|
||||
# Extract target host
|
||||
host = re.findall(r"^PING (.+) \(.+\): \d+ data bytes", output)[0]
|
||||
|
||||
# Separate echo replies from summary info
|
||||
replies, _stats = re.split(r"--- .+ ---", output)
|
||||
replies = [l for l in replies.splitlines()[1:] if l]
|
||||
|
||||
reply_stats = []
|
||||
for line in replies:
|
||||
# Extract the numerical values from each echo reply line
|
||||
bytes_seq_ttl_rtt = re.findall(
|
||||
r"(\d+) bytes.+ icmp_seq=(\d+) ttl=(\d+) time=(\d+\.\d+).*", line
|
||||
)[0]
|
||||
|
||||
_bytes, seq, ttl, rtt = _process_numbers(bytes_seq_ttl_rtt)
|
||||
|
||||
reply_stats.append(
|
||||
{"bytes": _bytes, "sequence": seq, "ttl": ttl, "rtt": rtt}
|
||||
)
|
||||
|
||||
stats = [l for l in _stats.splitlines() if l]
|
||||
|
||||
# Extract the top summary line numbers & process
|
||||
tx_rx_loss = re.findall(r"(\d+) .+, (\d+) .+, (\d+)\%.+", stats[0])[0]
|
||||
tx, rx, loss = _process_numbers(tx_rx_loss)
|
||||
|
||||
# Extract the bottom summary line numbers & process
|
||||
rt = stats[1].split(" = ")[1]
|
||||
min_max_avg = rt.split("/")[:-1]
|
||||
_min, _max, _avg = _process_numbers(min_max_avg)
|
||||
|
||||
return {
|
||||
"host": host,
|
||||
"transmitted": tx,
|
||||
"received": rx,
|
||||
"loss_percent": loss,
|
||||
"min_rtt": _min,
|
||||
"max_rtt": _max,
|
||||
"avg_rtt": _avg,
|
||||
"replies": reply_stats,
|
||||
}
|
||||
|
||||
except (KeyError, ValueError) as err:
|
||||
# KeyError for empty findalls, ValueError for regex errors
|
||||
raise ParsingError("Error parsing ping response: {e}", e=str(err))
|
114
hyperglass/parsing/models/frr.py
Normal file
114
hyperglass/parsing/models/frr.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""Data Models for Parsing FRRouting JSON Response."""
|
||||
|
||||
# Standard Library
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
|
||||
# Third Party
|
||||
from pydantic import StrictInt, StrictStr, StrictBool, constr, root_validator
|
||||
|
||||
# Project
|
||||
from hyperglass.log import log
|
||||
from hyperglass.models import HyperglassModel
|
||||
from hyperglass.parsing.models.serialized import ParsedRoutes
|
||||
|
||||
|
||||
def _alias_generator(field):
|
||||
components = field.split("_")
|
||||
return components[0] + "".join(x.title() for x in components[1:])
|
||||
|
||||
|
||||
class _FRRBase(HyperglassModel):
|
||||
class Config:
|
||||
alias_generator = _alias_generator
|
||||
extra = "ignore"
|
||||
|
||||
|
||||
class FRRNextHop(_FRRBase):
|
||||
"""FRR Next Hop Model."""
|
||||
|
||||
ip: StrictStr
|
||||
afi: StrictStr
|
||||
metric: StrictInt
|
||||
accessible: StrictBool
|
||||
used: StrictBool
|
||||
|
||||
|
||||
class FRRPeer(_FRRBase):
|
||||
"""FRR Peer Model."""
|
||||
|
||||
peer_id: StrictStr
|
||||
router_id: StrictStr
|
||||
type: constr(regex=r"(internal|external)")
|
||||
|
||||
|
||||
class FRRPath(_FRRBase):
|
||||
"""FRR Path Model."""
|
||||
|
||||
aspath: List[StrictInt]
|
||||
aggregator_as: StrictInt
|
||||
aggregator_id: StrictStr
|
||||
med: StrictInt = 0
|
||||
localpref: StrictInt
|
||||
weight: StrictInt
|
||||
valid: StrictBool
|
||||
last_update: StrictInt
|
||||
bestpath: StrictBool
|
||||
community: List[StrictStr]
|
||||
nexthops: List[FRRNextHop]
|
||||
peer: FRRPeer
|
||||
|
||||
@root_validator(pre=True)
|
||||
def validate_path(cls, values):
|
||||
"""Extract meaningful data from FRR response."""
|
||||
new = values.copy()
|
||||
new["aspath"] = values["aspath"]["segments"][0]["list"]
|
||||
new["community"] = values["community"]["list"]
|
||||
new["lastUpdate"] = values["lastUpdate"]["epoch"]
|
||||
bestpath = values.get("bestpath", {})
|
||||
new["bestpath"] = bestpath.get("overall", False)
|
||||
return new
|
||||
|
||||
|
||||
class FRRRoute(_FRRBase):
|
||||
"""FRR Route Model."""
|
||||
|
||||
prefix: StrictStr
|
||||
paths: List[FRRPath] = []
|
||||
|
||||
def serialize(self):
|
||||
"""Convert the FRR-specific fields to standard parsed data model."""
|
||||
|
||||
# TODO: somehow, get the actual VRF
|
||||
vrf = "default"
|
||||
|
||||
routes = []
|
||||
for route in self.paths:
|
||||
now = datetime.utcnow().timestamp()
|
||||
then = datetime.utcfromtimestamp(route.last_update).timestamp()
|
||||
age = int(now - then)
|
||||
routes.append(
|
||||
{
|
||||
"prefix": self.prefix,
|
||||
"active": route.bestpath,
|
||||
"age": age,
|
||||
"weight": route.weight,
|
||||
"med": route.med,
|
||||
"local_preference": route.localpref,
|
||||
"as_path": route.aspath,
|
||||
"communities": route.community,
|
||||
"next_hop": route.nexthops[0].ip,
|
||||
"source_as": route.aggregator_as,
|
||||
"source_rid": route.aggregator_id,
|
||||
"peer_rid": route.peer.peer_id,
|
||||
# TODO: somehow, get the actual RPKI state
|
||||
"rpki_state": 3,
|
||||
}
|
||||
)
|
||||
|
||||
serialized = ParsedRoutes(
|
||||
vrf=vrf, count=len(routes), routes=routes, winning_weight="high",
|
||||
)
|
||||
|
||||
log.info("Serialized FRR response: {}", serialized)
|
||||
return serialized
|
84
hyperglass/parsing/models/frr_bgp_community.json
Normal file
84
hyperglass/parsing/models/frr_bgp_community.json
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"vrfId": 0,
|
||||
"vrfName": "default",
|
||||
"tableVersion": 32856694,
|
||||
"routerId": "199.34.92.3",
|
||||
"defaultLocPrf": 100,
|
||||
"localAS": 14525,
|
||||
"routes": {
|
||||
"38.29.214.0/24": [
|
||||
{
|
||||
"valid": true,
|
||||
"bestpath": true,
|
||||
"pathFrom": "internal",
|
||||
"prefix": "38.29.214.0",
|
||||
"prefixLen": 24,
|
||||
"network": "38.29.214.0/24",
|
||||
"med": 0,
|
||||
"metric": 0,
|
||||
"localpref": 450,
|
||||
"locPrf": 450,
|
||||
"weight": 200,
|
||||
"peerId": "199.34.92.1",
|
||||
"aspath": "",
|
||||
"path": "",
|
||||
"origin": "IGP",
|
||||
"nexthops": [{ "ip": "199.34.92.1", "afi": "ipv4", "used": true }]
|
||||
}
|
||||
],
|
||||
"199.34.92.0/23": [
|
||||
{
|
||||
"valid": true,
|
||||
"pathFrom": "internal",
|
||||
"prefix": "199.34.92.0",
|
||||
"prefixLen": 23,
|
||||
"network": "199.34.92.0/23",
|
||||
"med": 0,
|
||||
"metric": 0,
|
||||
"localpref": 450,
|
||||
"locPrf": 450,
|
||||
"weight": 200,
|
||||
"peerId": "199.34.92.10",
|
||||
"aspath": "",
|
||||
"path": "",
|
||||
"origin": "IGP",
|
||||
"nexthops": [{ "ip": "199.34.92.10", "afi": "ipv4", "used": true }]
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"pathFrom": "internal",
|
||||
"prefix": "199.34.92.0",
|
||||
"prefixLen": 23,
|
||||
"network": "199.34.92.0/23",
|
||||
"med": 0,
|
||||
"metric": 0,
|
||||
"localpref": 450,
|
||||
"locPrf": 450,
|
||||
"weight": 200,
|
||||
"peerId": "199.34.92.9",
|
||||
"aspath": "",
|
||||
"path": "",
|
||||
"origin": "IGP",
|
||||
"nexthops": [{ "ip": "199.34.92.9", "afi": "ipv4", "used": true }]
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"bestpath": true,
|
||||
"pathFrom": "internal",
|
||||
"prefix": "199.34.92.0",
|
||||
"prefixLen": 23,
|
||||
"network": "199.34.92.0/23",
|
||||
"med": 0,
|
||||
"metric": 0,
|
||||
"localpref": 450,
|
||||
"locPrf": 450,
|
||||
"weight": 200,
|
||||
"peerId": "199.34.92.1",
|
||||
"aspath": "",
|
||||
"path": "",
|
||||
"origin": "IGP",
|
||||
"nexthops": [{ "ip": "199.34.92.1", "afi": "ipv4", "used": true }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
224
hyperglass/parsing/models/frr_bgp_route.json
Normal file
224
hyperglass/parsing/models/frr_bgp_route.json
Normal file
@@ -0,0 +1,224 @@
|
||||
{
|
||||
"prefix": "1.1.1.0/24",
|
||||
"paths": [
|
||||
{
|
||||
"aspath": {
|
||||
"string": "174 13335",
|
||||
"segments": [
|
||||
{
|
||||
"type": "as-sequence",
|
||||
"list": [174, 13335]
|
||||
}
|
||||
],
|
||||
"length": 2
|
||||
},
|
||||
"aggregatorAs": 13335,
|
||||
"aggregatorId": "108.162.239.1",
|
||||
"origin": "IGP",
|
||||
"med": 25090,
|
||||
"metric": 25090,
|
||||
"localpref": 100,
|
||||
"weight": 100,
|
||||
"valid": true,
|
||||
"community": {
|
||||
"string": "174:21001 174:22003 14525:0 14525:40 14525:1021 14525:2840 14525:3003 14525:4004 14525:9001",
|
||||
"list": [
|
||||
"174:21001",
|
||||
"174:22003",
|
||||
"14525:0",
|
||||
"14525:40",
|
||||
"14525:1021",
|
||||
"14525:2840",
|
||||
"14525:3003",
|
||||
"14525:4004",
|
||||
"14525:9001"
|
||||
]
|
||||
},
|
||||
"lastUpdate": {
|
||||
"epoch": 1588417118,
|
||||
"string": "Sat May 2 10:58:38 2020\n"
|
||||
},
|
||||
"nexthops": [
|
||||
{
|
||||
"ip": "199.34.92.7",
|
||||
"afi": "ipv4",
|
||||
"metric": 1100,
|
||||
"accessible": true,
|
||||
"used": true
|
||||
}
|
||||
],
|
||||
"peer": {
|
||||
"peerId": "199.34.92.7",
|
||||
"routerId": "199.34.92.7",
|
||||
"hostname": "er01.dtn01",
|
||||
"type": "internal"
|
||||
}
|
||||
},
|
||||
{
|
||||
"aspath": {
|
||||
"string": "1299 13335",
|
||||
"segments": [
|
||||
{
|
||||
"type": "as-sequence",
|
||||
"list": [1299, 13335]
|
||||
}
|
||||
],
|
||||
"length": 2
|
||||
},
|
||||
"aggregatorAs": 13335,
|
||||
"aggregatorId": "162.158.140.1",
|
||||
"origin": "IGP",
|
||||
"med": 0,
|
||||
"metric": 0,
|
||||
"localpref": 150,
|
||||
"weight": 200,
|
||||
"valid": true,
|
||||
"bestpath": {
|
||||
"bestpathFromAs": 1299
|
||||
},
|
||||
"community": {
|
||||
"string": "1299:35000 14525:0 14525:40 14525:1021 14525:2840 14525:3001 14525:4001 14525:9003",
|
||||
"list": [
|
||||
"1299:35000",
|
||||
"14525:0",
|
||||
"14525:40",
|
||||
"14525:1021",
|
||||
"14525:2840",
|
||||
"14525:3001",
|
||||
"14525:4001",
|
||||
"14525:9003"
|
||||
]
|
||||
},
|
||||
"lastUpdate": {
|
||||
"epoch": 1588584980,
|
||||
"string": "Mon May 4 09:36:20 2020\n"
|
||||
},
|
||||
"nexthops": [
|
||||
{
|
||||
"ip": "199.34.92.9",
|
||||
"afi": "ipv4",
|
||||
"metric": 200,
|
||||
"accessible": true,
|
||||
"used": true
|
||||
}
|
||||
],
|
||||
"peer": {
|
||||
"peerId": "199.34.92.9",
|
||||
"routerId": "199.34.92.9",
|
||||
"type": "internal"
|
||||
}
|
||||
},
|
||||
{
|
||||
"aspath": {
|
||||
"string": "6939 13335",
|
||||
"segments": [
|
||||
{
|
||||
"type": "as-sequence",
|
||||
"list": [6939, 13335]
|
||||
}
|
||||
],
|
||||
"length": 2
|
||||
},
|
||||
"aggregatorAs": 13335,
|
||||
"aggregatorId": "172.68.129.1",
|
||||
"origin": "IGP",
|
||||
"med": 0,
|
||||
"metric": 0,
|
||||
"localpref": 100,
|
||||
"weight": 100,
|
||||
"valid": true,
|
||||
"bestpath": {
|
||||
"bestpathFromAs": 6939
|
||||
},
|
||||
"community": {
|
||||
"string": "6939:7107 6939:8840 6939:9001 14525:0 14525:40 14525:1021 14525:2840 14525:3002 14525:4003 14525:9002",
|
||||
"list": [
|
||||
"6939:7107",
|
||||
"6939:8840",
|
||||
"6939:9001",
|
||||
"14525:0",
|
||||
"14525:40",
|
||||
"14525:1021",
|
||||
"14525:2840",
|
||||
"14525:3002",
|
||||
"14525:4003",
|
||||
"14525:9002"
|
||||
]
|
||||
},
|
||||
"lastUpdate": {
|
||||
"epoch": 1586990260,
|
||||
"string": "Wed Apr 15 22:37:40 2020\n"
|
||||
},
|
||||
"nexthops": [
|
||||
{
|
||||
"ip": "199.34.92.6",
|
||||
"afi": "ipv4",
|
||||
"metric": 151,
|
||||
"accessible": true,
|
||||
"used": true
|
||||
}
|
||||
],
|
||||
"peer": {
|
||||
"peerId": "199.34.92.6",
|
||||
"routerId": "199.34.92.6",
|
||||
"type": "internal"
|
||||
}
|
||||
},
|
||||
{
|
||||
"aspath": {
|
||||
"string": "174 13335",
|
||||
"segments": [
|
||||
{
|
||||
"type": "as-sequence",
|
||||
"list": [174, 13335]
|
||||
}
|
||||
],
|
||||
"length": 2
|
||||
},
|
||||
"aggregatorAs": 13335,
|
||||
"aggregatorId": "162.158.140.1",
|
||||
"origin": "IGP",
|
||||
"med": 2020,
|
||||
"metric": 2020,
|
||||
"localpref": 150,
|
||||
"weight": 200,
|
||||
"valid": true,
|
||||
"bestpath": {
|
||||
"bestpathFromAs": 174,
|
||||
"overall": true
|
||||
},
|
||||
"community": {
|
||||
"string": "174:21001 174:22013 14525:0 14525:20 14525:1021 14525:2840 14525:3001 14525:4001 14525:9001",
|
||||
"list": [
|
||||
"174:21001",
|
||||
"174:22013",
|
||||
"14525:0",
|
||||
"14525:20",
|
||||
"14525:1021",
|
||||
"14525:2840",
|
||||
"14525:3001",
|
||||
"14525:4001",
|
||||
"14525:9001"
|
||||
]
|
||||
},
|
||||
"lastUpdate": {
|
||||
"epoch": 1588584997,
|
||||
"string": "Mon May 4 09:36:37 2020\n"
|
||||
},
|
||||
"nexthops": [
|
||||
{
|
||||
"ip": "199.34.92.1",
|
||||
"afi": "ipv4",
|
||||
"metric": 101,
|
||||
"accessible": true,
|
||||
"used": true
|
||||
}
|
||||
],
|
||||
"peer": {
|
||||
"peerId": "199.34.92.1",
|
||||
"routerId": "199.34.92.1",
|
||||
"type": "internal"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@@ -121,7 +121,7 @@ class JuniperRoute(_JuniperBase):
|
||||
total_route_count: int
|
||||
active_route_count: int
|
||||
hidden_route_count: int
|
||||
rt: JuniperRouteTable
|
||||
rt: List[JuniperRouteTable]
|
||||
|
||||
def serialize(self):
|
||||
"""Convert the Juniper-specific fields to standard parsed data model."""
|
||||
@@ -131,21 +131,17 @@ class JuniperRoute(_JuniperBase):
|
||||
else:
|
||||
vrf = vrf_parts[0]
|
||||
|
||||
prefix = "/".join(
|
||||
str(i) for i in (self.rt.rt_destination, self.rt.rt_prefix_length)
|
||||
)
|
||||
|
||||
structure = {
|
||||
"vrf": vrf,
|
||||
"prefix": prefix,
|
||||
"count": self.rt.rt_entry_count,
|
||||
"winning_weight": "low",
|
||||
}
|
||||
|
||||
routes = []
|
||||
for route in self.rt.rt_entry:
|
||||
count = 0
|
||||
for table in self.rt:
|
||||
count += table.rt_entry_count
|
||||
prefix = "/".join(
|
||||
str(i) for i in (table.rt_destination, table.rt_prefix_length)
|
||||
)
|
||||
for route in table.rt_entry:
|
||||
routes.append(
|
||||
{
|
||||
"prefix": prefix,
|
||||
"active": route.active_tag,
|
||||
"age": route.age,
|
||||
"weight": route.preference,
|
||||
@@ -161,7 +157,9 @@ class JuniperRoute(_JuniperBase):
|
||||
}
|
||||
)
|
||||
|
||||
serialized = ParsedRoutes(routes=routes, **structure)
|
||||
serialized = ParsedRoutes(
|
||||
vrf=vrf, count=count, routes=routes, winning_weight="low",
|
||||
)
|
||||
|
||||
log.info("Serialized Juniper response: {}", serialized)
|
||||
return serialized
|
||||
|
@@ -13,6 +13,7 @@ from hyperglass.models import HyperglassModel
|
||||
class ParsedRouteEntry(HyperglassModel):
|
||||
"""Per-Route Response Model."""
|
||||
|
||||
prefix: StrictStr
|
||||
active: StrictBool
|
||||
age: StrictInt
|
||||
weight: StrictInt
|
||||
@@ -31,7 +32,6 @@ class ParsedRoutes(HyperglassModel):
|
||||
"""Parsed Response Model."""
|
||||
|
||||
vrf: StrictStr
|
||||
prefix: StrictStr
|
||||
count: StrictInt = 0
|
||||
routes: List[ParsedRouteEntry]
|
||||
winning_weight: constr(regex=r"(low|high)")
|
||||
|
10
hyperglass/parsing/nos.py
Normal file
10
hyperglass/parsing/nos.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# Project
|
||||
from hyperglass.parsing.juniper import parse_juniper
|
||||
|
||||
nos_parsers = {
|
||||
"juniper": {
|
||||
"bgp_route": parse_juniper,
|
||||
"bgp_aspath": parse_juniper,
|
||||
"bgp_community": parse_juniper,
|
||||
}
|
||||
}
|
251
hyperglass/ui/components/BGPTable.js
Normal file
251
hyperglass/ui/components/BGPTable.js
Normal file
@@ -0,0 +1,251 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
Flex,
|
||||
Icon,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Text,
|
||||
Tooltip,
|
||||
useColorMode
|
||||
} from "@chakra-ui/core";
|
||||
import { MdLastPage } from "react-icons/md";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTimePlugin from "dayjs/plugin/relativeTime";
|
||||
import utcPlugin from "dayjs/plugin/utc";
|
||||
import useConfig from "~/components/HyperglassProvider";
|
||||
import Table from "~/components/Table/index";
|
||||
|
||||
dayjs.extend(relativeTimePlugin);
|
||||
dayjs.extend(utcPlugin);
|
||||
|
||||
const isActiveColor = {
|
||||
true: { dark: "green.300", light: "green.500" },
|
||||
false: { dark: "gray.300", light: "gray.500" }
|
||||
};
|
||||
|
||||
const arrowColor = {
|
||||
true: { dark: "blackAlpha.500", light: "blackAlpha.500" },
|
||||
false: { dark: "whiteAlpha.300", light: "blackAlpha.500" }
|
||||
};
|
||||
|
||||
const rpkiIcon = ["not-allowed", "check-circle", "warning", "question"];
|
||||
|
||||
const rpkiColor = {
|
||||
true: {
|
||||
dark: ["red.500", "green.600", "yellow.500", "gray.800"],
|
||||
light: ["red.500", "green.500", "yellow.500", "gray.600"]
|
||||
},
|
||||
false: {
|
||||
dark: ["red.300", "green.300", "yellow.300", "gray.300"],
|
||||
light: ["red.400", "green.500", "yellow.400", "gray.500"]
|
||||
}
|
||||
};
|
||||
|
||||
const makeColumns = fields => {
|
||||
return fields.map(pair => {
|
||||
const [header, accessor, align] = pair;
|
||||
let columnConfig = {
|
||||
Header: header,
|
||||
accessor: accessor,
|
||||
align: align,
|
||||
hidden: false
|
||||
};
|
||||
if (align === null) {
|
||||
columnConfig.hidden = true;
|
||||
}
|
||||
return columnConfig;
|
||||
});
|
||||
};
|
||||
|
||||
// const longestASNLength = asPath => {
|
||||
// if (asPath.length === 0) {
|
||||
// return 0
|
||||
// }
|
||||
// const longest = asPath.reduce((l, c) => {
|
||||
// const strLongest = String(l);
|
||||
// const strCurrent = String(c);
|
||||
// return strCurrent.length > strLongest.length ? strCurrent : strLongest;
|
||||
// });
|
||||
// return longest.length;
|
||||
// };
|
||||
|
||||
const MonoField = ({ v, ...props }) => (
|
||||
<Text fontSize="sm" fontFamily="mono" {...props}>
|
||||
{v}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const Active = ({ isActive }) => {
|
||||
const { colorMode } = useColorMode();
|
||||
return (
|
||||
<Icon
|
||||
name={isActive ? "check-circle" : "warning"}
|
||||
color={isActiveColor[isActive][colorMode]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Age = ({ inSeconds }) => {
|
||||
const now = dayjs.utc();
|
||||
const then = now.subtract(inSeconds, "seconds");
|
||||
return (
|
||||
<Tooltip
|
||||
hasArrow
|
||||
label={then.toString().replace("GMT", "UTC")}
|
||||
placement="right"
|
||||
>
|
||||
<Text fontSize="sm">{now.to(then, true)}</Text>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const Weight = ({ weight, winningWeight }) => {
|
||||
const fixMeText =
|
||||
winningWeight === "low"
|
||||
? "Lower Weight is Preferred"
|
||||
: "Higher Weight is Preferred";
|
||||
return (
|
||||
<Tooltip hasArrow label={fixMeText} placement="right">
|
||||
<Text fontSize="sm" fontFamily="mono">
|
||||
{weight}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const ASPath = ({ path, active }) => {
|
||||
const { colorMode } = useColorMode();
|
||||
if (path.length === 0) {
|
||||
return <Icon as={MdLastPage} />;
|
||||
}
|
||||
let paths = [];
|
||||
path.map((asn, i) => {
|
||||
const asnStr = String(asn);
|
||||
i !== 0 &&
|
||||
paths.push(
|
||||
<Icon
|
||||
name="chevron-right"
|
||||
key={`separator-${i}`}
|
||||
color={arrowColor[active][colorMode]}
|
||||
/>
|
||||
);
|
||||
paths.push(
|
||||
<Text
|
||||
fontSize="sm"
|
||||
as="span"
|
||||
whiteSpace="pre"
|
||||
fontFamily="mono"
|
||||
key={`as-${asnStr}-${i}`}
|
||||
>
|
||||
{asnStr}
|
||||
</Text>
|
||||
);
|
||||
});
|
||||
return paths;
|
||||
};
|
||||
|
||||
const Communities = ({ communities }) => {
|
||||
const { colorMode } = useColorMode();
|
||||
let component;
|
||||
communities.length === 0
|
||||
? (component = (
|
||||
<Tooltip placement="right" hasArrow label="No Communities">
|
||||
<Icon name="question-outline" />
|
||||
</Tooltip>
|
||||
))
|
||||
: (component = (
|
||||
<Popover trigger="hover" placement="right">
|
||||
<PopoverTrigger>
|
||||
<Icon name="view" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
textAlign="left"
|
||||
p={4}
|
||||
maxW="fit-content"
|
||||
color={colorMode === "dark" ? "white" : "black"}
|
||||
>
|
||||
<PopoverArrow />
|
||||
{communities.map(c => (
|
||||
<MonoField fontWeight="normal" v={c} key={c.replace(":", "-")} />
|
||||
))}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
));
|
||||
return component;
|
||||
};
|
||||
|
||||
const RPKIState = ({ state, active }) => {
|
||||
const { web } = useConfig();
|
||||
const { colorMode } = useColorMode();
|
||||
const stateText = [
|
||||
web.text.rpki_invalid,
|
||||
web.text.rpki_valid,
|
||||
web.text.rpki_unknown,
|
||||
web.text.rpki_unverified
|
||||
];
|
||||
return (
|
||||
<Tooltip
|
||||
hasArrow
|
||||
placement="right"
|
||||
label={stateText[state] ?? stateText[3]}
|
||||
>
|
||||
<Icon
|
||||
name={rpkiIcon[state]}
|
||||
color={rpkiColor[active][colorMode][state]}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const Cell = ({ data, rawData, longestASN }) => {
|
||||
const component = {
|
||||
prefix: <MonoField v={data.value} />,
|
||||
active: <Active isActive={data.value} />,
|
||||
age: <Age inSeconds={data.value} />,
|
||||
weight: (
|
||||
<Weight weight={data.value} winningWeight={rawData.winning_weight} />
|
||||
),
|
||||
med: <MonoField v={data.value} />,
|
||||
local_preference: <MonoField v={data.value} />,
|
||||
as_path: (
|
||||
<ASPath
|
||||
path={data.value}
|
||||
active={data.row.values.active}
|
||||
longestASN={longestASN}
|
||||
/>
|
||||
),
|
||||
communities: <Communities communities={data.value} />,
|
||||
next_hop: <MonoField v={data.value} />,
|
||||
source_as: <MonoField v={data.value} />,
|
||||
source_rid: <MonoField v={data.value} />,
|
||||
peer_rid: <MonoField v={data.value} />,
|
||||
rpki_state: <RPKIState state={data.value} active={data.row.values.active} />
|
||||
};
|
||||
return component[data.column.id] ?? <> </>;
|
||||
};
|
||||
|
||||
const BGPTable = ({ children: data, ...props }) => {
|
||||
const config = useConfig();
|
||||
const columns = makeColumns(config.parsed_data_fields);
|
||||
// const allASN = data.routes.map(r => r.as_path).flat();
|
||||
// const asLength = longestASNLength(allASN);
|
||||
|
||||
return (
|
||||
<Flex my={8} maxW={["100%", "100%", "100%", "100%"]} w="100%" {...props}>
|
||||
<Table
|
||||
columns={columns}
|
||||
data={data.routes}
|
||||
rowHighlightProp="active"
|
||||
cellRender={d => <Cell data={d} rawData={data} />}
|
||||
bordersHorizontal
|
||||
rowHighlightBg="green"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
BGPTable.displayName = "BGPTable";
|
||||
|
||||
export default BGPTable;
|
@@ -1,4 +1,5 @@
|
||||
import * as React from "react";
|
||||
/** @jsx jsx */
|
||||
import { jsx } from "@emotion/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
AccordionItem,
|
||||
@@ -11,7 +12,8 @@ import {
|
||||
Flex,
|
||||
Tooltip,
|
||||
Text,
|
||||
useColorMode
|
||||
useColorMode,
|
||||
useTheme
|
||||
} from "@chakra-ui/core";
|
||||
import styled from "@emotion/styled";
|
||||
import LightningBolt from "~/components/icons/LightningBolt";
|
||||
@@ -25,6 +27,8 @@ import CopyButton from "~/components/CopyButton";
|
||||
import RequeryButton from "~/components/RequeryButton";
|
||||
import ResultHeader from "~/components/ResultHeader";
|
||||
import CacheTimeout from "~/components/CacheTimeout";
|
||||
import BGPTable from "~/components/BGPTable";
|
||||
import TextOutput from "~/components/TextOutput";
|
||||
|
||||
format.extend(String.prototype, {});
|
||||
|
||||
@@ -54,16 +58,25 @@ const AccordionHeaderWrapper = styled(Flex)`
|
||||
}
|
||||
`;
|
||||
|
||||
const structuredDataComponent = {
|
||||
bgp_route: BGPTable,
|
||||
bgp_aspath: BGPTable,
|
||||
bgp_community: BGPTable,
|
||||
ping: TextOutput,
|
||||
traceroute: TextOutput
|
||||
};
|
||||
|
||||
const statusMap = {
|
||||
success: "success",
|
||||
warning: "warning",
|
||||
error: "warning",
|
||||
danger: "error"
|
||||
};
|
||||
const bg = { dark: "gray.800", light: "blackAlpha.100" };
|
||||
|
||||
const color = { dark: "white", light: "black" };
|
||||
const selectionBg = { dark: "white", light: "black" };
|
||||
const selectionColor = { dark: "black", light: "white" };
|
||||
const scrollbar = { dark: "whiteAlpha.300", light: "blackAlpha.300" };
|
||||
const scrollbarHover = { dark: "whiteAlpha.400", light: "blackAlpha.400" };
|
||||
const scrollbarBg = { dark: "whiteAlpha.50", light: "blackAlpha.50" };
|
||||
|
||||
const Result = React.forwardRef(
|
||||
(
|
||||
@@ -81,6 +94,7 @@ const Result = React.forwardRef(
|
||||
ref
|
||||
) => {
|
||||
const config = useConfig();
|
||||
const theme = useTheme();
|
||||
const { isSm } = useMedia();
|
||||
const { colorMode } = useColorMode();
|
||||
let [{ data, loading, error }, refetch] = useAxios({
|
||||
@@ -103,12 +117,6 @@ const Result = React.forwardRef(
|
||||
setOpen(!isOpen);
|
||||
setOverride(true);
|
||||
};
|
||||
let cleanOutput =
|
||||
data &&
|
||||
data.output
|
||||
.split("\\n")
|
||||
.join("\n")
|
||||
.replace(/\n\n/g, "\n");
|
||||
|
||||
const errorKw = (error && error.response?.data?.keywords) || [];
|
||||
|
||||
@@ -132,6 +140,11 @@ const Result = React.forwardRef(
|
||||
statusMap[error.response?.data?.level]) ??
|
||||
"error";
|
||||
|
||||
let Output = TextOutput;
|
||||
if (data?.format === "application/json") {
|
||||
Output = structuredDataComponent[queryType];
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
!loading && resultsComplete === null && setComplete(index);
|
||||
}, [loading, resultsComplete]);
|
||||
@@ -148,7 +161,7 @@ const Result = React.forwardRef(
|
||||
css={css({
|
||||
"&:last-of-type": { borderBottom: "none" },
|
||||
"&:first-of-type": { borderTop: "none" }
|
||||
})}
|
||||
})(theme)}
|
||||
>
|
||||
<AccordionHeaderWrapper hoverBg="blackAlpha.50">
|
||||
<AccordionHeader
|
||||
@@ -168,9 +181,9 @@ const Result = React.forwardRef(
|
||||
runtime={data?.runtime}
|
||||
/>
|
||||
</AccordionHeader>
|
||||
<ButtonGroup px={3} py={2}>
|
||||
<ButtonGroup px={[1, 1, 3, 3]} py={2}>
|
||||
<CopyButton
|
||||
copyValue={cleanOutput}
|
||||
copyValue={data?.output}
|
||||
variant="ghost"
|
||||
isDisabled={loading}
|
||||
/>
|
||||
@@ -184,7 +197,21 @@ const Result = React.forwardRef(
|
||||
<AccordionPanel
|
||||
pb={4}
|
||||
overflowX="auto"
|
||||
css={css({ WebkitOverflowScrolling: "touch" })}
|
||||
css={css({
|
||||
WebkitOverflowScrolling: "touch",
|
||||
"&::-webkit-scrollbar": { height: "5px" },
|
||||
"&::-webkit-scrollbar-track": {
|
||||
backgroundColor: scrollbarBg[colorMode]
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: scrollbar[colorMode]
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb:hover": {
|
||||
backgroundColor: scrollbarHover[colorMode]
|
||||
},
|
||||
|
||||
"-ms-overflow-style": { display: "none" }
|
||||
})(theme)}
|
||||
>
|
||||
<Flex direction="row" flexWrap="wrap">
|
||||
<Flex
|
||||
@@ -192,30 +219,7 @@ const Result = React.forwardRef(
|
||||
flex="1 0 auto"
|
||||
maxW={error ? "100%" : null}
|
||||
>
|
||||
{data && !error && (
|
||||
<Box
|
||||
fontFamily="mono"
|
||||
mt={5}
|
||||
mx={2}
|
||||
p={3}
|
||||
border="1px"
|
||||
borderColor="inherit"
|
||||
rounded="md"
|
||||
bg={bg[colorMode]}
|
||||
color={color[colorMode]}
|
||||
fontSize="sm"
|
||||
whiteSpace="pre-wrap"
|
||||
as="pre"
|
||||
css={css({
|
||||
"&::selection": {
|
||||
backgroundColor: selectionBg[colorMode],
|
||||
color: selectionColor[colorMode]
|
||||
}
|
||||
})}
|
||||
>
|
||||
{cleanOutput}
|
||||
</Box>
|
||||
)}
|
||||
{!error && data && <Output>{data?.output}</Output>}
|
||||
{error && (
|
||||
<Alert rounded="lg" my={2} py={4} status={errorLevel}>
|
||||
<FormattedError keywords={errorKw} message={errorMsg} />
|
||||
@@ -264,45 +268,6 @@ const Result = React.forwardRef(
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* {config.cache.show_text && data && !error && isSm ? (
|
||||
<>
|
||||
<Tooltip
|
||||
display={data?.cached ? null : "none"}
|
||||
hasArrow
|
||||
label={config.web.text.cache_icon.format({
|
||||
time: data?.timestamp
|
||||
})}
|
||||
placement="top"
|
||||
>
|
||||
<Box mr={1} display={data?.cached ? "block" : "none"}>
|
||||
<LightningBolt color={color[colorMode]} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<CacheTimeout
|
||||
timeout={config.cache.timeout}
|
||||
text={config.web.text.cache_prefix}
|
||||
/>
|
||||
</>
|
||||
) : config.cache.show_text && data && !error ? (
|
||||
<>
|
||||
<CacheTimeout
|
||||
timeout={config.cache.timeout}
|
||||
text={config.web.text.cache_prefix}
|
||||
/>
|
||||
<Tooltip
|
||||
display={data?.cached ? null : "none"}
|
||||
hasArrow
|
||||
label={config.web.text.cache_icon.format({
|
||||
time: data?.timestamp
|
||||
})}
|
||||
placement="top"
|
||||
>
|
||||
<Box ml={1} display={data?.cached ? "block" : "none"}>
|
||||
<LightningBolt color={color[colorMode]} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</>
|
||||
) : null} */}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</AccordionPanel>
|
||||
|
@@ -60,7 +60,6 @@ const Table = ({
|
||||
nextPage,
|
||||
previousPage,
|
||||
setPageSize,
|
||||
setHiddenColumns,
|
||||
state: { pageIndex, pageSize }
|
||||
} = useTable(
|
||||
{
|
||||
|
44
hyperglass/ui/components/TextOutput.js
Normal file
44
hyperglass/ui/components/TextOutput.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/** @jsx jsx */
|
||||
import { jsx } from "@emotion/core";
|
||||
import { Box, css, useColorMode } from "@chakra-ui/core";
|
||||
|
||||
const bg = { dark: "gray.800", light: "blackAlpha.100" };
|
||||
const color = { dark: "white", light: "black" };
|
||||
const selectionBg = { dark: "white", light: "black" };
|
||||
const selectionColor = { dark: "black", light: "white" };
|
||||
|
||||
const TextOutput = ({ children, ...props }) => {
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
return (
|
||||
<Box
|
||||
fontFamily="mono"
|
||||
mt={5}
|
||||
mx={2}
|
||||
p={3}
|
||||
border="1px"
|
||||
borderColor="inherit"
|
||||
rounded="md"
|
||||
bg={bg[colorMode]}
|
||||
color={color[colorMode]}
|
||||
fontSize="sm"
|
||||
whiteSpace="pre-wrap"
|
||||
as="pre"
|
||||
css={css({
|
||||
"&::selection": {
|
||||
backgroundColor: selectionBg[colorMode],
|
||||
color: selectionColor[colorMode]
|
||||
}
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
{children
|
||||
.split("\\n")
|
||||
.join("\n")
|
||||
.replace(/\n\n/g, "\n")}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
TextOutput.displayName = "TextOutput";
|
||||
export default TextOutput;
|
Reference in New Issue
Block a user