mirror of
https://github.com/checktheroads/hyperglass
synced 2024-05-11 05:55:08 +00:00
add VRF support
This commit is contained in:
@@ -29,6 +29,7 @@ class Construct:
|
||||
self.transport = transport
|
||||
self.query_target = self.query_data["query_target"]
|
||||
self.query_vrf = self.query_data["query_vrf"]
|
||||
self.cmd_type = self.get_cmd_type(self.query_target, self.query_vrf)
|
||||
|
||||
@staticmethod
|
||||
def get_src(device, afi):
|
||||
@@ -51,17 +52,17 @@ class Construct:
|
||||
return operator.attrgetter(cmd_path)(commands)
|
||||
|
||||
@staticmethod
|
||||
def query_afi(query_target, query_vrf):
|
||||
def get_cmd_type(query_target, query_vrf):
|
||||
"""
|
||||
Constructs AFI string. If query_vrf is specified, AFI prefix is
|
||||
"vpnv", if not, AFI prefix is "ipv"
|
||||
"""
|
||||
protocol = ipaddress.ip_network(query_target).version
|
||||
if query_vrf and query_vrf != "default":
|
||||
afi = f"ipv{protocol}_vpn"
|
||||
cmd_type = f"ipv{protocol}_vrf"
|
||||
else:
|
||||
afi = f"ipv{protocol}"
|
||||
return afi
|
||||
cmd_type = f"ipv{protocol}_default"
|
||||
return cmd_type
|
||||
|
||||
def ping(self):
|
||||
"""Constructs ping query parameters from pre-validated input"""
|
||||
@@ -71,32 +72,31 @@ class Construct:
|
||||
)
|
||||
|
||||
query = []
|
||||
|
||||
query_afi = self.query_afi(self.query_target, self.query_vrf)
|
||||
afi = getattr(self.device.afis, query_afi)
|
||||
vrf = self.device.vrfs[self.device.vrfs.index(self.query_vrf)]
|
||||
|
||||
# TODO: AFI to VRF mapping still needs work. Possible solution:
|
||||
# move AFI model to be a direct child of a VRF. Each VRF can define an
|
||||
# ipv4 or ipv6 family. Determine AFI of query target, get source/label
|
||||
# as device -> vrfs -> afi[family] -> source/label
|
||||
query_protocol = f"ipv{ipaddress.ip_network(self.query_target).version}"
|
||||
vrf = getattr(self.device.vrfs, self.query_vrf)
|
||||
afi = getattr(vrf, query_protocol)
|
||||
|
||||
if self.transport == "rest":
|
||||
query.append(
|
||||
json.dumps(
|
||||
{
|
||||
"query_type": "ping",
|
||||
"afi": afi.label,
|
||||
"vrf": vrf,
|
||||
"source": afi.source,
|
||||
"afi": afi.afi_name,
|
||||
"vrf": afi.vrf_name,
|
||||
"source": afi.source_address,
|
||||
"target": self.query_target,
|
||||
}
|
||||
)
|
||||
)
|
||||
elif self.transport == "scrape":
|
||||
cmd = self.device_commands(self.device.commands, afi.label, "ping")
|
||||
cmd = self.device_commands(self.device.commands, self.cmd_type, "ping")
|
||||
query.append(
|
||||
cmd.format(target=self.query_target, source=afi.source, vrf=vrf)
|
||||
cmd.format(
|
||||
target=self.query_target,
|
||||
source=afi.source_address,
|
||||
vrf=afi.vrf_name,
|
||||
afi=afi.afi_name,
|
||||
)
|
||||
)
|
||||
|
||||
log.debug(f"Constructed query: {query}")
|
||||
@@ -113,29 +113,38 @@ class Construct:
|
||||
)
|
||||
)
|
||||
|
||||
query = None
|
||||
afi = self.query_afi(self.query_target, self.query_vrf)
|
||||
source = self.get_src(self.device, afi)
|
||||
query = []
|
||||
query_protocol = f"ipv{ipaddress.ip_network(self.query_target).version}"
|
||||
vrf = getattr(self.device.vrfs, self.query_vrf)
|
||||
afi = getattr(vrf, query_protocol)
|
||||
|
||||
if self.transport == "rest":
|
||||
query = json.dumps(
|
||||
query.append(
|
||||
json.dumps(
|
||||
{
|
||||
"query_type": "traceroute",
|
||||
"afi": afi,
|
||||
"vrf": self.query_vrf,
|
||||
"source": source,
|
||||
"afi": afi.afi_name,
|
||||
"vrf": afi.vrf_name,
|
||||
"source": afi.source_address,
|
||||
"target": self.query_target,
|
||||
}
|
||||
)
|
||||
)
|
||||
elif self.transport == "scrape":
|
||||
cmd = self.device_commands(self.device.commands, afi, "traceroute")
|
||||
query = cmd.format(
|
||||
target=self.query_target, source=source, vrf=self.query_vrf
|
||||
cmd = self.device_commands(
|
||||
self.device.commands, self.cmd_type, "traceroute"
|
||||
)
|
||||
query.append(
|
||||
cmd.format(
|
||||
target=self.query_target,
|
||||
source=afi.source_address,
|
||||
vrf=afi.vrf_name,
|
||||
afi=afi.afi_name,
|
||||
)
|
||||
)
|
||||
|
||||
log.debug(f"Constructed query: {query}")
|
||||
|
||||
return [query]
|
||||
return query
|
||||
|
||||
def bgp_route(self):
|
||||
"""
|
||||
@@ -145,29 +154,36 @@ class Construct:
|
||||
f"Constructing bgp_route query for {self.query_target} via {self.transport}"
|
||||
)
|
||||
|
||||
query = None
|
||||
afi = Construct.query_afi(self.query_target, self.query_vrf)
|
||||
source = self.get_src(self.device, afi)
|
||||
query = []
|
||||
query_protocol = f"ipv{ipaddress.ip_network(self.query_target).version}"
|
||||
vrf = getattr(self.device.vrfs, self.query_vrf)
|
||||
afi = getattr(vrf, query_protocol)
|
||||
|
||||
if self.transport == "rest":
|
||||
query = json.dumps(
|
||||
query.append(
|
||||
json.dumps(
|
||||
{
|
||||
"query_type": "bgp_route",
|
||||
"afi": afi,
|
||||
"vrf": self.query_vrf,
|
||||
"source": source,
|
||||
"afi": afi.afi_name,
|
||||
"vrf": afi.vrf_name,
|
||||
"source": afi.source_address,
|
||||
"target": self.query_target,
|
||||
}
|
||||
)
|
||||
)
|
||||
elif self.transport == "scrape":
|
||||
cmd = self.device_commands(self.device.commands, afi, "bgp_route")
|
||||
query = cmd.format(
|
||||
target=self.query_target, source=source, afi=afi, vrf=self.query_vrf
|
||||
cmd = self.device_commands(self.device.commands, self.cmd_type, "bgp_route")
|
||||
query.append(
|
||||
cmd.format(
|
||||
target=self.query_target,
|
||||
source=afi.source_address,
|
||||
vrf=afi.vrf_name,
|
||||
afi=afi.afi_name,
|
||||
)
|
||||
)
|
||||
|
||||
log.debug(f"Constructed query: {query}")
|
||||
|
||||
return [query]
|
||||
return query
|
||||
|
||||
def bgp_community(self):
|
||||
"""
|
||||
@@ -176,34 +192,45 @@ class Construct:
|
||||
"""
|
||||
log.debug(
|
||||
(
|
||||
f"Constructing bgp_community query for {self.query_target} "
|
||||
f"via {self.transport}"
|
||||
f"Constructing bgp_community query for "
|
||||
f"{self.query_target} via {self.transport}"
|
||||
)
|
||||
)
|
||||
|
||||
query = None
|
||||
afi = self.query_afi(self.query_target, self.query_vrf)
|
||||
log.debug(afi)
|
||||
source = self.get_src(self.device, afi)
|
||||
query = []
|
||||
|
||||
vrf = getattr(self.device.vrfs, self.query_vrf)
|
||||
afi = getattr(vrf, self.query_afi)
|
||||
|
||||
# TODO: Reimplement "dual" concept?
|
||||
# ValueError: '14525:5001' does not appear to be an IPv4 or IPv6 network
|
||||
|
||||
if self.transport == "rest":
|
||||
query = json.dumps(
|
||||
query.append(
|
||||
json.dumps(
|
||||
{
|
||||
"query_type": "bgp_community",
|
||||
"afi": afi,
|
||||
"vrf": self.query_vrf,
|
||||
"source": source,
|
||||
"afi": afi.afi_name,
|
||||
"vrf": afi.vrf_name,
|
||||
"source": afi.source_address,
|
||||
"target": self.query_target,
|
||||
}
|
||||
)
|
||||
)
|
||||
elif self.transport == "scrape":
|
||||
cmd = self.device_commands(self.device.commands, afi, "bgp_community")
|
||||
query = cmd.format(
|
||||
target=self.query_target, source=source, vrf=self.query_vrf
|
||||
cmd = self.device_commands(
|
||||
self.device.commands, self.cmd_type, "bgp_community"
|
||||
)
|
||||
query.append(
|
||||
cmd.format(
|
||||
target=self.query_target,
|
||||
source=afi.source_address,
|
||||
vrf=afi.vrf_name,
|
||||
afi=afi.afi_name,
|
||||
)
|
||||
)
|
||||
|
||||
log.debug(f"Constructed query: {query}")
|
||||
|
||||
return query
|
||||
|
||||
def bgp_aspath(self):
|
||||
|
@@ -323,7 +323,7 @@ class Execute:
|
||||
log.debug(f"Matched device config: {device_config}")
|
||||
|
||||
# Run query parameters through validity checks
|
||||
validation = Validate(device_config, self.query_type, self.query_target)
|
||||
validation = Validate(device_config, self.query_data, self.query_target)
|
||||
valid_input = validation.validate_query()
|
||||
if valid_input:
|
||||
log.debug(f"Validation passed for query: {self.query_data}")
|
||||
|
@@ -5,6 +5,7 @@ error message.
|
||||
"""
|
||||
# Standard Library Imports
|
||||
import ipaddress
|
||||
import operator
|
||||
import re
|
||||
|
||||
# Third Party Imports
|
||||
@@ -13,6 +14,7 @@ from logzero import logger as log
|
||||
# Project Imports
|
||||
from hyperglass.configuration import logzero_config # noqa: F401
|
||||
from hyperglass.configuration import params
|
||||
from hyperglass.configuration import vrfs
|
||||
from hyperglass.exceptions import InputInvalid, InputNotAllowed
|
||||
|
||||
|
||||
@@ -96,36 +98,57 @@ def ip_validate(target):
|
||||
return valid_ip
|
||||
|
||||
|
||||
def ip_blacklist(target):
|
||||
def ip_access_list(query_data):
|
||||
"""
|
||||
Check blacklist list for prefixes/IPs, return boolean based on list
|
||||
membership.
|
||||
Check VRF access list for matching prefixes, returns an error if a
|
||||
match is found.
|
||||
"""
|
||||
log.debug(f"Blacklist Enabled: {params.features.blacklist.enable}")
|
||||
target = ipaddress.ip_network(target)
|
||||
if params.features.blacklist.enable:
|
||||
target_ver = target.version
|
||||
user_blacklist = params.features.blacklist.networks
|
||||
networks = [
|
||||
net
|
||||
for net in user_blacklist
|
||||
if ipaddress.ip_network(net).version == target_ver
|
||||
]
|
||||
log.debug(
|
||||
f"IPv{target_ver} Blacklist Networks: {[str(net) for net in networks]}"
|
||||
)
|
||||
for net in networks:
|
||||
blacklist_net = ipaddress.ip_network(net)
|
||||
log.debug(f'Checking Access List for: {query_data["query_target"]}')
|
||||
|
||||
def member_of(target, network):
|
||||
"""
|
||||
Returns boolean if an input target IP is a member of an input
|
||||
network.
|
||||
"""
|
||||
log.debug(f"Checking membership of {target} for {network}")
|
||||
|
||||
membership = False
|
||||
if (
|
||||
blacklist_net.network_address <= target.network_address
|
||||
and blacklist_net.broadcast_address >= target.broadcast_address
|
||||
network.network_address <= target.network_address
|
||||
and network.broadcast_address >= target.broadcast_address # NOQA: W503
|
||||
):
|
||||
log.debug(f"Blacklist Match Found for {target} in {str(net)}")
|
||||
_exception = ValueError(params.messages.blacklist)
|
||||
_exception.details = {"blacklisted_net": str(net)}
|
||||
raise _exception
|
||||
log.debug(f"{target} is a member of {network}")
|
||||
membership = True
|
||||
return membership
|
||||
|
||||
target = ipaddress.ip_network(query_data["query_target"])
|
||||
vrf_acl = operator.attrgetter(f'{query_data["query_vrf"]}.access_list')(vrfs)
|
||||
target_ver = target.version
|
||||
|
||||
log.debug(f"Access List: {vrf_acl}")
|
||||
|
||||
for ace in vrf_acl:
|
||||
for action, net in {
|
||||
a: n for a, n in ace.items() for ace in vrf_acl if n.version == target_ver
|
||||
}.items():
|
||||
# If the target is a member of an allowed network, exit successfully.
|
||||
if member_of(target, net) and action == "allow":
|
||||
log.debug(f"{target} is specifically allowed")
|
||||
return target
|
||||
|
||||
# If the target is a member of a denied network, return an error.
|
||||
elif member_of(target, net) and action == "deny":
|
||||
log.debug(f"{target} is specifically denied")
|
||||
_exception = ValueError(params.messages.acl_denied)
|
||||
_exception.details = {"denied_network": str(net)}
|
||||
raise _exception
|
||||
|
||||
# Implicitly deny queries if an allow statement does not exist.
|
||||
log.debug(f"{target} is implicitly denied")
|
||||
_exception = ValueError(params.messages.acl_not_allowed)
|
||||
_exception.details = {"denied_network": ""}
|
||||
raise _exception
|
||||
|
||||
|
||||
def ip_attributes(target):
|
||||
"""
|
||||
@@ -192,10 +215,11 @@ class Validate:
|
||||
boolean for validity, specific error message, and status code.
|
||||
"""
|
||||
|
||||
def __init__(self, device, query_type, target):
|
||||
def __init__(self, device, query_data, target):
|
||||
"""Initialize device parameters and error codes."""
|
||||
self.device = device
|
||||
self.query_type = query_type
|
||||
self.query_data = query_data
|
||||
self.query_type = self.query_data["query_type"]
|
||||
self.target = target
|
||||
|
||||
def validate_ip(self):
|
||||
@@ -214,14 +238,12 @@ class Validate:
|
||||
**unformatted_error.details,
|
||||
)
|
||||
|
||||
# If target is a member of the blacklist, return an error.
|
||||
# If target is a not allowed, return an error.
|
||||
try:
|
||||
ip_blacklist(self.target)
|
||||
ip_access_list(self.query_data)
|
||||
except ValueError as unformatted_error:
|
||||
raise InputNotAllowed(
|
||||
params.messages.blacklist,
|
||||
target=self.target,
|
||||
**unformatted_error.details,
|
||||
str(unformatted_error), target=self.target, **unformatted_error.details
|
||||
)
|
||||
|
||||
# Perform further validation of a valid IP address, return an
|
||||
|
@@ -23,7 +23,6 @@ from hyperglass.configuration.models import (
|
||||
credentials as _credentials,
|
||||
)
|
||||
from hyperglass.exceptions import ConfigError, ConfigInvalid, ConfigMissing
|
||||
from hyperglass.constants import afi_nos_map
|
||||
|
||||
# Project Directories
|
||||
working_dir = Path(__file__).resolve().parent
|
||||
@@ -97,11 +96,17 @@ except ValidationError as validation_errors:
|
||||
# Validate that VRFs configured on a device are actually defined
|
||||
for dev in devices.hostnames:
|
||||
dev_cls = getattr(devices, dev)
|
||||
for vrf in getattr(dev_cls, "vrfs"):
|
||||
display_vrfs = []
|
||||
for vrf in getattr(dev_cls, "_vrfs"):
|
||||
if vrf not in vrfs._all:
|
||||
raise ConfigInvalid(
|
||||
field=vrf, error_msg=f"{vrf} is not in configured VRFs: {vrfs._all}"
|
||||
)
|
||||
vrf_attr = getattr(vrfs, vrf)
|
||||
display_vrfs.append(vrf_attr.display_name)
|
||||
devices.routers[dev]["display_vrfs"] = display_vrfs
|
||||
setattr(dev_cls, "display_vrfs", display_vrfs)
|
||||
|
||||
|
||||
# Logzero Configuration
|
||||
log_level = 20
|
||||
@@ -182,7 +187,7 @@ class Networks:
|
||||
router: {
|
||||
"location": router_params["location"],
|
||||
"display_name": router_params["display_name"],
|
||||
"vrfs": router_params["vrfs"],
|
||||
"vrfs": router_params["display_vrfs"],
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -191,7 +196,7 @@ class Networks:
|
||||
router: {
|
||||
"location": router_params["location"],
|
||||
"display_name": router_params["display_name"],
|
||||
"vrfs": router_params["vrfs"],
|
||||
"vrfs": router_params["display_vrfs"],
|
||||
}
|
||||
}
|
||||
if not frontend_dict:
|
||||
|
@@ -72,7 +72,7 @@ class Commands(BaseSettings):
|
||||
class CiscoIOS(BaseSettings):
|
||||
"""Class model for default cisco_ios commands"""
|
||||
|
||||
class VPNv4IPv4(BaseSettings):
|
||||
class IPv4Vrf(BaseSettings):
|
||||
"""Default commands for dual afi commands"""
|
||||
|
||||
bgp_community: str = "show bgp {afi} unicast vrf {vrf} community {target}"
|
||||
@@ -80,11 +80,10 @@ class Commands(BaseSettings):
|
||||
bgp_route: str = "show bgp {afi} unicast vrf {vrf} {target}"
|
||||
ping: str = "ping vrf {vrf} {target} repeat 5 source {source}"
|
||||
traceroute: str = (
|
||||
"traceroute vrf {vrf} {target} timeout 1 probe 2 source {source} "
|
||||
"| exclude Type escape"
|
||||
"traceroute vrf {vrf} {target} timeout 1 probe 2 source {source}"
|
||||
)
|
||||
|
||||
class VPNv6IPv6(BaseSettings):
|
||||
class IPv6Vrf(BaseSettings):
|
||||
"""Default commands for dual afi commands"""
|
||||
|
||||
bgp_community: str = "show bgp {afi} unicast vrf {vrf} community {target}"
|
||||
@@ -92,40 +91,33 @@ class Commands(BaseSettings):
|
||||
bgp_route: str = "show bgp {afi} unicast vrf {vrf} {target}"
|
||||
ping: str = "ping vrf {vrf} {target} repeat 5 source {source}"
|
||||
traceroute: str = (
|
||||
"traceroute vrf {vrf} {target} timeout 1 probe 2 source {source} "
|
||||
"| exclude Type escape"
|
||||
"traceroute vrf {vrf} {target} timeout 1 probe 2 source {source}"
|
||||
)
|
||||
|
||||
class IPv4(BaseSettings):
|
||||
class IPv4Default(BaseSettings):
|
||||
"""Default commands for ipv4 commands"""
|
||||
|
||||
bgp_community: str = "show bgp {afi} unicast community {target}"
|
||||
bgp_aspath: str = 'show bgp {afi} unicast quote-regexp "{target}"'
|
||||
bgp_route: str = "show bgp {afi} unicast {target} | exclude pathid:|Epoch"
|
||||
ping: str = "ping {target} repeat 5 source {source} | exclude Type escape"
|
||||
traceroute: str = (
|
||||
"traceroute {target} timeout 1 probe 2 source {source} "
|
||||
"| exclude Type escape"
|
||||
)
|
||||
ping: str = "ping {target} repeat 5 source {source}"
|
||||
traceroute: str = "traceroute {target} timeout 1 probe 2 source {source}"
|
||||
|
||||
class IPv6(BaseSettings):
|
||||
class IPv6Default(BaseSettings):
|
||||
"""Default commands for ipv6 commands"""
|
||||
|
||||
bgp_community: str = "show bgp {afi} unicast community {target}"
|
||||
bgp_aspath: str = 'show bgp {afi} unicast quote-regexp "{target}"'
|
||||
bgp_route: str = "show bgp {afi} unicast {target} | exclude pathid:|Epoch"
|
||||
ping: str = (
|
||||
"ping {afi} {target} repeat 5 source {source} | exclude Type escape"
|
||||
)
|
||||
ping: str = ("ping {afi} {target} repeat 5 source {source}")
|
||||
traceroute: str = (
|
||||
"traceroute ipv6 {target} timeout 1 probe 2 source {source} "
|
||||
"| exclude Type escape"
|
||||
"traceroute ipv6 {target} timeout 1 probe 2 source {source}"
|
||||
)
|
||||
|
||||
ipv4: IPv4 = IPv4()
|
||||
ipv6: IPv6 = IPv6()
|
||||
vpn_ipv4: VPNv4IPv4 = VPNv4IPv4()
|
||||
vpn_ipv6: VPNv6IPv6 = VPNv6IPv6()
|
||||
ipv4_default: IPv4Default = IPv4Default()
|
||||
ipv6_default: IPv6Default = IPv6Default()
|
||||
ipv4_vrf: IPv4Vrf = IPv4Vrf()
|
||||
ipv6_vrf: IPv6Vrf = IPv6Vrf()
|
||||
|
||||
class CiscoXR(BaseSettings):
|
||||
"""Class model for default cisco_xr commands"""
|
||||
|
@@ -7,22 +7,15 @@ Validates input for overridden parameters.
|
||||
"""
|
||||
# Standard Library Imports
|
||||
from math import ceil
|
||||
from typing import List
|
||||
|
||||
# Third Party Imports
|
||||
from pydantic import BaseSettings
|
||||
from pydantic import IPvAnyNetwork
|
||||
from pydantic import constr
|
||||
|
||||
|
||||
class Features(BaseSettings):
|
||||
"""Class model for params.features"""
|
||||
|
||||
class Vrf(BaseSettings):
|
||||
"""Class model for params.features.vrf"""
|
||||
|
||||
enable: bool = False
|
||||
|
||||
class BgpRoute(BaseSettings):
|
||||
"""Class model for params.features.bgp_route"""
|
||||
|
||||
@@ -68,19 +61,6 @@ class Features(BaseSettings):
|
||||
|
||||
enable: bool = True
|
||||
|
||||
class Blacklist(BaseSettings):
|
||||
"""Class model for params.features.blacklist"""
|
||||
|
||||
enable: bool = True
|
||||
networks: List[IPvAnyNetwork] = [
|
||||
"198.18.0.0/15",
|
||||
"100.64.0.0/10",
|
||||
"2001:db8::/32",
|
||||
"10.0.0.0/8",
|
||||
"192.168.0.0/16",
|
||||
"172.16.0.0/12",
|
||||
]
|
||||
|
||||
class Cache(BaseSettings):
|
||||
"""Class model for params.features.cache"""
|
||||
|
||||
@@ -138,8 +118,6 @@ class Features(BaseSettings):
|
||||
bgp_aspath: BgpAsPath = BgpAsPath()
|
||||
ping: Ping = Ping()
|
||||
traceroute: Traceroute = Traceroute()
|
||||
blacklist: Blacklist = Blacklist()
|
||||
cache: Cache = Cache()
|
||||
max_prefix: MaxPrefix = MaxPrefix()
|
||||
rate_limit: RateLimit = RateLimit()
|
||||
vrf: Vrf = Vrf()
|
||||
|
@@ -16,13 +16,15 @@ class Messages(BaseSettings):
|
||||
no_query_type: str = "A query type must be specified."
|
||||
no_location: str = "A location must be selected."
|
||||
no_input: str = "{field} must be specified."
|
||||
blacklist: str = "{target} a member of {blacklisted_net}, which is not allowed."
|
||||
acl_denied: str = "{target} is a member of {denied_network}, which is not allowed."
|
||||
acl_not_allowed: str = "{target} is not allowed."
|
||||
max_prefix: str = (
|
||||
"Prefix length must be shorter than /{max_length}. {target} is too specific."
|
||||
)
|
||||
requires_ipv6_cidr: str = (
|
||||
"{device_name} requires IPv6 BGP lookups to be in CIDR notation."
|
||||
)
|
||||
feature_not_enabled: str = "{feature} is not enabled for {device_name}."
|
||||
invalid_input: str = "{target} is not a valid {query_type} target."
|
||||
invalid_field: str = "{input} is an invalid {field}."
|
||||
general: str = "Something went wrong."
|
||||
@@ -31,5 +33,5 @@ class Messages(BaseSettings):
|
||||
connection_error: str = "Error connecting to {device_name}: {error}"
|
||||
authentication_error: str = "Authentication error occurred."
|
||||
noresponse_error: str = "No response."
|
||||
vrf_not_associated: str = "{vrf} is not associated with {device_name}."
|
||||
vrf_not_associated: str = "VRF {vrf_name} is not associated with {device_name}."
|
||||
no_matching_vrfs: str = "No VRFs Match"
|
||||
|
@@ -8,34 +8,71 @@ Validates input for overridden parameters.
|
||||
# Standard Library Imports
|
||||
from typing import List
|
||||
from typing import Union
|
||||
from ipaddress import IPv4Address, IPv6Address
|
||||
|
||||
# Third Party Imports
|
||||
from pydantic import BaseSettings
|
||||
from pydantic import IPvAnyAddress
|
||||
from pydantic import validator
|
||||
from logzero import logger
|
||||
from logzero import logger as log
|
||||
|
||||
# Project Imports
|
||||
from hyperglass.configuration.models._utils import clean_name
|
||||
from hyperglass.constants import Supported
|
||||
from hyperglass.exceptions import UnsupportedDevice
|
||||
from hyperglass.constants import afi_nos_map
|
||||
from hyperglass.exceptions import ConfigError
|
||||
|
||||
|
||||
class Afi(BaseSettings):
|
||||
class DeviceVrf4(BaseSettings):
|
||||
"""Model for AFI definitions"""
|
||||
|
||||
label: str
|
||||
source: IPvAnyAddress
|
||||
afi_name: str = ""
|
||||
vrf_name: str = ""
|
||||
source_address: IPv4Address
|
||||
|
||||
class Config:
|
||||
"""Pydantic Config"""
|
||||
|
||||
validate_assignment = True
|
||||
validate_all = True
|
||||
|
||||
|
||||
class Afis(BaseSettings):
|
||||
"""Model for AFI map"""
|
||||
class DeviceVrf6(BaseSettings):
|
||||
"""Model for AFI definitions"""
|
||||
|
||||
ipv4: Union[Afi, None] = None
|
||||
ipv6: Union[Afi, None] = None
|
||||
ipv4_vpn: Union[Afi, None] = None
|
||||
ipv6_vpn: Union[Afi, None] = None
|
||||
afi_name: str = ""
|
||||
vrf_name: str = ""
|
||||
source_address: IPv6Address
|
||||
|
||||
class Config:
|
||||
"""Pydantic Config"""
|
||||
|
||||
validate_assignment = True
|
||||
validate_all = True
|
||||
|
||||
|
||||
class VrfAfis(BaseSettings):
|
||||
"""Model for per-AFI dicts of VRF params"""
|
||||
|
||||
ipv4: Union[DeviceVrf4, None] = None
|
||||
ipv6: Union[DeviceVrf6, None] = None
|
||||
|
||||
class Config:
|
||||
"""Pydantic Config"""
|
||||
|
||||
validate_assignment = True
|
||||
validate_all = True
|
||||
|
||||
|
||||
class Vrf(BaseSettings):
|
||||
default: VrfAfis
|
||||
|
||||
class Config:
|
||||
"""Pydantic Config"""
|
||||
|
||||
extra = "allow"
|
||||
validate_assignment = True
|
||||
validate_all = True
|
||||
|
||||
|
||||
class Router(BaseSettings):
|
||||
@@ -50,8 +87,9 @@ class Router(BaseSettings):
|
||||
port: int
|
||||
nos: str
|
||||
commands: Union[str, None] = None
|
||||
vrfs: List[str] = ["default"]
|
||||
afis: Afis
|
||||
vrfs: Vrf
|
||||
_vrfs: List[str]
|
||||
display_vrfs: List[str] = []
|
||||
|
||||
@validator("nos")
|
||||
def supported_nos(cls, v): # noqa: N805
|
||||
@@ -76,23 +114,48 @@ class Router(BaseSettings):
|
||||
v = values["nos"]
|
||||
return v
|
||||
|
||||
@validator("afis", pre=True)
|
||||
def validate_afis(cls, v, values): # noqa: N805
|
||||
@validator("vrfs", pre=True, whole=True, always=True)
|
||||
def validate_vrfs(cls, v, values): # noqa: N805
|
||||
"""
|
||||
If an AFI map is not defined, try to get one based on the
|
||||
NOS name. If that doesn't exist, use a default.
|
||||
"""
|
||||
logger.debug(f"V In: {v}")
|
||||
for (afi_name, afi_params) in {
|
||||
afi: params for afi, params in v.items() if params is not None
|
||||
log.debug(f"Start: {v}")
|
||||
_vrfs = []
|
||||
for vrf_label, vrf_afis in v.items():
|
||||
if vrf_label is None:
|
||||
raise ConfigError(
|
||||
"The default routing table with source IPs must be defined"
|
||||
)
|
||||
vrf_label = clean_name(vrf_label)
|
||||
_vrfs.append(vrf_label)
|
||||
if not vrf_afis.get("ipv4"):
|
||||
vrf_afis.update({"ipv4": None})
|
||||
if not vrf_afis.get("ipv6"):
|
||||
vrf_afis.update({"ipv6": None})
|
||||
for afi, params in {
|
||||
a: p for a, p in vrf_afis.items() if p is not None
|
||||
}.items():
|
||||
if afi_params.get("label") is None:
|
||||
label = afi_nos_map.get(values["nos"], None)
|
||||
if label is None:
|
||||
label = afi_nos_map["default"][afi_name]
|
||||
v[afi_name].update({"label": label})
|
||||
if not params.get("source_address"):
|
||||
raise ConfigError(
|
||||
'A "source_address" must be defined in {afi}', afi=afi
|
||||
)
|
||||
if not params.get("afi_name"):
|
||||
params.update({"afi_name": afi})
|
||||
if not params.get("vrf_name"):
|
||||
params.update({"vrf_name": vrf_label})
|
||||
setattr(Vrf, vrf_label, VrfAfis(**vrf_afis))
|
||||
log.debug(_vrfs)
|
||||
values["_vrfs"] = _vrfs
|
||||
return v
|
||||
|
||||
class Config:
|
||||
"""Pydantic Config"""
|
||||
|
||||
validate_assignment = True
|
||||
validate_all = True
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Routers(BaseSettings):
|
||||
"""Base model for devices class."""
|
||||
|
@@ -7,20 +7,45 @@ Validates input for overridden parameters.
|
||||
"""
|
||||
# Standard Library Imports
|
||||
from typing import List
|
||||
from typing import Dict
|
||||
|
||||
# Third Party Imports
|
||||
from pydantic import BaseSettings
|
||||
from pydantic import IPvAnyNetwork
|
||||
from pydantic import validator
|
||||
|
||||
# Project Imports
|
||||
from hyperglass.configuration.models._utils import clean_name
|
||||
from hyperglass.exceptions import ConfigInvalid
|
||||
|
||||
|
||||
class Vrf(BaseSettings):
|
||||
"""Model for per VRF/afi config in devices.yaml"""
|
||||
|
||||
display_name: str
|
||||
label: str
|
||||
afis: List[str]
|
||||
ipv4: bool = True
|
||||
ipv6: bool = True
|
||||
access_list: List[Dict[str, IPvAnyNetwork]] = [
|
||||
{"allow": "0.0.0.0/0"},
|
||||
{"allow": "::/0"},
|
||||
]
|
||||
|
||||
@validator("access_list", whole=True, always=True)
|
||||
def validate_action(cls, value):
|
||||
allowed_actions = ("allow", "deny")
|
||||
for li in value:
|
||||
for action, network in li.items():
|
||||
if action not in allowed_actions:
|
||||
raise ConfigInvalid(
|
||||
field=action,
|
||||
error_msg=(
|
||||
"Access List Entries must be formatted as "
|
||||
'"- action: network" (list of dictionaries with the action '
|
||||
"as the key, and the network as the value), e.g. "
|
||||
'"- deny: 192.0.2.0/24 or "- allow: 2001:db8::/32".'
|
||||
),
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class Vrfs(BaseSettings):
|
||||
@@ -33,29 +58,25 @@ class Vrfs(BaseSettings):
|
||||
characters from VRF names, dynamically sets attributes for
|
||||
the Vrfs class.
|
||||
"""
|
||||
vrfs: Vrf = {
|
||||
"default": {
|
||||
"display_name": "Default",
|
||||
"label": "default",
|
||||
"afis": ["ipv4, ipv6"],
|
||||
}
|
||||
}
|
||||
labels: List[str] = ["default"]
|
||||
_all: List[str] = ["default"]
|
||||
|
||||
# Default settings which include the default/global routing table
|
||||
vrfs: Vrf = {"default": {"display_name": "Global", "ipv4": True, "ipv6": True}}
|
||||
display_names: List[str] = ["Global"]
|
||||
_all: List[str] = ["global"]
|
||||
|
||||
for (vrf_key, params) in input_params.items():
|
||||
vrf = clean_name(vrf_key)
|
||||
vrf_params = Vrf(**params)
|
||||
vrfs.update({vrf: vrf_params.dict()})
|
||||
labels.append(params.get("label"))
|
||||
display_names.append(params.get("display_name"))
|
||||
_all.append(vrf_key)
|
||||
for (vrf_key, params) in vrfs.items():
|
||||
setattr(Vrfs, vrf_key, params)
|
||||
setattr(Vrfs, vrf_key, Vrf(**params))
|
||||
|
||||
labels: List[str] = list(set(labels))
|
||||
display_names: List[str] = list(set(display_names))
|
||||
_all: List[str] = list(set(_all))
|
||||
Vrfs.vrfs = vrfs
|
||||
Vrfs.labels = labels
|
||||
Vrfs.display_names = display_names
|
||||
Vrfs._all = _all
|
||||
return Vrfs()
|
||||
|
||||
|
@@ -6,8 +6,8 @@ protocol_map = {80: "http", 8080: "http", 443: "https", 8443: "https"}
|
||||
|
||||
afi_nos_map = {
|
||||
"default": {
|
||||
"ipv4": "ipv4",
|
||||
"ipv6": "ipv6",
|
||||
"ipv4_global": "ipv4",
|
||||
"ipv6_global": "ipv6",
|
||||
"ipv4_vpn": "vpnv4",
|
||||
"ipv6_vpn": "vpnv6",
|
||||
}
|
||||
|
@@ -27,6 +27,7 @@ from sanic_limiter import get_remote_address
|
||||
from hyperglass.render import render_html
|
||||
from hyperglass.command.execute import Execute
|
||||
from hyperglass.configuration import devices
|
||||
from hyperglass.configuration import vrfs
|
||||
from hyperglass.configuration import logzero_config # noqa: F401
|
||||
from hyperglass.configuration import params
|
||||
from hyperglass.constants import Supported
|
||||
@@ -250,7 +251,7 @@ async def validate_input(query_data): # noqa: C901
|
||||
query_location = supported_query_data.get("query_location", "")
|
||||
query_type = supported_query_data.get("query_type", "")
|
||||
query_target = supported_query_data.get("query_target", "")
|
||||
query_vrf = supported_query_data.get("query_vrf", [])
|
||||
query_vrf = supported_query_data.get("query_vrf", "")
|
||||
|
||||
# Verify that query_target is not empty
|
||||
if not query_target:
|
||||
@@ -359,9 +360,8 @@ async def validate_input(query_data): # noqa: C901
|
||||
"keywords": [params.branding.text.query_location, query_type],
|
||||
}
|
||||
)
|
||||
if params.features.vrf.enable:
|
||||
# Verify that query_vrf is a list
|
||||
if query_vrf and not isinstance(query_vrf, list):
|
||||
# Verify that query_vrf is a string
|
||||
if query_vrf and not isinstance(query_vrf, str):
|
||||
raise InvalidUsage(
|
||||
{
|
||||
"message": params.messages.invalid_field.format(
|
||||
@@ -372,17 +372,28 @@ async def validate_input(query_data): # noqa: C901
|
||||
}
|
||||
)
|
||||
# Verify that vrfs in query_vrf are defined
|
||||
if query_vrf and not all(vrf in query_vrf for vrf in devices.vrfs):
|
||||
display_vrfs = [v["display_name"] for k, v in vrfs.vrfs.items()]
|
||||
if query_vrf and not any(vrf in query_vrf for vrf in display_vrfs):
|
||||
display_device = getattr(devices, query_location)
|
||||
raise InvalidUsage(
|
||||
{
|
||||
"message": params.messages.invalid_field.format(
|
||||
input=query_vrf, field=params.branding.text.query_vrf
|
||||
"message": params.messages.vrf_not_associated.format(
|
||||
vrf_name=query_vrf, device_name=display_device.display_name
|
||||
),
|
||||
"alert": "warning",
|
||||
"keywords": [params.branding.text.query_vrf, query_vrf],
|
||||
"keywords": [query_vrf, query_location],
|
||||
}
|
||||
)
|
||||
return query_data
|
||||
# If VRF display name from UI/API matches a configured display name, set the
|
||||
# query_vrf value to the configured VRF key name
|
||||
if query_vrf:
|
||||
supported_query_data["query_vrf"] = [
|
||||
k for k, v in vrfs.vrfs.items() if v["display_name"] == query_vrf
|
||||
][0]
|
||||
if not query_vrf:
|
||||
supported_query_data["query_vrf"] = "default"
|
||||
log.debug(f"Validated Query: {supported_query_data}")
|
||||
return supported_query_data
|
||||
|
||||
|
||||
@app.route("/query", methods=["POST"])
|
||||
|
@@ -386,7 +386,7 @@ $('#lgForm').on('submit', (e) => {
|
||||
const queryType = $('#query_type').val() || '';
|
||||
const queryLocation = $('#location').val() || '';
|
||||
const queryTarget = $('#query_target').val() || '';
|
||||
const queryVrf = $('#query_vrf').val() || [];
|
||||
const queryVrf = $('#query_vrf').val() || '';
|
||||
|
||||
const queryTargetContainer = $('#query_target');
|
||||
const queryTypeContainer = $('#query_type').next('.dropdown-toggle');
|
||||
|
Reference in New Issue
Block a user