From ad3e368246f29ba084c67da1f83efddb3e559677 Mon Sep 17 00:00:00 2001 From: checktheroads Date: Wed, 11 Sep 2019 01:35:31 -0700 Subject: [PATCH] start vrf data model; move models to dedicated module --- hyperglass/configuration/__init__.py | 35 +- hyperglass/configuration/models.py | 793 ------------------ hyperglass/configuration/models/__init__.py | 7 + hyperglass/configuration/models/_utils.py | 16 + hyperglass/configuration/models/branding.py | 121 +++ hyperglass/configuration/models/commands.py | 208 +++++ .../configuration/models/credentials.py | 44 + hyperglass/configuration/models/features.py | 145 ++++ hyperglass/configuration/models/general.py | 27 + hyperglass/configuration/models/messages.py | 35 + hyperglass/configuration/models/networks.py | 45 + hyperglass/configuration/models/params.py | 29 + hyperglass/configuration/models/proxies.py | 62 ++ hyperglass/configuration/models/routers.py | 90 ++ hyperglass/configuration/models/vrfs.py | 66 ++ 15 files changed, 916 insertions(+), 807 deletions(-) delete mode 100644 hyperglass/configuration/models.py create mode 100644 hyperglass/configuration/models/__init__.py create mode 100644 hyperglass/configuration/models/_utils.py create mode 100644 hyperglass/configuration/models/branding.py create mode 100644 hyperglass/configuration/models/commands.py create mode 100644 hyperglass/configuration/models/credentials.py create mode 100644 hyperglass/configuration/models/features.py create mode 100644 hyperglass/configuration/models/general.py create mode 100644 hyperglass/configuration/models/messages.py create mode 100644 hyperglass/configuration/models/networks.py create mode 100644 hyperglass/configuration/models/params.py create mode 100644 hyperglass/configuration/models/proxies.py create mode 100644 hyperglass/configuration/models/routers.py create mode 100644 hyperglass/configuration/models/vrfs.py diff --git a/hyperglass/configuration/__init__.py b/hyperglass/configuration/__init__.py index 6729913..080747b 100644 --- a/hyperglass/configuration/__init__.py +++ b/hyperglass/configuration/__init__.py @@ -4,7 +4,6 @@ default values if undefined. """ # Standard Library Imports -import operator from pathlib import Path # Third Party Imports @@ -14,7 +13,15 @@ from logzero import logger from pydantic import ValidationError # Project Imports -from hyperglass.configuration import models +from hyperglass.configuration.models import ( + params as _params, + commands as _commands, + routers as _routers, + proxies as _proxies, + networks as _networks, + vrfs as _vrfs, + credentials as _credentials, +) from hyperglass.exceptions import ConfigError, ConfigInvalid, ConfigMissing # Project Directories @@ -59,19 +66,19 @@ except (yaml.YAMLError, yaml.MarkedYAMLError) as yaml_error: # Map imported user config files to expected schema: try: if user_config: - params = models.Params(**user_config) + params = _params.Params(**user_config) elif not user_config: - params = models.Params() + params = _params.Params() if user_commands: - commands = models.Commands.import_params(user_commands) + commands = _commands.Commands.import_params(user_commands) elif not user_commands: - commands = models.Commands() + commands = _commands.Commands() - devices = models.Routers.import_params(user_devices["router"]) - credentials = models.Credentials.import_params(user_devices["credential"]) - proxies = models.Proxies.import_params(user_devices["proxy"]) - _networks = models.Networks.import_params(user_devices["network"]) - vrfs = models.Vrfs.import_params(user_devices.get("vrf")) + devices = _routers.Routers.import_params(user_devices["router"]) + credentials = _credentials.Credentials.import_params(user_devices["credential"]) + proxies = _proxies.Proxies.import_params(user_devices["proxy"]) + imported_networks = _networks.Networks.import_params(user_devices["network"]) + vrfs = _vrfs.Vrfs.import_params(user_devices.get("vrf")) except ValidationError as validation_errors: @@ -109,7 +116,7 @@ logzero_config = logzero.setup_default_logger( class Networks: def __init__(self): self.routers = devices.routers - self.networks = _networks.networks + self.networks = imported_networks.networks def networks_verbose(self): locations_dict = {} @@ -141,8 +148,8 @@ class Networks: def networks_display(self): locations_dict = {} - for (router, router_params) in devices.routers.items(): - for (netname, net_params) in _networks.networks.items(): + for (router, router_params) in self.routers.items(): + for (netname, net_params) in self.networks.items(): if router_params["network"] == netname: net_display = net_params["display_name"] if net_display in locations_dict: diff --git a/hyperglass/configuration/models.py b/hyperglass/configuration/models.py deleted file mode 100644 index d803206..0000000 --- a/hyperglass/configuration/models.py +++ /dev/null @@ -1,793 +0,0 @@ -""" -Defines models for all config variables. - -Imports config variables and overrides default class attributes. - -Validates input for overridden parameters. -""" -# Standard Library Imports -import re -from ipaddress import IPv4Address -from ipaddress import IPv6Address -from math import ceil -from typing import List -from typing import Union - -# Third Party Imports -from pydantic import BaseSettings -from pydantic import IPvAnyAddress -from pydantic import IPvAnyNetwork -from pydantic import SecretStr -from pydantic import constr -from pydantic import validator -from pydantic.color import Color - -# Project Imports -from hyperglass.constants import Supported -from hyperglass.exceptions import ConfigInvalid -from hyperglass.exceptions import UnsupportedDevice - - -def clean_name(_name): - """ - Converts any "desirable" seperators to underscore, then - removes all characters that are unsupported in Python class - variable names. Also removes leading numbers underscores. - """ - _replaced = re.sub(r"[\-|\.|\@|\~|\:\/|\s]", "_", _name) - _scrubbed = "".join(re.findall(r"([a-zA-Z]\w+|\_+)", _replaced)) - return _scrubbed.lower() - - -class Vrf(BaseSettings): - """Model for per VRF/afi config in devices.yaml""" - - display_name: str - name: str - afis: List[str] - - -class Vrfs(BaseSettings): - """Base model for vrfs class""" - - @classmethod - def import_params(cls, input_params): - """ - Imports passed dict from YAML config, removes unsupported - characters from VRF names, dynamically sets attributes for - the Vrfs class. - """ - vrfs: Vrf = { - "default": { - "display_name": "Default", - "name": "default", - "afis": ["ipv4, ipv6"], - } - } - names: List[str] = ["default"] - _all: List[str] = ["default"] - - for (vrf_key, params) in input_params.items(): - vrf = clean_name(vrf_key) - vrf_params = Vrf(**params) - vrfs.update({vrf: vrf_params.dict()}) - names.append(params.get("name")) - _all.append(vrf_key) - for (vrf_key, params) in vrfs.items(): - setattr(Vrfs, vrf_key, params) - - names: List[str] = list(set(names)) - _all: List[str] = list(set(_all)) - Vrfs.vrfs = vrfs - Vrfs.names = names - Vrfs._all = _all - return Vrfs() - - class Config: - """Pydantic Config""" - - validate_all = True - validate_assignment = True - - -class Router(BaseSettings): - """Model for per-router config in devices.yaml.""" - - address: Union[IPvAnyAddress, str] - network: str - src_addr_ipv4: IPv4Address - src_addr_ipv6: IPv6Address - credential: str - proxy: Union[str, None] = None - location: str - display_name: str - port: int - nos: str - commands: Union[str, None] = None - vrfs: List[str] = ["default"] - - @validator("nos") - def supported_nos(cls, v): # noqa: N805 - """Validates that passed nos string is supported by hyperglass""" - if not Supported.is_supported(v): - raise UnsupportedDevice(f'"{v}" device type is not supported.') - return v - - @validator("credential", "proxy", "location") - def clean_name(cls, v): # noqa: N805 - """Remove or replace unsupported characters from field values""" - return clean_name(v) - - # @validator("vrfs") - # def validate_vrfs(cls, v): - # configured_vrfs = Vrfs().names - # if v not in configured_vrfs: - # raise ConfigInvalid( - # field=v, error_msg=f"VRF must be in {str(configured_vrfs)}" - # ) - - # @validator("afis") - # def validate_afi(cls, v): # noqa: N805 - # """Validates that configured AFI is supported""" - # supported_afis = ("ipv4", "ipv6", "vpnv4", "vpnv6") - # if v.lower() not in supported_afis: - # raise ConfigInvalid( - # field=v, error_msg=f"AFI must be one of: {str(supported_afis)}" - # ) - # return v.lower() - - @validator("commands", always=True) - def validate_commands(cls, v, values): # noqa: N805 - if v is None: - v = values["nos"] - return v - - -class Routers(BaseSettings): - """Base model for devices class.""" - - @classmethod - def import_params(cls, input_params): - """ - Imports passed dict from YAML config, removes unsupported - characters from device names, dynamically sets attributes for - the Routers class. - """ - routers = {} - hostnames = [] - vrfs = set() - for (devname, params) in input_params.items(): - dev = clean_name(devname) - router_params = Router(**params) - setattr(Routers, dev, router_params) - routers.update({dev: router_params.dict()}) - hostnames.append(dev) - for vrf in router_params.dict()["vrfs"]: - vrfs.add(vrf) - Routers.routers = routers - Routers.hostnames = hostnames - Routers.vrfs = list(vrfs) - return Routers() - - class Config: - """Pydantic Config""" - - validate_all = True - validate_assignment = True - - -class Network(BaseSettings): - """Model for per-network/asn config in devices.yaml""" - - display_name: str - - -class Networks(BaseSettings): - """Base model for networks class""" - - @classmethod - def import_params(cls, input_params): - """ - Imports passed dict from YAML config, removes unsupported - characters from device names, dynamically sets attributes for - the credentials class. - """ - obj = Networks() - networks = {} - for (netname, params) in input_params.items(): - netname = clean_name(netname) - setattr(Networks, netname, Network(**params)) - networks.update({netname: Network(**params).dict()}) - Networks.networks = networks - return obj - - class Config: - """Pydantic Config""" - - validate_all = True - validate_assignment = True - - -class Credential(BaseSettings): - """Model for per-credential config in devices.yaml""" - - username: str - password: SecretStr - - -class Credentials(BaseSettings): - """Base model for credentials class""" - - @classmethod - def import_params(cls, input_params): - """ - Imports passed dict from YAML config, removes unsupported - characters from device names, dynamically sets attributes for - the credentials class. - """ - obj = Credentials() - for (credname, params) in input_params.items(): - cred = clean_name(credname) - setattr(Credentials, cred, Credential(**params)) - return obj - - class Config: - """Pydantic Config""" - - validate_all = True - validate_assignment = True - - -class Proxy(BaseSettings): - """Model for per-proxy config in devices.yaml""" - - address: Union[IPvAnyAddress, str] - port: int = 22 - username: str - password: SecretStr - nos: str - ssh_command: str - - @validator("nos") - def supported_nos(cls, v): # noqa: N805 - """ - Validates that passed nos string is supported by hyperglass. - """ - if not v == "linux_ssh": - raise UnsupportedDevice(f'"{v}" device type is not supported.') - return v - - -class Proxies(BaseSettings): - """Base model for proxies class""" - - @classmethod - def import_params(cls, input_params): - """ - Imports passed dict from YAML config, removes unsupported - characters from device names, dynamically sets attributes for - the proxies class. - """ - obj = Proxies() - for (devname, params) in input_params.items(): - dev = clean_name(devname) - setattr(Proxies, dev, Proxy(**params)) - return obj - - class Config: - """Pydantic Config""" - - validate_all = True - validate_assignment = True - - -class General(BaseSettings): - """Class model for params.general""" - - debug: bool = False - primary_asn: str = "65001" - org_name: str = "The Company" - google_analytics: Union[str, None] = None - redis_host: Union[str, IPvAnyNetwork] = "localhost" - redis_port: int = 6379 - requires_ipv6_cidr: List[str] = ["cisco_ios", "cisco_nxos"] - request_timeout: int = 15 - - -class Branding(BaseSettings): - """Class model for params.branding""" - - site_name: str = "hyperglass" - - class Colors(BaseSettings): - """Class model for params.colors""" - - primary: Color = "#40798c" - secondary: Color = "#330036" - danger: Color = "#a21024" - warning: Color = "#eec643" - light: Color = "#fbfffe" - dark: Color = "#383541" - background: Color = "#fbfffe" - - class Credit(BaseSettings): - """Class model for params.branding.credit""" - - enable: bool = True - - class Font(BaseSettings): - """Class model for params.branding.font""" - - primary: str = "Nunito" - mono: str = "Fira Code" - - class HelpMenu(BaseSettings): - """Class model for params.branding.help_menu""" - - enable: bool = True - - class Logo(BaseSettings): - """Class model for params.branding.logo""" - - path: str = "ui/images/hyperglass-dark.png" - width: int = 384 - favicons: str = "ui/images/favicons/" - - class PeeringDb(BaseSettings): - """Class model for params.branding.peering_db""" - - enable: bool = True - - class Terms(BaseSettings): - """Class model for params.branding.terms""" - - enable: bool = True - - class Text(BaseSettings): - """Class model for params.branding.text""" - - title_mode: str = "logo_only" - title: str = "hyperglass" - subtitle: str = "AS{primary_asn}" - query_location: str = "Location" - query_type: str = "Query" - query_target: str = "Target" - terms: str = "Terms" - info: str = "Help" - peeringdb = "PeeringDB" - bgp_route: str = "BGP Route" - bgp_community: str = "BGP Community" - bgp_aspath: str = "BGP AS Path" - ping: str = "Ping" - traceroute: str = "Traceroute" - vrf: str = "VRF" - - class Error404(BaseSettings): - """Class model for 404 Error Page""" - - title: str = "Error" - subtitle: str = "{uri} isn't a thing" - button: str = "Home" - - class Error500(BaseSettings): - """Class model for 500 Error Page""" - - title: str = "Error" - subtitle: str = "Something Went Wrong" - button: str = "Home" - - class Error504(BaseSettings): - """Class model for 504 Error Element""" - - message: str = "Unable to reach {target}" - - error404: Error404 = Error404() - error500: Error500 = Error500() - error504: Error504 = Error504() - - @validator("title_mode") - def check_title_mode(cls, v): - """Verifies title_mode matches supported values""" - supported_modes = ["logo_only", "text_only", "logo_title", "all"] - if v not in supported_modes: - raise ValueError("title_mode must be one of {}".format(supported_modes)) - return v - - colors: Colors = Colors() - credit: Credit = Credit() - font: Font = Font() - help_menu: HelpMenu = HelpMenu() - logo: Logo = Logo() - peering_db: PeeringDb = PeeringDb() - terms: Terms = Terms() - text: Text = Text() - - -class Messages(BaseSettings): - """Class model for params.messages""" - - 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." - 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." - ) - invalid_input: str = "{target} is not a valid {query_type} target." - invalid_field: str = "{input} is an invalid {field}." - general: str = "Something went wrong." - directed_cidr: str = "{query_type} queries can not be in CIDR format." - request_timeout: str = "Request timed out." - 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}." - no_matching_vrfs: str = "No VRFs Match" - - -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""" - - enable: bool = True - - class BgpCommunity(BaseSettings): - """Class model for params.features.bgp_community""" - - enable: bool = True - - class Regex(BaseSettings): - """Class model for params.features.bgp_community.regex""" - - decimal: str = r"^[0-9]{1,10}$" - extended_as: str = r"^([0-9]{0,5})\:([0-9]{1,5})$" - large: str = r"^([0-9]{1,10})\:([0-9]{1,10})\:[0-9]{1,10}$" - - regex: Regex = Regex() - - class BgpAsPath(BaseSettings): - """Class model for params.features.bgp_aspath""" - - enable: bool = True - - class Regex(BaseSettings): - """Class model for params.bgp_aspath.regex""" - - mode: constr(regex="asplain|asdot") = "asplain" - asplain: str = r"^(\^|^\_)(\d+\_|\d+\$|\d+\(\_\.\+\_\))+$" - asdot: str = ( - r"^(\^|^\_)((\d+\.\d+)\_|(\d+\.\d+)\$|(\d+\.\d+)\(\_\.\+\_\))+$" - ) - - regex: Regex = Regex() - - class Ping(BaseSettings): - """Class model for params.features.ping""" - - enable: bool = True - - class Traceroute(BaseSettings): - """Class model for params.features.traceroute""" - - 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""" - - redis_id: int = 0 - timeout: int = 120 - show_text: bool = True - text: str = "Results will be cached for {timeout} minutes.".format( - timeout=ceil(timeout / 60) - ) - - class MaxPrefix(BaseSettings): - """Class model for params.features.max_prefix""" - - enable: bool = False - ipv4: int = 24 - ipv6: int = 64 - message: str = ( - "Prefix length must be smaller than /{m}. {i} is too specific." - ) - - class RateLimit(BaseSettings): - """Class model for params.features.rate_limit""" - - redis_id: int = 1 - - class Query(BaseSettings): - """Class model for params.features.rate_limit.query""" - - rate: int = 5 - period: str = "minute" - title: str = "Query Limit Reached" - message: str = ( - "Query limit of {rate} per {period} reached. " - "Please wait one minute and try again." - ).format(rate=rate, period=period) - button: str = "Try Again" - - class Site(BaseSettings): - """Class model for params.features.rate_limit.site""" - - rate: int = 60 - period: str = "minute" - title: str = "Limit Reached" - subtitle: str = ( - "You have accessed this site more than {rate} " - "times in the last {period}." - ).format(rate=rate, period=period) - button: str = "Try Again" - - query: Query = Query() - site: Site = Site() - - bgp_route: BgpRoute = BgpRoute() - bgp_community: BgpCommunity = BgpCommunity() - 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() - - -class Params(BaseSettings): - """Base model for params""" - - general: General = General() - features: Features = Features() - branding: Branding = Branding() - messages: Messages = Messages() - - class Config: - """Pydantic Config""" - - validate_all = True - validate_assignment = True - - -class NosModel(BaseSettings): - """Class model for non-default commands""" - - class Dual(BaseSettings): - """Class model for non-default dual afi commands""" - - bgp_aspath: str = None - bgp_community: str = None - - class IPv4(BaseSettings): - """Class model for non-default ipv4 commands""" - - bgp_route: str = None - ping: str = None - traceroute: str = None - - class IPv6(BaseSettings): - """Class model for non-default ipv6 commands""" - - bgp_route: str = None - ping: str = None - traceroute: str = None - - dual: Dual = Dual() - ipv4: IPv4 = IPv4() - ipv6: IPv6 = IPv6() - - -class Commands(BaseSettings): - """Base class for commands class""" - - @classmethod - def import_params(cls, input_params): - """ - Imports passed dict from YAML config, dynamically sets - attributes for the commands class. - """ - obj = Commands() - for (nos, cmds) in input_params.items(): - setattr(Commands, nos, NosModel(**cmds)) - return obj - - # class CiscoIOS(BaseSettings): - # """Class model for default cisco_ios commands""" - - # class Dual(BaseSettings): - # """Default commands for dual afi commands""" - - # bgp_community: str = ( - # "show bgp all community {target} | section {afis}Network" - # ) - - # bgp_aspath: str = ( - # 'show bgp all quote-regexp "{target}" | section {afis}Network' - # ) - - # class IPv4(BaseSettings): - # """Default commands for ipv4 commands""" - - # bgp_route: str = "show bgp ipv4 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" - # ) - - # class IPv6(BaseSettings): - # """Default commands for ipv6 commands""" - - # bgp_route: str = "show bgp ipv6 unicast {target} | exclude pathid:|Epoch" - # ping: str = ( - # "ping ipv6 {target} repeat 5 source {source} | exclude Type escape" - # ) - # traceroute: str = ( - # "traceroute ipv6 {target} timeout 1 probe 2 source {source} " - # "| exclude Type escape" - # ) - - # dual: Dual = Dual() - # ipv4: IPv4 = IPv4() - # ipv6: IPv6 = IPv6() - class CiscoIOS(BaseSettings): - """Class model for default cisco_ios commands""" - - class VPNv4(BaseSettings): - """Default commands for dual afi commands""" - - bgp_community: str = "show bgp {afi} unicast vrf {vrf} community {target}" - bgp_aspath: str = 'show bgp {afi} unicast vrf {vrf} quote-regexp "{target}"' - 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" - ) - - class VPNv6(BaseSettings): - """Default commands for dual afi commands""" - - bgp_community: str = "show bgp {afi} unicast vrf {vrf} community {target}" - bgp_aspath: str = 'show bgp {afi} unicast vrf {vrf} quote-regexp "{target}"' - 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" - ) - - class IPv4(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" - ) - - class IPv6(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" - ) - traceroute: str = ( - "traceroute ipv6 {target} timeout 1 probe 2 source {source} " - "| exclude Type escape" - ) - - vpnv4: VPNv4 = VPNv4() - vpnv6: VPNv6 = VPNv6() - ipv4: IPv4 = IPv4() - ipv6: IPv6 = IPv6() - - class CiscoXR(BaseSettings): - """Class model for default cisco_xr commands""" - - class Dual(BaseSettings): - """Default commands for dual afi commands""" - - bgp_community: str = ( - "show bgp all unicast community {target} | utility egrep -v " - '"\\(BGP |Table |Non-stop\\)"' - ) - bgp_aspath: str = ( - "show bgp all unicast regexp {target} | utility egrep -v " - '"\\(BGP |Table |Non-stop\\)"' - ) - - class IPv4(BaseSettings): - """Default commands for ipv4 commands""" - - bgp_route: str = ( - "show bgp ipv4 unicast {target} | util egrep \\(BGP routing table " - "entry|Path \\#|aggregated by|Origin |Community:|validity| from \\)" - ) - ping: str = "ping ipv4 {target} count 5 source {src_addr_ipv4}" - traceroute: str = ( - "traceroute ipv4 {target} timeout 1 probe 2 source {source}" - ) - - class IPv6(BaseSettings): - """Default commands for ipv6 commands""" - - bgp_route: str = ( - "show bgp ipv6 unicast {target} | util egrep \\(BGP routing table " - "entry|Path \\#|aggregated by|Origin |Community:|validity| from \\)" - ) - ping: str = "ping ipv6 {target} count 5 source {src_addr_ipv6}" - traceroute: str = ( - "traceroute ipv6 {target} timeout 1 probe 2 source {source}" - ) - - dual: Dual = Dual() - ipv4: IPv4 = IPv4() - ipv6: IPv6 = IPv6() - - class Juniper(BaseSettings): - """Class model for default juniper commands""" - - class Dual(BaseSettings): - """Default commands for dual afi commands""" - - bgp_community = "show route protocol bgp community {target}" - bgp_aspath = "show route protocol bgp aspath-regex {target}" - - class IPv4(BaseSettings): - """Default commands for ipv4 commands""" - - bgp_route = "show route protocol bgp table inet.0 {target} detail" - ping = "ping inet {target} count 5 source {src_addr_ipv4}" - traceroute = "traceroute inet {target} wait 1 source {source}" - - class IPv6(BaseSettings): - """Default commands for ipv6 commands""" - - bgp_route = "show route protocol bgp table inet6.0 {target} detail" - ping = "ping inet6 {target} count 5 source {src_addr_ipv6}" - traceroute = "traceroute inet6 {target} wait 1 source {source}" - - dual: Dual = Dual() - ipv4: IPv4 = IPv4() - ipv6: IPv6 = IPv6() - - cisco_ios: NosModel = CiscoIOS() - cisco_xr: NosModel = CiscoXR() - juniper: NosModel = Juniper() - - class Config: - """Pydantic Config""" - - validate_all = False - validate_assignment = True diff --git a/hyperglass/configuration/models/__init__.py b/hyperglass/configuration/models/__init__.py new file mode 100644 index 0000000..53db139 --- /dev/null +++ b/hyperglass/configuration/models/__init__.py @@ -0,0 +1,7 @@ +""" +Defines models for all config variables. + +Imports config variables and overrides default class attributes. + +Validates input for overridden parameters. +""" diff --git a/hyperglass/configuration/models/_utils.py b/hyperglass/configuration/models/_utils.py new file mode 100644 index 0000000..6bca69c --- /dev/null +++ b/hyperglass/configuration/models/_utils.py @@ -0,0 +1,16 @@ +""" +Utility Functions for Pydantic Models +""" + +import re + + +def clean_name(_name): + """ + Converts any "desirable" seperators to underscore, then + removes all characters that are unsupported in Python class + variable names. Also removes leading numbers underscores. + """ + _replaced = re.sub(r"[\-|\.|\@|\~|\:\/|\s]", "_", _name) + _scrubbed = "".join(re.findall(r"([a-zA-Z]\w+|\_+)", _replaced)) + return _scrubbed.lower() diff --git a/hyperglass/configuration/models/branding.py b/hyperglass/configuration/models/branding.py new file mode 100644 index 0000000..b15421b --- /dev/null +++ b/hyperglass/configuration/models/branding.py @@ -0,0 +1,121 @@ +""" +Defines models for all Branding variables. + +Imports config variables and overrides default class attributes. + +Validates input for overridden parameters. +""" + +# Third Party Imports +from pydantic import BaseSettings +from pydantic import validator +from pydantic.color import Color + + +class Branding(BaseSettings): + """Class model for params.branding""" + + site_name: str = "hyperglass" + + class Colors(BaseSettings): + """Class model for params.colors""" + + primary: Color = "#40798c" + secondary: Color = "#330036" + danger: Color = "#a21024" + warning: Color = "#eec643" + light: Color = "#fbfffe" + dark: Color = "#383541" + background: Color = "#fbfffe" + + class Credit(BaseSettings): + """Class model for params.branding.credit""" + + enable: bool = True + + class Font(BaseSettings): + """Class model for params.branding.font""" + + primary: str = "Nunito" + mono: str = "Fira Code" + + class HelpMenu(BaseSettings): + """Class model for params.branding.help_menu""" + + enable: bool = True + + class Logo(BaseSettings): + """Class model for params.branding.logo""" + + path: str = "ui/images/hyperglass-dark.png" + width: int = 384 + favicons: str = "ui/images/favicons/" + + class PeeringDb(BaseSettings): + """Class model for params.branding.peering_db""" + + enable: bool = True + + class Terms(BaseSettings): + """Class model for params.branding.terms""" + + enable: bool = True + + class Text(BaseSettings): + """Class model for params.branding.text""" + + title_mode: str = "logo_only" + title: str = "hyperglass" + subtitle: str = "AS{primary_asn}" + query_location: str = "Location" + query_type: str = "Query" + query_target: str = "Target" + terms: str = "Terms" + info: str = "Help" + peeringdb = "PeeringDB" + bgp_route: str = "BGP Route" + bgp_community: str = "BGP Community" + bgp_aspath: str = "BGP AS Path" + ping: str = "Ping" + traceroute: str = "Traceroute" + vrf: str = "VRF" + + class Error404(BaseSettings): + """Class model for 404 Error Page""" + + title: str = "Error" + subtitle: str = "{uri} isn't a thing" + button: str = "Home" + + class Error500(BaseSettings): + """Class model for 500 Error Page""" + + title: str = "Error" + subtitle: str = "Something Went Wrong" + button: str = "Home" + + class Error504(BaseSettings): + """Class model for 504 Error Element""" + + message: str = "Unable to reach {target}" + + error404: Error404 = Error404() + error500: Error500 = Error500() + error504: Error504 = Error504() + + @validator("title_mode") + def check_title_mode(cls, v): + """Verifies title_mode matches supported values""" + supported_modes = ["logo_only", "text_only", "logo_title", "all"] + if v not in supported_modes: + raise ValueError("title_mode must be one of {}".format(supported_modes)) + return v + + colors: Colors = Colors() + credit: Credit = Credit() + font: Font = Font() + help_menu: HelpMenu = HelpMenu() + logo: Logo = Logo() + peering_db: PeeringDb = PeeringDb() + terms: Terms = Terms() + text: Text = Text() diff --git a/hyperglass/configuration/models/commands.py b/hyperglass/configuration/models/commands.py new file mode 100644 index 0000000..0426bb0 --- /dev/null +++ b/hyperglass/configuration/models/commands.py @@ -0,0 +1,208 @@ +""" +Defines models for all config variables. + +Imports config variables and overrides default class attributes. + +Validates input for overridden parameters. +""" + +# Third Party Imports +from pydantic import BaseSettings + + +class Command(BaseSettings): + """Class model for non-default commands""" + + class IPv4(BaseSettings): + """Class model for non-default dual afi commands""" + + bgp_route: str = "" + bgp_aspath: str = "" + bgp_community: str = "" + ping: str = "" + traceroute: str = "" + + class IPv6(BaseSettings): + """Class model for non-default ipv4 commands""" + + bgp_route: str = "" + bgp_aspath: str = "" + bgp_community: str = "" + ping: str = "" + traceroute: str = "" + + class VPNIPv4(BaseSettings): + """Class model for non-default ipv6 commands""" + + bgp_route: str = "" + bgp_aspath: str = "" + bgp_community: str = "" + ping: str = "" + traceroute: str = "" + + class VPNIPv6(BaseSettings): + """Class model for non-default ipv6 commands""" + + bgp_route: str = "" + bgp_aspath: str = "" + bgp_community: str = "" + ping: str = "" + traceroute: str = "" + + ipv4: IPv4 = IPv4() + ipv6: IPv6 = IPv6() + vpn_ipv4: VPNIPv4 = VPNIPv4() + vpn_ipv6: VPNIPv6 = VPNIPv6() + + +class Commands(BaseSettings): + """Base class for commands class""" + + @classmethod + def import_params(cls, input_params): + """ + Imports passed dict from YAML config, dynamically sets + attributes for the commands class. + """ + obj = Commands() + for (nos, cmds) in input_params.items(): + setattr(Commands, nos, Command(**cmds)) + return obj + + class CiscoIOS(BaseSettings): + """Class model for default cisco_ios commands""" + + class VPNv4IPv4(BaseSettings): + """Default commands for dual afi commands""" + + bgp_community: str = "show bgp {afi} unicast vrf {vrf} community {target}" + bgp_aspath: str = 'show bgp {afi} unicast vrf {vrf} quote-regexp "{target}"' + 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" + ) + + class VPNv6IPv6(BaseSettings): + """Default commands for dual afi commands""" + + bgp_community: str = "show bgp {afi} unicast vrf {vrf} community {target}" + bgp_aspath: str = 'show bgp {afi} unicast vrf {vrf} quote-regexp "{target}"' + 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" + ) + + class IPv4(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" + ) + + class IPv6(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" + ) + traceroute: str = ( + "traceroute ipv6 {target} timeout 1 probe 2 source {source} " + "| exclude Type escape" + ) + + ipv4: IPv4 = IPv4() + ipv6: IPv6 = IPv6() + vpn_ipv4: VPNv4IPv4 = VPNv4IPv4() + vpn_ipv6: VPNv6IPv6 = VPNv6IPv6() + + class CiscoXR(BaseSettings): + """Class model for default cisco_xr commands""" + + class Dual(BaseSettings): + """Default commands for dual afi commands""" + + bgp_community: str = ( + "show bgp all unicast community {target} | utility egrep -v " + '"\\(BGP |Table |Non-stop\\)"' + ) + bgp_aspath: str = ( + "show bgp all unicast regexp {target} | utility egrep -v " + '"\\(BGP |Table |Non-stop\\)"' + ) + + class IPv4(BaseSettings): + """Default commands for ipv4 commands""" + + bgp_route: str = ( + "show bgp ipv4 unicast {target} | util egrep \\(BGP routing table " + "entry|Path \\#|aggregated by|Origin |Community:|validity| from \\)" + ) + ping: str = "ping ipv4 {target} count 5 source {src_addr_ipv4}" + traceroute: str = ( + "traceroute ipv4 {target} timeout 1 probe 2 source {source}" + ) + + class IPv6(BaseSettings): + """Default commands for ipv6 commands""" + + bgp_route: str = ( + "show bgp ipv6 unicast {target} | util egrep \\(BGP routing table " + "entry|Path \\#|aggregated by|Origin |Community:|validity| from \\)" + ) + ping: str = "ping ipv6 {target} count 5 source {src_addr_ipv6}" + traceroute: str = ( + "traceroute ipv6 {target} timeout 1 probe 2 source {source}" + ) + + dual: Dual = Dual() + ipv4: IPv4 = IPv4() + ipv6: IPv6 = IPv6() + + class Juniper(BaseSettings): + """Class model for default juniper commands""" + + class Dual(BaseSettings): + """Default commands for dual afi commands""" + + bgp_community = "show route protocol bgp community {target}" + bgp_aspath = "show route protocol bgp aspath-regex {target}" + + class IPv4(BaseSettings): + """Default commands for ipv4 commands""" + + bgp_route = "show route protocol bgp table inet.0 {target} detail" + ping = "ping inet {target} count 5 source {src_addr_ipv4}" + traceroute = "traceroute inet {target} wait 1 source {source}" + + class IPv6(BaseSettings): + """Default commands for ipv6 commands""" + + bgp_route = "show route protocol bgp table inet6.0 {target} detail" + ping = "ping inet6 {target} count 5 source {src_addr_ipv6}" + traceroute = "traceroute inet6 {target} wait 1 source {source}" + + dual: Dual = Dual() + ipv4: IPv4 = IPv4() + ipv6: IPv6 = IPv6() + + cisco_ios: Command = CiscoIOS() + cisco_xr: Command = CiscoXR() + juniper: Command = Juniper() + + class Config: + """Pydantic Config""" + + validate_all = False + validate_assignment = True diff --git a/hyperglass/configuration/models/credentials.py b/hyperglass/configuration/models/credentials.py new file mode 100644 index 0000000..ba2ed15 --- /dev/null +++ b/hyperglass/configuration/models/credentials.py @@ -0,0 +1,44 @@ +""" +Defines models for Credential config variables. + +Imports config variables and overrides default class attributes. + +Validates input for overridden parameters. +""" + +# Third Party Imports +from pydantic import BaseSettings +from pydantic import SecretStr + +# Project Imports +from hyperglass.configuration.models._utils import clean_name + + +class Credential(BaseSettings): + """Model for per-credential config in devices.yaml""" + + username: str + password: SecretStr + + +class Credentials(BaseSettings): + """Base model for credentials class""" + + @classmethod + def import_params(cls, input_params): + """ + Imports passed dict from YAML config, removes unsupported + characters from device names, dynamically sets attributes for + the credentials class. + """ + obj = Credentials() + for (credname, params) in input_params.items(): + cred = clean_name(credname) + setattr(Credentials, cred, Credential(**params)) + return obj + + class Config: + """Pydantic Config""" + + validate_all = True + validate_assignment = True diff --git a/hyperglass/configuration/models/features.py b/hyperglass/configuration/models/features.py new file mode 100644 index 0000000..824c043 --- /dev/null +++ b/hyperglass/configuration/models/features.py @@ -0,0 +1,145 @@ +""" +Defines models for all Features variables. + +Imports config variables and overrides default class attributes. + +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""" + + enable: bool = True + + class BgpCommunity(BaseSettings): + """Class model for params.features.bgp_community""" + + enable: bool = True + + class Regex(BaseSettings): + """Class model for params.features.bgp_community.regex""" + + decimal: str = r"^[0-9]{1,10}$" + extended_as: str = r"^([0-9]{0,5})\:([0-9]{1,5})$" + large: str = r"^([0-9]{1,10})\:([0-9]{1,10})\:[0-9]{1,10}$" + + regex: Regex = Regex() + + class BgpAsPath(BaseSettings): + """Class model for params.features.bgp_aspath""" + + enable: bool = True + + class Regex(BaseSettings): + """Class model for params.bgp_aspath.regex""" + + mode: constr(regex="asplain|asdot") = "asplain" + asplain: str = r"^(\^|^\_)(\d+\_|\d+\$|\d+\(\_\.\+\_\))+$" + asdot: str = ( + r"^(\^|^\_)((\d+\.\d+)\_|(\d+\.\d+)\$|(\d+\.\d+)\(\_\.\+\_\))+$" + ) + + regex: Regex = Regex() + + class Ping(BaseSettings): + """Class model for params.features.ping""" + + enable: bool = True + + class Traceroute(BaseSettings): + """Class model for params.features.traceroute""" + + 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""" + + redis_id: int = 0 + timeout: int = 120 + show_text: bool = True + text: str = "Results will be cached for {timeout} minutes.".format( + timeout=ceil(timeout / 60) + ) + + class MaxPrefix(BaseSettings): + """Class model for params.features.max_prefix""" + + enable: bool = False + ipv4: int = 24 + ipv6: int = 64 + message: str = ( + "Prefix length must be smaller than /{m}. {i} is too specific." + ) + + class RateLimit(BaseSettings): + """Class model for params.features.rate_limit""" + + redis_id: int = 1 + + class Query(BaseSettings): + """Class model for params.features.rate_limit.query""" + + rate: int = 5 + period: str = "minute" + title: str = "Query Limit Reached" + message: str = ( + "Query limit of {rate} per {period} reached. " + "Please wait one minute and try again." + ).format(rate=rate, period=period) + button: str = "Try Again" + + class Site(BaseSettings): + """Class model for params.features.rate_limit.site""" + + rate: int = 60 + period: str = "minute" + title: str = "Limit Reached" + subtitle: str = ( + "You have accessed this site more than {rate} " + "times in the last {period}." + ).format(rate=rate, period=period) + button: str = "Try Again" + + query: Query = Query() + site: Site = Site() + + bgp_route: BgpRoute = BgpRoute() + bgp_community: BgpCommunity = BgpCommunity() + 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/general.py b/hyperglass/configuration/models/general.py new file mode 100644 index 0000000..ace686f --- /dev/null +++ b/hyperglass/configuration/models/general.py @@ -0,0 +1,27 @@ +""" +Defines models for General config variables. + +Imports config variables and overrides default class attributes. + +Validates input for overridden parameters. +""" +# Standard Library Imports +from typing import List +from typing import Union + +# Third Party Imports +from pydantic import BaseSettings +from pydantic import IPvAnyNetwork + + +class General(BaseSettings): + """Class model for params.general""" + + debug: bool = False + primary_asn: str = "65001" + org_name: str = "The Company" + google_analytics: Union[str, None] = None + redis_host: Union[str, IPvAnyNetwork] = "localhost" + redis_port: int = 6379 + requires_ipv6_cidr: List[str] = ["cisco_ios", "cisco_nxos"] + request_timeout: int = 15 diff --git a/hyperglass/configuration/models/messages.py b/hyperglass/configuration/models/messages.py new file mode 100644 index 0000000..e0e9d91 --- /dev/null +++ b/hyperglass/configuration/models/messages.py @@ -0,0 +1,35 @@ +""" +Defines models for Messages config variables. + +Imports config variables and overrides default class attributes. + +Validates input for overridden parameters. +""" + +# Third Party Imports +from pydantic import BaseSettings + + +class Messages(BaseSettings): + """Class model for params.messages""" + + 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." + 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." + ) + invalid_input: str = "{target} is not a valid {query_type} target." + invalid_field: str = "{input} is an invalid {field}." + general: str = "Something went wrong." + directed_cidr: str = "{query_type} queries can not be in CIDR format." + request_timeout: str = "Request timed out." + 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}." + no_matching_vrfs: str = "No VRFs Match" diff --git a/hyperglass/configuration/models/networks.py b/hyperglass/configuration/models/networks.py new file mode 100644 index 0000000..8aaebec --- /dev/null +++ b/hyperglass/configuration/models/networks.py @@ -0,0 +1,45 @@ +""" +Defines models for Networks config variables. + +Imports config variables and overrides default class attributes. + +Validates input for overridden parameters. +""" + +# Third Party Imports +from pydantic import BaseSettings + +# Project Imports +from hyperglass.configuration.models._utils import clean_name + + +class Network(BaseSettings): + """Model for per-network/asn config in devices.yaml""" + + display_name: str + + +class Networks(BaseSettings): + """Base model for networks class""" + + @classmethod + def import_params(cls, input_params): + """ + Imports passed dict from YAML config, removes unsupported + characters from device names, dynamically sets attributes for + the credentials class. + """ + obj = Networks() + networks = {} + for (netname, params) in input_params.items(): + netname = clean_name(netname) + setattr(Networks, netname, Network(**params)) + networks.update({netname: Network(**params).dict()}) + Networks.networks = networks + return obj + + class Config: + """Pydantic Config""" + + validate_all = True + validate_assignment = True diff --git a/hyperglass/configuration/models/params.py b/hyperglass/configuration/models/params.py new file mode 100644 index 0000000..20b1cef --- /dev/null +++ b/hyperglass/configuration/models/params.py @@ -0,0 +1,29 @@ +""" +Defines models for all Params variables. + +Imports config variables and overrides default class attributes. + +Validates input for overridden parameters. +""" + +# Third Party Imports +from pydantic import BaseSettings +from hyperglass.configuration.models.branding import Branding +from hyperglass.configuration.models.features import Features +from hyperglass.configuration.models.general import General +from hyperglass.configuration.models.messages import Messages + + +class Params(BaseSettings): + """Base model for params""" + + general: General = General() + features: Features = Features() + branding: Branding = Branding() + messages: Messages = Messages() + + class Config: + """Pydantic Config""" + + validate_all = True + validate_assignment = True diff --git a/hyperglass/configuration/models/proxies.py b/hyperglass/configuration/models/proxies.py new file mode 100644 index 0000000..3dbb710 --- /dev/null +++ b/hyperglass/configuration/models/proxies.py @@ -0,0 +1,62 @@ +""" +Defines models for Router config variables. + +Imports config variables and overrides default class attributes. + +Validates input for overridden parameters. +""" +# Standard Library Imports +from typing import Union + +# Third Party Imports +from pydantic import BaseSettings +from pydantic import IPvAnyAddress +from pydantic import SecretStr +from pydantic import validator + +# Project Imports +from hyperglass.configuration.models._utils import clean_name +from hyperglass.exceptions import UnsupportedDevice + + +class Proxy(BaseSettings): + """Model for per-proxy config in devices.yaml""" + + address: Union[IPvAnyAddress, str] + port: int = 22 + username: str + password: SecretStr + nos: str + ssh_command: str + + @validator("nos") + def supported_nos(cls, v): # noqa: N805 + """ + Validates that passed nos string is supported by hyperglass. + """ + if not v == "linux_ssh": + raise UnsupportedDevice(f'"{v}" device type is not supported.') + return v + + +class Proxies(BaseSettings): + """Base model for proxies class""" + + @classmethod + def import_params(cls, input_params): + """ + Imports passed dict from YAML config, removes unsupported + characters from device names, dynamically sets attributes for + the proxies class. + """ + obj = Proxies() + for (devname, params) in input_params.items(): + dev = clean_name(devname) + setattr(Proxies, dev, Proxy(**params)) + return obj + + class Config: + """Pydantic Config""" + + validate_all = True + validate_assignment = True diff --git a/hyperglass/configuration/models/routers.py b/hyperglass/configuration/models/routers.py new file mode 100644 index 0000000..762a5ce --- /dev/null +++ b/hyperglass/configuration/models/routers.py @@ -0,0 +1,90 @@ +""" +Defines models for Router config variables. + +Imports config variables and overrides default class attributes. + +Validates input for overridden parameters. +""" +# Standard Library Imports +from ipaddress import IPv4Address +from ipaddress import IPv6Address +from typing import List +from typing import Union + +# Third Party Imports +from pydantic import BaseSettings +from pydantic import IPvAnyAddress +from pydantic import validator + +# Project Imports +from hyperglass.configuration.models._utils import clean_name +from hyperglass.constants import Supported +from hyperglass.exceptions import UnsupportedDevice + + +class Router(BaseSettings): + """Model for per-router config in devices.yaml.""" + + address: Union[IPvAnyAddress, str] + network: str + src_addr_ipv4: IPv4Address + src_addr_ipv6: IPv6Address + credential: str + proxy: Union[str, None] = None + location: str + display_name: str + port: int + nos: str + commands: Union[str, None] = None + vrfs: List[str] = ["default"] + + @validator("nos") + def supported_nos(cls, v): # noqa: N805 + """Validates that passed nos string is supported by hyperglass""" + if not Supported.is_supported(v): + raise UnsupportedDevice(f'"{v}" device type is not supported.') + return v + + @validator("credential", "proxy", "location") + def clean_name(cls, v): # noqa: N805 + """Remove or replace unsupported characters from field values""" + return clean_name(v) + + @validator("commands", always=True) + def validate_commands(cls, v, values): # noqa: N805 + if v is None: + v = values["nos"] + return v + + +class Routers(BaseSettings): + """Base model for devices class.""" + + @classmethod + def import_params(cls, input_params): + """ + Imports passed dict from YAML config, removes unsupported + characters from device names, dynamically sets attributes for + the Routers class. + """ + routers = {} + hostnames = [] + vrfs = set() + for (devname, params) in input_params.items(): + dev = clean_name(devname) + router_params = Router(**params) + setattr(Routers, dev, router_params) + routers.update({dev: router_params.dict()}) + hostnames.append(dev) + for vrf in router_params.dict()["vrfs"]: + vrfs.add(vrf) + Routers.routers = routers + Routers.hostnames = hostnames + Routers.vrfs = list(vrfs) + return Routers() + + class Config: + """Pydantic Config""" + + validate_all = True + validate_assignment = True diff --git a/hyperglass/configuration/models/vrfs.py b/hyperglass/configuration/models/vrfs.py new file mode 100644 index 0000000..e8aff34 --- /dev/null +++ b/hyperglass/configuration/models/vrfs.py @@ -0,0 +1,66 @@ +""" +Defines models for VRF config variables. + +Imports config variables and overrides default class attributes. + +Validates input for overridden parameters. +""" +# Standard Library Imports +from typing import List + +# Third Party Imports +from pydantic import BaseSettings + +# Project Imports +from hyperglass.configuration.models._utils import clean_name + + +class Vrf(BaseSettings): + """Model for per VRF/afi config in devices.yaml""" + + display_name: str + name: str + afis: List[str] + + +class Vrfs(BaseSettings): + """Base model for vrfs class""" + + @classmethod + def import_params(cls, input_params): + """ + Imports passed dict from YAML config, removes unsupported + characters from VRF names, dynamically sets attributes for + the Vrfs class. + """ + vrfs: Vrf = { + "default": { + "display_name": "Default", + "name": "default", + "afis": ["ipv4, ipv6"], + } + } + names: List[str] = ["default"] + _all: List[str] = ["default"] + + for (vrf_key, params) in input_params.items(): + vrf = clean_name(vrf_key) + vrf_params = Vrf(**params) + vrfs.update({vrf: vrf_params.dict()}) + names.append(params.get("name")) + _all.append(vrf_key) + for (vrf_key, params) in vrfs.items(): + setattr(Vrfs, vrf_key, params) + + names: List[str] = list(set(names)) + _all: List[str] = list(set(_all)) + Vrfs.vrfs = vrfs + Vrfs.names = names + Vrfs._all = _all + return Vrfs() + + class Config: + """Pydantic Config""" + + validate_all = True + validate_assignment = True