1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00

add VRF support

This commit is contained in:
checktheroads
2019-09-30 07:51:17 -07:00
parent ed6770fc0f
commit f756e11615
12 changed files with 340 additions and 219 deletions

View File

@@ -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):

View File

@@ -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}")

View File

@@ -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

View File

@@ -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:

View File

@@ -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"""

View File

@@ -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()

View File

@@ -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"

View File

@@ -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."""

View File

@@ -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()

View File

@@ -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",
}

View File

@@ -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"])

View File

@@ -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');