diff --git a/hyperglass/command/__init__.py b/hyperglass/command/__init__.py index 336b986..1b910f7 100644 --- a/hyperglass/command/__init__.py +++ b/hyperglass/command/__init__.py @@ -1,4 +1,5 @@ -""" +"""Validate, construct, execute queries. + Constructs SSH commands or API call parameters based on front end input, executes the commands/calls, returns the output to front end. """ diff --git a/hyperglass/command/construct.py b/hyperglass/command/construct.py index a8dba84..4eadb3a 100644 --- a/hyperglass/command/construct.py +++ b/hyperglass/command/construct.py @@ -1,29 +1,25 @@ -""" +"""Construct SSH command/API parameters from validated query data. + Accepts filtered & validated input from execute.py, constructs SSH command for Netmiko library or API call parameters for supported hyperglass API modules. """ + # Standard Library Imports import ipaddress import json import operator import re -# Third Party Imports -from logzero import logger as log - # Project Imports from hyperglass.configuration import commands -from hyperglass.configuration import logzero_config # NOQA: F401 from hyperglass.constants import target_format_space from hyperglass.exceptions import HyperglassError +from hyperglass.util import log class Construct: - """ - Constructs SSH commands or REST API queries based on validated - input parameters. - """ + """Construct SSH commands/REST API parameters from validated query data.""" def get_device_vrf(self): _device_vrf = None diff --git a/hyperglass/command/encode.py b/hyperglass/command/encode.py index 4261a93..92e4770 100644 --- a/hyperglass/command/encode.py +++ b/hyperglass/command/encode.py @@ -1,3 +1,5 @@ +"""Handle JSON Web Token Encoding & Decoding.""" + # Standard Library Imports import datetime @@ -9,7 +11,7 @@ from hyperglass.exceptions import RestError async def jwt_decode(payload, secret): - """Decode & validate an encoded JSON Web Token (JWT)""" + """Decode & validate an encoded JSON Web Token (JWT).""" try: decoded = jwt.decode(payload, secret, algorithm="HS256") decoded = decoded["payload"] @@ -19,7 +21,7 @@ async def jwt_decode(payload, secret): async def jwt_encode(payload, secret, duration): - """Encode a query to a JSON Web Token (JWT)""" + """Encode a query to a JSON Web Token (JWT).""" token = { "payload": payload, "nbf": datetime.datetime.utcnow(), diff --git a/hyperglass/command/execute.py b/hyperglass/command/execute.py index fbefff1..914b795 100644 --- a/hyperglass/command/execute.py +++ b/hyperglass/command/execute.py @@ -1,4 +1,5 @@ -""" +"""Execute validated & constructed query on device. + Accepts input from front end application, validates the input and returns errors if input is invalid. Passes validated parameters to construct.py, which is used to build & run the Netmiko connectoins or @@ -11,7 +12,6 @@ import re # Third Party Imports import httpx import sshtunnel -from logzero import logger as log from netmiko import ConnectHandler from netmiko import NetMikoAuthenticationException from netmiko import NetmikoAuthError @@ -20,10 +20,10 @@ from netmiko import NetMikoTimeoutException # Project Imports from hyperglass.command.construct import Construct +from hyperglass.command.encode import jwt_decode +from hyperglass.command.encode import jwt_encode from hyperglass.command.validate import Validate -from hyperglass.command.encode import jwt_decode, jwt_encode from hyperglass.configuration import devices -from hyperglass.configuration import logzero_config # noqa: F401 from hyperglass.configuration import params from hyperglass.constants import Supported from hyperglass.constants import protocol_map @@ -32,6 +32,7 @@ from hyperglass.exceptions import DeviceTimeout from hyperglass.exceptions import ResponseEmpty from hyperglass.exceptions import RestError from hyperglass.exceptions import ScrapeError +from hyperglass.util import log class Connect: @@ -219,7 +220,6 @@ class Connect: """Sends HTTP POST to router running a hyperglass API agent""" log.debug(f"Query parameters: {self.query}") - # uri = Supported.map_rest(self.device.nos) headers = {"Content-Type": "application/json"} http_protocol = protocol_map.get(self.device.port, "https") endpoint = "{protocol}://{addr}:{port}/query".format( diff --git a/hyperglass/command/validate.py b/hyperglass/command/validate.py index d9c9ccb..1cde1b7 100644 --- a/hyperglass/command/validate.py +++ b/hyperglass/command/validate.py @@ -1,4 +1,5 @@ -""" +"""Validate query data. + Accepts raw input data from execute.py, passes it through specific filters based on query type, returns validity boolean and specific error message. @@ -7,15 +8,12 @@ error message. import ipaddress import re -# Third Party Imports -from logzero import logger as log - # Project Imports -from hyperglass.configuration import logzero_config # noqa: F401 from hyperglass.configuration import params from hyperglass.exceptions import HyperglassError from hyperglass.exceptions import InputInvalid from hyperglass.exceptions import InputNotAllowed +from hyperglass.util import log class IPType: diff --git a/hyperglass/configuration/__init__.py b/hyperglass/configuration/__init__.py index f25fb7e..34705b7 100644 --- a/hyperglass/configuration/__init__.py +++ b/hyperglass/configuration/__init__.py @@ -1,24 +1,22 @@ -""" -Imports configuration varibles from configuration files and returns -default values if undefined. -""" +"""Import configuration files and returns default values if undefined.""" # Standard Library Imports from pathlib import Path # Third Party Imports -import logzero import yaml -from logzero import logger as log from pydantic import ValidationError # Project Imports from hyperglass.configuration.models import commands as _commands from hyperglass.configuration.models import params as _params from hyperglass.configuration.models import routers as _routers +from hyperglass.constants import LOG_HANDLER +from hyperglass.constants import LOG_LEVELS from hyperglass.exceptions import ConfigError from hyperglass.exceptions import ConfigInvalid from hyperglass.exceptions import ConfigMissing +from hyperglass.util import log # Project Directories working_dir = Path(__file__).resolve().parent @@ -82,19 +80,14 @@ except ValidationError as validation_errors: ) -# Logzero Configuration -log_level = 20 +# Logging Config if params.general.debug: - log_level = 10 -log_format = ( - "%(color)s[%(asctime)s.%(msecs)03d %(module)s:%(funcName)s:%(lineno)d " - "%(levelname)s]%(end_color)s %(message)s" -) -date_format = "%Y-%m-%d %H:%M:%S" -logzero_formatter = logzero.LogFormatter(fmt=log_format, datefmt=date_format) -logzero_config = logzero.setup_default_logger( - formatter=logzero_formatter, level=log_level -) + _log_level = "DEBUG" + LOG_HANDLER["level"] = _log_level + log.remove() + log.configure(handlers=[LOG_HANDLER], levels=LOG_LEVELS) + +log.debug("Debugging Enabled") def build_frontend_networks(): diff --git a/hyperglass/configuration/models/_utils.py b/hyperglass/configuration/models/_utils.py index c07a0f0..98006d4 100644 --- a/hyperglass/configuration/models/_utils.py +++ b/hyperglass/configuration/models/_utils.py @@ -1,6 +1,4 @@ -""" -Utility Functions for Pydantic Models -""" +"""Utility Functions for Pydantic Models.""" # Standard Library Imports import re @@ -10,10 +8,17 @@ from pydantic import BaseSettings 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. + """Remove unsupported characters from field names. + + Converts any "desirable" seperators to underscore, then removes all + characters that are unsupported in Python class variable names. + Also removes leading numbers underscores. + + Arguments: + _name {str} -- Initial field name + + Returns: + {str} -- Cleaned field name """ _replaced = re.sub(r"[\-|\.|\@|\~|\:\/|\s]", "_", _name) _scrubbed = "".join(re.findall(r"([a-zA-Z]\w+|\_+)", _replaced)) @@ -21,12 +26,15 @@ def clean_name(_name): class HyperglassModel(BaseSettings): - """Base model for all hyperglass configuration models""" + """Base model for all hyperglass configuration models.""" pass class Config: - """Default pydantic configuration""" + """Default Pydantic configuration. + + See https://pydantic-docs.helpmanual.io/usage/model_config + """ validate_all = True extra = "forbid" @@ -35,11 +43,11 @@ class HyperglassModel(BaseSettings): class HyperglassModelExtra(HyperglassModel): - """Model for hyperglass configuration models with dynamic fields""" + """Model for hyperglass configuration models with dynamic fields.""" pass class Config: - """Default pydantic configuration""" + """Default pydantic configuration.""" extra = "allow" diff --git a/hyperglass/configuration/models/branding.py b/hyperglass/configuration/models/branding.py index 60991d2..4155420 100644 --- a/hyperglass/configuration/models/branding.py +++ b/hyperglass/configuration/models/branding.py @@ -1,10 +1,4 @@ -""" -Defines models for all Branding variables. - -Imports config variables and overrides default class attributes. - -Validates input for overridden parameters. -""" +"""Validate branding configuration variables.""" # Third Party Imports from pydantic import constr diff --git a/hyperglass/configuration/models/commands.py b/hyperglass/configuration/models/commands.py index bb8ffb1..0e9f803 100644 --- a/hyperglass/configuration/models/commands.py +++ b/hyperglass/configuration/models/commands.py @@ -1,10 +1,5 @@ -""" -Defines models for all config variables. +"""Validate command configuration variables.""" -Imports config variables and overrides default class attributes. - -Validates input for overridden parameters. -""" # Disable string length warnings so I can actually read these commands # flake8: noqa: E501 diff --git a/hyperglass/configuration/models/credentials.py b/hyperglass/configuration/models/credentials.py index d36734d..abe22f7 100644 --- a/hyperglass/configuration/models/credentials.py +++ b/hyperglass/configuration/models/credentials.py @@ -1,10 +1,4 @@ -""" -Defines models for Credential config variables. - -Imports config variables and overrides default class attributes. - -Validates input for overridden parameters. -""" +"""Validate credential configuration variables.""" # Third Party Imports from pydantic import SecretStr @@ -15,14 +9,14 @@ from hyperglass.configuration.models._utils import clean_name class Credential(HyperglassModel): - """Model for per-credential config in devices.yaml""" + """Model for per-credential config in devices.yaml.""" username: str password: SecretStr class Credentials(HyperglassModel): - """Base model for credentials class""" + """Base model for credentials class.""" @classmethod def import_params(cls, input_params): diff --git a/hyperglass/configuration/models/features.py b/hyperglass/configuration/models/features.py index c4d623e..a8ba358 100644 --- a/hyperglass/configuration/models/features.py +++ b/hyperglass/configuration/models/features.py @@ -1,10 +1,5 @@ -""" -Defines models for all Features variables. +"""Validate feature configuration variables.""" -Imports config variables and overrides default class attributes. - -Validates input for overridden parameters. -""" # Standard Library Imports from math import ceil @@ -16,20 +11,20 @@ from hyperglass.configuration.models._utils import HyperglassModel class Features(HyperglassModel): - """Class model for params.features""" + """Validation model for params.features.""" class BgpRoute(HyperglassModel): - """Class model for params.features.bgp_route""" + """Validation model for params.features.bgp_route.""" enable: bool = True class BgpCommunity(HyperglassModel): - """Class model for params.features.bgp_community""" + """Validation model for params.features.bgp_community.""" enable: bool = True class Regex(HyperglassModel): - """Class model for params.features.bgp_community.regex""" + """Validation 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})$" @@ -38,12 +33,12 @@ class Features(HyperglassModel): regex: Regex = Regex() class BgpAsPath(HyperglassModel): - """Class model for params.features.bgp_aspath""" + """Validation model for params.features.bgp_aspath.""" enable: bool = True class Regex(HyperglassModel): - """Class model for params.bgp_aspath.regex""" + """Validation model for params.bgp_aspath.regex.""" mode: constr(regex="asplain|asdot") = "asplain" asplain: str = r"^(\^|^\_)(\d+\_|\d+\$|\d+\(\_\.\+\_\))+$" @@ -54,17 +49,17 @@ class Features(HyperglassModel): regex: Regex = Regex() class Ping(HyperglassModel): - """Class model for params.features.ping""" + """Validation model for params.features.ping.""" enable: bool = True class Traceroute(HyperglassModel): - """Class model for params.features.traceroute""" + """Validation model for params.features.traceroute.""" enable: bool = True class Cache(HyperglassModel): - """Class model for params.features.cache""" + """Validation model for params.features.cache.""" redis_id: int = 0 timeout: int = 120 @@ -74,7 +69,7 @@ class Features(HyperglassModel): ) class MaxPrefix(HyperglassModel): - """Class model for params.features.max_prefix""" + """Validation model for params.features.max_prefix.""" enable: bool = False ipv4: int = 24 @@ -84,12 +79,12 @@ class Features(HyperglassModel): ) class RateLimit(HyperglassModel): - """Class model for params.features.rate_limit""" + """Validation model for params.features.rate_limit.""" redis_id: int = 1 class Query(HyperglassModel): - """Class model for params.features.rate_limit.query""" + """Validation model for params.features.rate_limit.query.""" rate: int = 5 period: str = "minute" @@ -101,7 +96,7 @@ class Features(HyperglassModel): button: str = "Try Again" class Site(HyperglassModel): - """Class model for params.features.rate_limit.site""" + """Validation model for params.features.rate_limit.site.""" rate: int = 60 period: str = "minute" diff --git a/hyperglass/configuration/models/general.py b/hyperglass/configuration/models/general.py index 7d0a0d3..6952096 100644 --- a/hyperglass/configuration/models/general.py +++ b/hyperglass/configuration/models/general.py @@ -1,10 +1,5 @@ -""" -Defines models for General config variables. +"""Validate general configuration variables.""" -Imports config variables and overrides default class attributes. - -Validates input for overridden parameters. -""" # Standard Library Imports from typing import List @@ -13,7 +8,7 @@ from hyperglass.configuration.models._utils import HyperglassModel class General(HyperglassModel): - """Class model for params.general""" + """Validation model for params.general.""" debug: bool = False primary_asn: str = "65001" diff --git a/hyperglass/configuration/models/messages.py b/hyperglass/configuration/models/messages.py index 45c3053..b98a079 100644 --- a/hyperglass/configuration/models/messages.py +++ b/hyperglass/configuration/models/messages.py @@ -1,17 +1,11 @@ -""" -Defines models for Messages config variables. - -Imports config variables and overrides default class attributes. - -Validates input for overridden parameters. -""" +"""Validate error message configuration variables.""" # Project Imports from hyperglass.configuration.models._utils import HyperglassModel class Messages(HyperglassModel): - """Class model for params.messages""" + """Validation model for params.messages.""" no_input: str = "{field} must be specified." acl_denied: str = "{target} is a member of {denied_network}, which is not allowed." diff --git a/hyperglass/configuration/models/networks.py b/hyperglass/configuration/models/networks.py index 5118f8d..5d74981 100644 --- a/hyperglass/configuration/models/networks.py +++ b/hyperglass/configuration/models/networks.py @@ -1,10 +1,4 @@ -""" -Defines models for Networks config variables. - -Imports config variables and overrides default class attributes. - -Validates input for overridden parameters. -""" +"""Validate network configuration variables.""" # Project Imports from hyperglass.configuration.models._utils import HyperglassModel @@ -12,14 +6,14 @@ from hyperglass.configuration.models._utils import clean_name class Network(HyperglassModel): - """Model for per-network/asn config in devices.yaml""" + """Validation Model for per-network/asn config in devices.yaml.""" name: str display_name: str class Networks(HyperglassModel): - """Base model for networks class""" + """Base model for networks class.""" @classmethod def import_params(cls, input_params): diff --git a/hyperglass/configuration/models/params.py b/hyperglass/configuration/models/params.py index 5100b62..731f349 100644 --- a/hyperglass/configuration/models/params.py +++ b/hyperglass/configuration/models/params.py @@ -1,10 +1,4 @@ -""" -Defines models for all Params variables. - -Imports config variables and overrides default class attributes. - -Validates input for overridden parameters. -""" +"""Configuration validation entry point.""" # Project Imports from hyperglass.configuration.models._utils import HyperglassModel @@ -15,7 +9,7 @@ from hyperglass.configuration.models.messages import Messages class Params(HyperglassModel): - """Base model for params""" + """Validation model for all configuration variables.""" general: General = General() features: Features = Features() diff --git a/hyperglass/configuration/models/proxies.py b/hyperglass/configuration/models/proxies.py index d8f39ca..d50f6b0 100644 --- a/hyperglass/configuration/models/proxies.py +++ b/hyperglass/configuration/models/proxies.py @@ -1,10 +1,4 @@ -""" -Defines models for Router config variables. - -Imports config variables and overrides default class attributes. - -Validates input for overridden parameters. -""" +"""Validate SSH proxy configuration variables.""" # Third Party Imports from pydantic import validator @@ -17,7 +11,7 @@ from hyperglass.exceptions import UnsupportedDevice class Proxy(HyperglassModel): - """Model for per-proxy config in devices.yaml""" + """Validation model for per-proxy config in devices.yaml.""" name: str address: str @@ -26,17 +20,17 @@ class Proxy(HyperglassModel): nos: str = "linux_ssh" @validator("nos") - def supported_nos(cls, v): # noqa: N805 + def supported_nos(cls, value): # 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 + if not value == "linux_ssh": + raise UnsupportedDevice(f'"{value}" device type is not supported.') + return value class Proxies(HyperglassModel): - """Base model for proxies class""" + """Validation model for SSH proxy configuration.""" @classmethod def import_params(cls, input_params): diff --git a/hyperglass/configuration/models/routers.py b/hyperglass/configuration/models/routers.py index 22b3336..729926b 100644 --- a/hyperglass/configuration/models/routers.py +++ b/hyperglass/configuration/models/routers.py @@ -1,10 +1,5 @@ -""" -Defines models for Router config variables. +"""Validate router configuration variables.""" -Imports config variables and overrides default class attributes. - -Validates input for overridden parameters. -""" # Standard Library Imports import re from typing import List @@ -12,7 +7,6 @@ from typing import Union # Third Party Imports from pydantic import validator -from logzero import logger as log # Project Imports from hyperglass.configuration.models._utils import HyperglassModel @@ -22,14 +16,15 @@ from hyperglass.configuration.models.commands import Command from hyperglass.configuration.models.credentials import Credential from hyperglass.configuration.models.networks import Network from hyperglass.configuration.models.proxies import Proxy -from hyperglass.configuration.models.vrfs import Vrf, DefaultVrf +from hyperglass.configuration.models.vrfs import DefaultVrf, Vrf from hyperglass.constants import Supported from hyperglass.exceptions import ConfigError from hyperglass.exceptions import UnsupportedDevice +from hyperglass.util import log class Router(HyperglassModel): - """Model for per-router config in devices.yaml.""" + """Validation model for per-router config in devices.yaml.""" name: str address: str @@ -129,7 +124,7 @@ class Router(HyperglassModel): class Routers(HyperglassModelExtra): - """Base model for devices class.""" + """Validation model for device configurations.""" hostnames: List[str] = [] vrfs: List[str] = [] diff --git a/hyperglass/configuration/models/vrfs.py b/hyperglass/configuration/models/vrfs.py index a7d9023..6cdbe57 100644 --- a/hyperglass/configuration/models/vrfs.py +++ b/hyperglass/configuration/models/vrfs.py @@ -1,10 +1,5 @@ -""" -Defines models for VRF config variables. +"""Validate VRF configuration variables.""" -Imports config variables and overrides default class attributes. - -Validates input for overridden parameters. -""" # Standard Library Imports from ipaddress import IPv4Address from ipaddress import IPv4Network @@ -25,7 +20,7 @@ from hyperglass.exceptions import ConfigError class DeviceVrf4(HyperglassModel): - """Model for AFI definitions""" + """Validation model for IPv4 AFI definitions.""" vrf_name: str source_address: IPv4Address @@ -46,7 +41,7 @@ class DeviceVrf4(HyperglassModel): class DeviceVrf6(HyperglassModel): - """Model for AFI definitions""" + """Validation model for IPv6 AFI definitions.""" vrf_name: str source_address: IPv6Address @@ -67,7 +62,7 @@ class DeviceVrf6(HyperglassModel): class Vrf(HyperglassModel): - """Model for per VRF/afi config in devices.yaml""" + """Validation model for per VRF/afi config in devices.yaml.""" name: str display_name: str @@ -94,16 +89,21 @@ class Vrf(HyperglassModel): class DefaultVrf(HyperglassModel): + """Validation model for default routing table VRF.""" name: str = "default" display_name: str = "Global" access_list = [{"allow": IPv4Network("0.0.0.0/0")}, {"allow": IPv6Network("::/0")}] class DefaultVrf4(HyperglassModel): + """Validation model for IPv4 default routing table VRF definition.""" + vrf_name: str = "default" source_address: IPv4Address = IPv4Address("127.0.0.1") class DefaultVrf6(HyperglassModel): + """Validation model for IPv6 default routing table VRF definition.""" + vrf_name: str = "default" source_address: IPv6Address = IPv6Address("::1") diff --git a/hyperglass/constants.py b/hyperglass/constants.py index e16fb1e..da37482 100644 --- a/hyperglass/constants.py +++ b/hyperglass/constants.py @@ -1,11 +1,25 @@ -""" -Global Constants for hyperglass -""" +"""Constant definitions used throughout the application.""" +import sys protocol_map = {80: "http", 8080: "http", 443: "https", 8443: "https"} target_format_space = ("huawei", "huawei_vrpv8") +LOG_FMT = ( + "[{level}] {time:YYYYMMDD} | {time:HH:mm:ss} {name} " + "| {function} {message}" +) +LOG_LEVELS = [ + {"name": "DEBUG", "no": 10, "color": ""}, + {"name": "INFO", "no": 20, "color": ""}, + {"name": "SUCCESS", "no": 25, "color": ""}, + {"name": "WARNING", "no": 30, "color": ""}, + {"name": "ERROR", "no": 40, "color": ""}, + {"name": "CRITICAL", "no": 50, "color": ""}, +] + +LOG_HANDLER = {"sink": sys.stdout, "format": LOG_FMT, "level": "INFO"} + class Supported: """ diff --git a/hyperglass/exceptions.py b/hyperglass/exceptions.py index 8f3e7b9..4fb903a 100644 --- a/hyperglass/exceptions.py +++ b/hyperglass/exceptions.py @@ -1,137 +1,161 @@ -""" -Custom exceptions for hyperglass -""" +"""Custom exceptions for hyperglass.""" + +import json as _json +from hyperglass.util import log class HyperglassError(Exception): - """hyperglass base exception""" + """hyperglass base exception.""" - def __init__(self, message="", alert="warning", keywords=[]): + def __init__(self, message="", alert="warning", keywords=None): + """Initialize the hyperglass base exception class. + + Keyword Arguments: + message {str} -- Error message (default: {""}) + alert {str} -- Error severity (default: {"warning"}) + keywords {list} -- 'Important' keywords (default: {None}) + """ self.message = message self.alert = alert - self.keywords = keywords + self.keywords = keywords or [] + if self.alert == "warning": + log.error(repr(self)) + elif self.alert == "danger": + log.critical(repr(self)) + else: + log.info(repr(self)) def __str__(self): + """Return the instance's error message. + + Returns: + {str} -- Error Message + """ return self.message + def __repr__(self): + """Return the instance's severity & error message in a string. + + Returns: + {str} -- Error message with code + """ + return f"[{self.alert.upper()}] {self.message}" + def __dict__(self): + """Return the instance's attributes as a dictionary. + + Returns: + {dict} -- Exception attributes in dict + """ return {"message": self.message, "alert": self.alert, "keywords": self.keywords} + def json(self): + """Return the instance's attributes as a JSON object. -class ConfigError(HyperglassError): + Returns: + {str} -- Exception attributes as JSON + """ + return _json.dumps(self.__dict__()) + + @property + def message(self): + """Return the instance's `message` attribute. + + Returns: + {str} -- Error Message + """ + return self.message + + @property + def alert(self): + """Return the instance's `alert` attribute. + + Returns: + {str} -- Alert name + """ + return self.alert + + @property + def keywords(self): + """Return the instance's `keywords` attribute. + + Returns: + {list} -- Keywords List + """ + return self.keywords + + +class _UnformattedHyperglassError(HyperglassError): + """Base exception class for freeform error messages.""" + + def __init__(self, unformatted_msg, alert="warning", **kwargs): + """Format error message with keyword arguments. + + Keyword Arguments: + message {str} -- Error message (default: {""}) + alert {str} -- Error severity (default: {"warning"}) + keywords {list} -- 'Important' keywords (default: {None}) + """ + self.message = unformatted_msg.format(**kwargs) + self.alert = alert + self.keywords = list(kwargs.values()) + super().__init__(message=self.message, alert=self.alert, keywords=self.keywords) + + +class ConfigError(_UnformattedHyperglassError): """Raised for generic user-config issues.""" - def __init__(self, unformatted_msg, **kwargs): - self.message = unformatted_msg.format(**kwargs) - self.keywords = [value for value in kwargs.values()] - super().__init__(message=self.message, keywords=self.keywords) + +class ConfigInvalid(_UnformattedHyperglassError): + """Raised when a config item fails type or option validation.""" + + message = 'The value field "{field}" is invalid: {error_msg}' -class ConfigInvalid(HyperglassError): - """Raised when a config item fails type or option validation""" +class ConfigMissing(_UnformattedHyperglassError): + """Raised when a required config file or item is missing or undefined.""" - def __init__(self, **kwargs): - self.message = 'The value field "{field}" is invalid: {error_msg}'.format( - **kwargs - ) - self.keywords = [value for value in kwargs.values()] - super().__init__(message=self.message, keywords=self.keywords) + message = ( + "{missing_item} is missing or undefined and is required to start " + "hyperglass. Please consult the installation documentation." + ) -class ConfigMissing(HyperglassError): - """ - Raised when a required config file or item is missing or undefined. - """ +class ScrapeError(_UnformattedHyperglassError): + """Raised when a scrape/netmiko error occurs.""" - def __init__(self, **kwargs): - self.message = ( - "{missing_item} is missing or undefined and is required to start " - "hyperglass. Please consult the installation documentation." - ).format(**kwargs) - self.keywords = [value for value in kwargs.values()] - super().__init__(message=self.message, keywords=self.keywords) + alert = "danger" -class ScrapeError(HyperglassError): - """Raised upon a scrape/netmiko error""" +class AuthError(_UnformattedHyperglassError): + """Raised when authentication to a device fails.""" - def __init__(self, msg, **kwargs): - self.message = msg.format(**kwargs) - self.alert = "danger" - self.keywords = [value for value in kwargs.values()] - super().__init__(message=self.message, alert=self.alert, keywords=self.keywords) + alert = "danger" -class AuthError(HyperglassError): - """Raised when authentication to a device fails""" +class RestError(_UnformattedHyperglassError): + """Raised upon a rest API client error.""" - def __init__(self, msg, **kwargs): - self.message = msg.format(**kwargs) - self.alert = "danger" - self.keywords = [value for value in kwargs.values()] - super().__init__(message=self.message, alert=self.alert, keywords=self.keywords) + alert = "danger" -class RestError(HyperglassError): - """Raised upon a rest API client error""" - - def __init__(self, msg, **kwargs): - self.message = msg.format(**kwargs) - self.alert = "danger" - self.keywords = [value for value in kwargs.values()] - super().__init__(message=self.message, alert=self.alert, keywords=self.keywords) - - -class InputInvalid(HyperglassError): - """Raised when input validation fails""" - - def __init__(self, unformatted_msg, **kwargs): - self.message = unformatted_msg.format(**kwargs) - self.alert = "warning" - self.keywords = [value for value in kwargs.values()] - super().__init__(message=self.message, alert=self.alert, keywords=self.keywords) - - -class InputNotAllowed(HyperglassError): - """ - Raised when input validation fails due to a blacklist or - requires_ipv6_cidr check - """ - - def __init__(self, unformatted_msg, **kwargs): - self.message = unformatted_msg.format(**kwargs) - self.alert = "warning" - self.keywords = [value for value in kwargs.values()] - super().__init__(message=self.message, alert=self.alert, keywords=self.keywords) - - -class ResponseEmpty(HyperglassError): - """ - Raised when hyperglass is able to connect to the device and execute - a valid query, but the response is empty. - """ - - def __init__(self, unformatted_msg, **kwargs): - self.message = unformatted_msg.format(**kwargs) - self.alert = "warning" - self.keywords = [value for value in kwargs.values()] - super().__init__(message=self.message, alert=self.alert, keywords=self.keywords) - - -class UnsupportedDevice(HyperglassError): - """Raised when an input NOS is not in the supported NOS list.""" - - def __init__(self, **kwargs): - self.message = "".format(**kwargs) - self.keywords = [value for value in kwargs.values()] - super().__init__(message=self.message, keywords=self.keywords) - - -class DeviceTimeout(HyperglassError): +class DeviceTimeout(_UnformattedHyperglassError): """Raised when the connection to a device times out.""" - def __init__(self, msg, **kwargs): - self.message = msg.format(**kwargs) - self.alert = "danger" - self.keywords = [value for value in kwargs.values()] - super().__init__(message=self.message, alert=self.alert, keywords=self.keywords) + alert = "danger" + + +class InputInvalid(_UnformattedHyperglassError): + """Raised when input validation fails.""" + + +class InputNotAllowed(_UnformattedHyperglassError): + """Raised when input validation fails due to a configured check.""" + + +class ResponseEmpty(_UnformattedHyperglassError): + """Raised when hyperglass can connect to the device but the response is empty.""" + + +class UnsupportedDevice(_UnformattedHyperglassError): + """Raised when an input NOS is not in the supported NOS list.""" diff --git a/hyperglass/hyperglass.py b/hyperglass/hyperglass.py index 80188e3..f5979a5 100644 --- a/hyperglass/hyperglass.py +++ b/hyperglass/hyperglass.py @@ -1,4 +1,4 @@ -"""Hyperglass Front End""" +"""Hyperglass Front End.""" # Standard Library Imports import operator @@ -8,7 +8,6 @@ from pathlib import Path # Third Party Imports import aredis import stackprinter -from logzero import logger as log from prometheus_client import CONTENT_TYPE_LATEST from prometheus_client import CollectorRegistry from prometheus_client import Counter @@ -27,7 +26,6 @@ from sanic_limiter import get_remote_address # Project Imports from hyperglass.command.execute import Execute from hyperglass.configuration import devices -from hyperglass.configuration import logzero_config # noqa: F401 from hyperglass.configuration import params from hyperglass.constants import Supported from hyperglass.exceptions import AuthError @@ -39,6 +37,7 @@ from hyperglass.exceptions import ResponseEmpty from hyperglass.exceptions import RestError from hyperglass.exceptions import ScrapeError from hyperglass.render import render_html +from hyperglass.util import log stackprinter.set_excepthook() diff --git a/hyperglass/render/html.py b/hyperglass/render/html.py index 3968bcb..320bbb1 100644 --- a/hyperglass/render/html.py +++ b/hyperglass/render/html.py @@ -1,20 +1,18 @@ -""" -Renders Jinja2 & Sass templates for use by the front end application -""" +"""Renders Jinja2 & Sass templates for use by the front end application.""" + # Standard Library Imports from pathlib import Path # Third Party Imports import jinja2 import yaml -from logzero import logger as log from markdown2 import Markdown # Project Imports -from hyperglass.configuration import logzero_config # NOQA: F401 from hyperglass.configuration import networks from hyperglass.configuration import params from hyperglass.exceptions import HyperglassError +from hyperglass.util import log # Module Directories working_directory = Path(__file__).resolve().parent @@ -190,7 +188,7 @@ def generate_markdown(section, file_name=None): def render_html(template_name, **kwargs): - """Renders Jinja2 HTML templates""" + """Render Jinja2 HTML templates.""" details_name_list = ["footer", "bgp_aspath", "bgp_community"] details_dict = {} for details_name in details_name_list: diff --git a/hyperglass/render/webassets.py b/hyperglass/render/webassets.py index 3885c25..d335d39 100644 --- a/hyperglass/render/webassets.py +++ b/hyperglass/render/webassets.py @@ -1,6 +1,5 @@ -""" -Renders Jinja2 & Sass templates for use by the front end application -""" +"""Renders Jinja2 & Sass templates for use by the front end application.""" + # Standard Library Imports import json import subprocess @@ -8,15 +7,14 @@ from pathlib import Path # Third Party Imports import jinja2 -from logzero import logger as log # Project Imports -from hyperglass.configuration import frontend_networks from hyperglass.configuration import frontend_devices +from hyperglass.configuration import frontend_networks from hyperglass.configuration import frontend_params -from hyperglass.configuration import logzero_config # NOQA: F401 from hyperglass.configuration import params from hyperglass.exceptions import HyperglassError +from hyperglass.util import log # Module Directories working_directory = Path(__file__).resolve().parent diff --git a/hyperglass/util.py b/hyperglass/util.py new file mode 100644 index 0000000..8cfcd6b --- /dev/null +++ b/hyperglass/util.py @@ -0,0 +1,45 @@ +"""Utility fuctions.""" + +# Third Party Imports +from loguru import logger as _loguru_logger + +# Project Imports +from hyperglass.constants import LOG_HANDLER +from hyperglass.constants import LOG_LEVELS +from hyperglass.exceptions import ConfigInvalid + +_loguru_logger.remove() +_loguru_logger.configure(handlers=[LOG_HANDLER], levels=LOG_LEVELS) + +log = _loguru_logger + + +async def check_redis(host, port): + """Validate if Redis is running. + + Arguments: + host {str} -- IP address or hostname of Redis server + port {[type]} -- TCP port of Redis server + + Raises: + ConfigInvalid: Raised if redis server is unreachable + + Returns: + {bool} -- True if running, False if not + """ + import asyncio + from socket import gaierror + + try: + _reader, _writer = await asyncio.open_connection(str(host), int(port)) + except gaierror: + raise ConfigInvalid( + "Redis isn't running: {host}:{port} is unreachable/unresolvable.", + alert="danger", + host=host, + port=port, + ) + if _reader or _writer: + return True + else: + return False