"""Validate VRF configuration variables.""" # Standard Library Imports from ipaddress import IPv4Address from ipaddress import IPv4Network from ipaddress import IPv6Address from ipaddress import IPv6Network from typing import List from typing import Optional # Third Party Imports from pydantic import Field from pydantic import FilePath from pydantic import StrictBool from pydantic import StrictStr from pydantic import conint from pydantic import constr from pydantic import root_validator from pydantic import validator # Project Imports from hyperglass.configuration.models._utils import HyperglassModel from hyperglass.configuration.models._utils import HyperglassModelExtra class AccessList4(HyperglassModel): """Validation model for IPv4 access-lists.""" network: IPv4Network = Field( "0.0.0.0/0", title="Network", description="IPv4 Network/Prefix that should be permitted or denied. ", ) action: constr(regex=r"permit|deny") = Field( "permit", title="Action", description="Permit or deny any networks contained within the prefix.", ) ge: conint(ge=0, le=32) = Field( 0, title="Greater Than or Equal To", description="Similar to `ge` in a Cisco prefix-list, the `ge` field defines the **bottom** threshold for prefix size. For example, a value of `24` would result in a query for `192.0.2.0/23` being denied, but a query for `192.0.2.0/32` would be permitted. If this field is set to a value smaller than the `network` field's prefix length, this field's value will be overwritten to the prefix length of the prefix in the `network` field.", ) le: conint(ge=0, le=32) = Field( 32, title="Less Than or Equal To", description="Similar to `le` in a Cisco prefix-list, the `le` field defines the **top** threshold for prefix size. For example, a value of `24` would result in a query for `192.0.2.0/23` being permitted, but a query for `192.0.2.0/32` would be denied.", ) @validator("ge") def validate_model(cls, value, values): """Ensure ge is at least the size of the input prefix. Arguments: value {int} -- Initial ge value values {dict} -- Other post-validation fields Returns: {int} -- Validated ge value """ net_len = values["network"].prefixlen if net_len > value: value = net_len return value class AccessList6(HyperglassModel): """Validation model for IPv6 access-lists.""" network: IPv6Network = Field( "::/0", title="Network", description="IPv6 Network/Prefix that should be permitted or denied. ", ) action: constr(regex=r"permit|deny") = Field( "permit", title="Action", description="Permit or deny any networks contained within the prefix.", # regex="permit|deny", ) ge: conint(ge=0, le=128) = Field( 0, title="Greater Than or Equal To", description="Similar to `ge` in a Cisco prefix-list, the `ge` field defines the **bottom** threshold for prefix size. For example, a value of `64` would result in a query for `2001:db8::/48` being denied, but a query for `2001:db8::1/128` would be permitted. If this field is set to a value smaller than the `network` field's prefix length, this field's value will be overwritten to the prefix length of the prefix in the `network` field.", # ge=0, # le=128, ) le: conint(ge=0, le=128) = Field( 128, title="Less Than or Equal To", description="Similar to `le` in a Cisco prefix-list, the `le` field defines the **top** threshold for prefix size. For example, a value of `64` would result in a query for `2001:db8::/48` being permitted, but a query for `2001:db8::1/128` would be denied.", # ge=0, # le=128, ) @validator("ge") def validate_model(cls, value, values): """Ensure ge is at least the size of the input prefix. Arguments: value {int} -- Initial ge value values {dict} -- Other post-validation fields Returns: {int} -- Validated ge value """ net_len = values["network"].prefixlen if net_len > value: value = net_len return value class InfoConfigParams(HyperglassModelExtra): """Validation model for per-help params.""" title: Optional[StrictStr] class Config: """Pydantic model configuration.""" title = "Help Parameters" description = "Set dynamic or reusable values which may be used in the help/information content. Params my be access by using Python string formatting syntax, e.g. `{param_name}`. Any arbitrary values may be added." class InfoConfig(HyperglassModel): """Validation model for help configuration.""" enable: StrictBool = True file: Optional[FilePath] params: InfoConfigParams = InfoConfigParams() class Config: """Pydantic model configuration.""" fields = { "enable": { "title": "Enable", "description": "Enable or disable the display of help/information content for this query type.", }, "file": { "title": "File Name", "description": "Path to a valid text or Markdown file containing custom content.", }, } class Info(HyperglassModel): """Validation model for per-VRF, per-Command help.""" bgp_aspath: InfoConfig = InfoConfig() bgp_community: InfoConfig = InfoConfig() bgp_route: InfoConfig = InfoConfig() ping: InfoConfig = InfoConfig() traceroute: InfoConfig = InfoConfig() class Config: """Pydantic model configuration.""" title = "VRF Information" description = "Per-VRF help & information content." fields = { "bgp_aspath": { "title": "BGP AS Path", "description": "Show information about bgp_aspath queries when selected.", }, "bgp_community": { "title": "BGP Community", "description": "Show information about bgp_community queries when selected.", }, "bgp_route": { "title": "BGP Route", "description": "Show information about bgp_route queries when selected.", }, "ping": { "title": "Ping", "description": "Show information about ping queries when selected.", }, "traceroute": { "title": "Traceroute", "description": "Show information about traceroute queries when selected.", }, } class DeviceVrf4(HyperglassModelExtra): """Validation model for IPv4 AFI definitions.""" source_address: IPv4Address access_list: List[AccessList4] = [AccessList4()] class DeviceVrf6(HyperglassModelExtra): """Validation model for IPv6 AFI definitions.""" source_address: IPv6Address access_list: List[AccessList6] = [AccessList6()] class Vrf(HyperglassModel): """Validation model for per VRF/afi config in devices.yaml.""" name: StrictStr display_name: StrictStr info: Info = Info() ipv4: Optional[DeviceVrf4] ipv6: Optional[DeviceVrf6] @root_validator def set_dynamic(cls, values): """Set dynamic attributes before VRF initialization. Arguments: values {dict} -- Post-validation VRF attributes Returns: {dict} -- VRF with new attributes set """ if values["name"] == "default": protocol4 = "ipv4_default" protocol6 = "ipv6_default" else: protocol4 = "ipv4_vpn" protocol6 = "ipv6_vpn" if values.get("ipv4") is not None: values["ipv4"].protocol = protocol4 values["ipv4"].version = 4 if values.get("ipv6") is not None: values["ipv6"].protocol = protocol6 values["ipv6"].version = 6 return values def __getitem__(self, i): """Access the VRF's AFI by IP protocol number. Arguments: i {int} -- IP Protocol number (4|6) Raises: AttributeError: Raised if passed number is not 4 or 6. Returns: {object} -- AFI object """ if i not in (4, 6): raise AttributeError(f"Must be 4 or 6, got '{i}") return getattr(self, f"ipv{i}") def __hash__(self): """Make VRF object hashable so the object can be deduplicated with set(). Returns: {int} -- Hash of VRF name """ return hash((self.name,)) def __eq__(self, other): """Make VRF object comparable so the object can be deduplicated with set(). Arguments: other {object} -- Object to compare Returns: {bool} -- True if comparison attributes are the same value """ result = False if isinstance(other, HyperglassModel): result = self.name == other.name return result class Config: """Pydantic model configuration.""" title = "VRF" description = "Per-VRF configuration." fields = { "name": { "title": "Name", "description": "VRF name as configured on the router/device.", }, "display_name": { "title": "Display Name", "description": "Display name of VRF for use in the hyperglass UI. If none is specified, hyperglass will attempt to generate one.", }, }