mirror of
https://github.com/checktheroads/hyperglass
synced 2024-05-11 05:55:08 +00:00
add VRF support
This commit is contained in:
@@ -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
|
||||
}.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."""
|
||||
|
@@ -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()
|
||||
|
||||
|
Reference in New Issue
Block a user