From f756e116152225aca58f1f1e93056e141c69b8e7 Mon Sep 17 00:00:00 2001 From: checktheroads Date: Mon, 30 Sep 2019 07:51:17 -0700 Subject: [PATCH] add VRF support --- hyperglass/command/construct.py | 163 ++++++++++++-------- hyperglass/command/execute.py | 2 +- hyperglass/command/validate.py | 88 +++++++---- hyperglass/configuration/__init__.py | 13 +- hyperglass/configuration/models/commands.py | 36 ++--- hyperglass/configuration/models/features.py | 22 --- hyperglass/configuration/models/messages.py | 6 +- hyperglass/configuration/models/routers.py | 111 ++++++++++--- hyperglass/configuration/models/vrfs.py | 51 ++++-- hyperglass/constants.py | 4 +- hyperglass/hyperglass.py | 61 +++++--- hyperglass/static/hyperglass.es6 | 2 +- 12 files changed, 340 insertions(+), 219 deletions(-) diff --git a/hyperglass/command/construct.py b/hyperglass/command/construct.py index 780d9e1..1b1b165 100644 --- a/hyperglass/command/construct.py +++ b/hyperglass/command/construct.py @@ -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_type": "traceroute", - "afi": afi, - "vrf": self.query_vrf, - "source": source, - "target": self.query_target, - } + query.append( + json.dumps( + { + "query_type": "traceroute", + "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_type": "bgp_route", - "afi": afi, - "vrf": self.query_vrf, - "source": source, - "target": self.query_target, - } + query.append( + json.dumps( + { + "query_type": "bgp_route", + "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_type": "bgp_community", - "afi": afi, - "vrf": self.query_vrf, - "source": source, - "target": self.query_target, - } + query.append( + json.dumps( + { + "query_type": "bgp_community", + "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): diff --git a/hyperglass/command/execute.py b/hyperglass/command/execute.py index 833dcdb..5c8bf60 100644 --- a/hyperglass/command/execute.py +++ b/hyperglass/command/execute.py @@ -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}") diff --git a/hyperglass/command/validate.py b/hyperglass/command/validate.py index 7f23aaa..51c60eb 100644 --- a/hyperglass/command/validate.py +++ b/hyperglass/command/validate.py @@ -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,35 +98,56 @@ 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) - if ( - blacklist_net.network_address <= target.network_address - and blacklist_net.broadcast_address >= target.broadcast_address - ): - log.debug(f"Blacklist Match Found for {target} in {str(net)}") - _exception = ValueError(params.messages.blacklist) - _exception.details = {"blacklisted_net": str(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 ( + network.network_address <= target.network_address + and network.broadcast_address >= target.broadcast_address # NOQA: W503 + ): + 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 - return target + + # 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 diff --git a/hyperglass/configuration/__init__.py b/hyperglass/configuration/__init__.py index c7e9c7a..b49ea84 100644 --- a/hyperglass/configuration/__init__.py +++ b/hyperglass/configuration/__init__.py @@ -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: diff --git a/hyperglass/configuration/models/commands.py b/hyperglass/configuration/models/commands.py index 0426bb0..a76b757 100644 --- a/hyperglass/configuration/models/commands.py +++ b/hyperglass/configuration/models/commands.py @@ -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""" diff --git a/hyperglass/configuration/models/features.py b/hyperglass/configuration/models/features.py index 824c043..39ab40a 100644 --- a/hyperglass/configuration/models/features.py +++ b/hyperglass/configuration/models/features.py @@ -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() diff --git a/hyperglass/configuration/models/messages.py b/hyperglass/configuration/models/messages.py index e0e9d91..ec6d1dc 100644 --- a/hyperglass/configuration/models/messages.py +++ b/hyperglass/configuration/models/messages.py @@ -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" diff --git a/hyperglass/configuration/models/routers.py b/hyperglass/configuration/models/routers.py index 26c1e1e..b902237 100644 --- a/hyperglass/configuration/models/routers.py +++ b/hyperglass/configuration/models/routers.py @@ -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 - }.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}) + 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 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.""" diff --git a/hyperglass/configuration/models/vrfs.py b/hyperglass/configuration/models/vrfs.py index 28ead91..2b8a7e9 100644 --- a/hyperglass/configuration/models/vrfs.py +++ b/hyperglass/configuration/models/vrfs.py @@ -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() diff --git a/hyperglass/constants.py b/hyperglass/constants.py index 28899f2..c630b10 100644 --- a/hyperglass/constants.py +++ b/hyperglass/constants.py @@ -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", } diff --git a/hyperglass/hyperglass.py b/hyperglass/hyperglass.py index 87bc38e..6ccfe48 100644 --- a/hyperglass/hyperglass.py +++ b/hyperglass/hyperglass.py @@ -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,30 +360,40 @@ 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): - raise InvalidUsage( - { - "message": params.messages.invalid_field.format( - input=query_vrf, field=params.branding.text.query_vrf - ), - "alert": "warning", - "keywords": [params.branding.text.query_vrf, query_vrf], - } - ) - # Verify that vrfs in query_vrf are defined - if query_vrf and not all(vrf in query_vrf for vrf in devices.vrfs): - raise InvalidUsage( - { - "message": params.messages.invalid_field.format( - input=query_vrf, field=params.branding.text.query_vrf - ), - "alert": "warning", - "keywords": [params.branding.text.query_vrf, query_vrf], - } - ) - return query_data + # Verify that query_vrf is a string + if query_vrf and not isinstance(query_vrf, str): + raise InvalidUsage( + { + "message": params.messages.invalid_field.format( + input=query_vrf, field=params.branding.text.query_vrf + ), + "alert": "warning", + "keywords": [params.branding.text.query_vrf, query_vrf], + } + ) + # Verify that vrfs in query_vrf are defined + 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.vrf_not_associated.format( + vrf_name=query_vrf, device_name=display_device.display_name + ), + "alert": "warning", + "keywords": [query_vrf, query_location], + } + ) + # 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"]) diff --git a/hyperglass/static/hyperglass.es6 b/hyperglass/static/hyperglass.es6 index d65489d..94dd42e 100644 --- a/hyperglass/static/hyperglass.es6 +++ b/hyperglass/static/hyperglass.es6 @@ -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');