1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00
Files
checktheroads-hyperglass/hyperglass/configuration/__init__.py

510 lines
15 KiB
Python
Raw Normal View History

"""Import configuration files and returns default values if undefined."""
2020-02-03 02:35:11 -07:00
# Standard Library
import os
2020-01-26 02:18:39 -07:00
import copy
import json
from typing import Dict, List, Union, Callable
from pathlib import Path
2020-02-03 02:35:11 -07:00
# Third Party
import yaml
from pydantic import ValidationError
2020-02-03 02:35:11 -07:00
# Project
2020-04-14 10:24:20 -07:00
from hyperglass.log import (
log,
set_log_level,
enable_file_logging,
enable_syslog_logging,
)
from hyperglass.util import check_path, set_app_path, set_cache_env, current_log_level
from hyperglass.models import HyperglassModel
2020-02-03 02:35:11 -07:00
from hyperglass.constants import (
TRANSPORT_REST,
2020-02-03 02:35:11 -07:00
SUPPORTED_QUERY_TYPES,
PARSED_RESPONSE_FIELDS,
SUPPORTED_STRUCTURED_OUTPUT,
2020-03-22 18:05:41 -07:00
__version__,
2020-02-03 02:35:11 -07:00
)
from hyperglass.exceptions import ConfigError, ConfigInvalid, ConfigMissing
from hyperglass.configuration.models import params as _params
from hyperglass.configuration.models import routers as _routers
2020-02-03 02:35:11 -07:00
from hyperglass.configuration.models import commands as _commands
from hyperglass.configuration.defaults import (
CREDIT,
DEFAULT_HELP,
DEFAULT_TERMS,
DEFAULT_DETAILS,
)
2020-02-03 02:35:11 -07:00
from hyperglass.configuration.markdown import get_markdown
2019-10-04 17:17:08 -07:00
2020-04-16 09:29:57 -07:00
set_app_path(required=True)
2020-02-15 11:01:16 -07:00
CONFIG_PATH = Path(os.environ["hyperglass_directory"])
log.info("Configuration directory: {d}", d=str(CONFIG_PATH))
# Project Directories
WORKING_DIR = Path(__file__).resolve().parent
CONFIG_FILES = (
("hyperglass.yaml", False),
("devices.yaml", True),
("commands.yaml", False),
)
2020-04-13 01:04:28 -07:00
def _check_config_files(directory):
"""Verify config files exist and are readable.
Arguments:
directory {Path} -- Config directory Path object
Raises:
ConfigMissing: Raised if a required config file does not pass checks.
Returns:
{tuple} -- main config, devices config, commands config
"""
files = ()
for file in CONFIG_FILES:
file_name, required = file
file_path = directory / file_name
2020-04-13 01:04:28 -07:00
checked = check_path(file_path)
if checked is None and required:
raise ConfigMissing(missing_item=str(file_path))
if checked is None and not required:
log.warning(
"'{f}' was not found, but is not required to run hyperglass. "
+ "Defaults will be used.",
f=str(file_path),
)
files += (file_path,)
return files
STATIC_PATH = CONFIG_PATH / "static"
2020-04-13 01:04:28 -07:00
CONFIG_MAIN, CONFIG_DEVICES, CONFIG_COMMANDS = _check_config_files(CONFIG_PATH)
2019-09-03 00:42:22 -07:00
2019-12-31 11:08:30 -07:00
def _config_required(config_path: Path) -> Dict:
2020-04-13 01:04:28 -07:00
try:
with config_path.open("r") as cf:
config = yaml.safe_load(cf)
2020-04-13 01:04:28 -07:00
except (yaml.YAMLError, yaml.MarkedYAMLError) as yaml_error:
raise ConfigError(str(yaml_error))
if config is None:
log.critical("{} appears to be empty", str(config_path))
raise ConfigMissing(missing_item=config_path.name)
2020-04-13 01:04:28 -07:00
return config
def _config_optional(config_path: Path) -> Dict:
2020-04-13 01:04:28 -07:00
if config_path is None:
config = {}
2020-04-13 01:04:28 -07:00
else:
try:
with config_path.open("r") as cf:
config = yaml.safe_load(cf) or {}
2020-04-13 01:04:28 -07:00
except (yaml.YAMLError, yaml.MarkedYAMLError) as yaml_error:
raise ConfigError(error_msg=str(yaml_error))
2020-04-13 01:04:28 -07:00
return config
def _validate_nos_commands(all_nos, commands):
nos_with_commands = commands.dict().keys()
for nos in all_nos:
valid = False
if nos in SUPPORTED_STRUCTURED_OUTPUT:
valid = True
elif nos in TRANSPORT_REST:
valid = True
elif nos in nos_with_commands:
valid = True
if not valid:
raise ConfigError(
'"{nos}" is used on a device, '
+ 'but no command profile for "{nos}" is defined.',
nos=nos,
)
return True
def _validate_config(config: Union[Dict, List], importer: Callable) -> HyperglassModel:
validated = None
try:
if isinstance(config, Dict):
validated = importer(**config)
elif isinstance(config, List):
validated = importer(config)
except ValidationError as err:
log.error(str(err))
raise ConfigInvalid(err.errors()) from None
return validated
2020-04-13 01:04:28 -07:00
user_config = _config_optional(CONFIG_MAIN)
2019-12-31 11:08:30 -07:00
# Read raw debug value from config to enable debugging quickly.
2020-04-14 10:24:20 -07:00
set_log_level(logger=log, debug=user_config.get("debug", True))
2019-12-31 11:08:30 -07:00
# Map imported user configuration to expected schema.
log.debug("Unvalidated configuration from {}: {}", CONFIG_MAIN, user_config)
params = _validate_config(config=user_config, importer=_params.Params)
# Re-evaluate debug state after config is validated
2020-07-19 14:46:20 -07:00
log_level = current_log_level(log)
if params.debug and log_level != "debug":
set_log_level(logger=log, debug=True)
2020-07-19 14:46:20 -07:00
elif not params.debug and log_level == "debug":
set_log_level(logger=log, debug=False)
# Map imported user commands to expected schema.
2020-04-13 01:04:28 -07:00
_user_commands = _config_optional(CONFIG_COMMANDS)
log.debug("Unvalidated commands from {}: {}", CONFIG_COMMANDS, _user_commands)
commands = _validate_config(
config=_user_commands, importer=_commands.Commands.import_params
)
# Map imported user devices to expected schema.
_user_devices = _config_required(CONFIG_DEVICES)
log.debug("Unvalidated devices from {}: {}", CONFIG_DEVICES, _user_devices)
devices = _validate_config(
config=_user_devices.get("routers", []), importer=_routers.Routers._import,
)
2019-09-09 23:05:10 -07:00
# Validate commands are both supported and properly mapped.
_validate_nos_commands(devices.all_nos, commands)
# Set cache configurations to environment variables, so they can be
# used without importing this module (Gunicorn, etc).
2020-04-15 02:12:01 -07:00
set_cache_env(db=params.cache.database, host=params.cache.host, port=params.cache.port)
2020-04-13 17:31:31 -07:00
# Set up file logging once configuration parameters are initialized.
2020-04-14 10:24:20 -07:00
enable_file_logging(
logger=log,
log_directory=params.logging.directory,
log_format=params.logging.format,
log_max_size=params.logging.max_size,
2020-04-13 17:31:31 -07:00
)
2020-03-23 01:10:27 -07:00
2020-04-14 10:24:20 -07:00
# Set up syslog logging if enabled.
if params.logging.syslog is not None and params.logging.syslog.enable:
enable_syslog_logging(
logger=log,
syslog_host=params.logging.syslog.host,
syslog_port=params.logging.syslog.port,
)
2020-04-16 01:09:37 -07:00
if params.logging.http is not None and params.logging.http.enable:
log.debug("HTTP logging is enabled")
2020-04-14 10:24:20 -07:00
2020-03-23 01:10:27 -07:00
# Perform post-config initialization string formatting or other
# functions that require access to other config levels. E.g.,
# something in 'params.web.text' needs to be formatted with a value
# from params.
2020-01-28 09:52:54 -07:00
try:
params.web.text.subtitle = params.web.text.subtitle.format(
**params.dict(exclude={"web", "queries", "messages"})
2020-01-28 09:52:54 -07:00
)
2020-04-11 16:48:20 -07:00
# If keywords are unmodified (default), add the org name &
# site_title.
if _params.Params().site_keywords == params.site_keywords:
params.site_keywords = sorted(
{*params.site_keywords, params.org_name, params.site_title}
)
2020-01-28 09:52:54 -07:00
except KeyError:
pass
2019-12-31 11:08:30 -07:00
def _build_frontend_networks():
"""Build filtered JSON structure of networks for frontend.
2019-07-29 22:13:11 -07:00
2019-12-31 11:08:30 -07:00
Schema:
{
"device.network.display_name": {
"device.name": {
"display_name": "device.display_name",
"vrfs": [
"Global",
"vrf.display_name"
]
}
}
}
2019-12-31 11:08:30 -07:00
Raises:
ConfigError: Raised if parsing/building error occurs.
Returns:
{dict} -- Frontend networks
"""
frontend_dict = {}
for device in devices.routers:
if device.network.display_name in frontend_dict:
frontend_dict[device.network.display_name].update(
{
device.name: {
"display_name": device.network.display_name,
"vrfs": [vrf.display_name for vrf in device.vrfs],
}
}
)
elif device.network.display_name not in frontend_dict:
frontend_dict[device.network.display_name] = {
device.name: {
"display_name": device.network.display_name,
"vrfs": [vrf.display_name for vrf in device.vrfs],
}
}
frontend_dict["default_vrf"] = devices.default_vrf
if not frontend_dict:
raise ConfigError(error_msg="Unable to build network to device mapping")
return frontend_dict
2019-12-31 11:08:30 -07:00
def _build_frontend_devices():
"""Build filtered JSON structure of devices for frontend.
Schema:
{
"device.name": {
"display_name": "device.display_name",
"vrfs": [
"Global",
"vrf.display_name"
]
}
}
2019-12-31 11:08:30 -07:00
Raises:
ConfigError: Raised if parsing/building error occurs.
Returns:
{dict} -- Frontend devices
"""
frontend_dict = {}
for device in devices.routers:
if device.name in frontend_dict:
frontend_dict[device.name].update(
{
"network": device.network.display_name,
"display_name": device.display_name,
2020-01-16 02:51:10 -07:00
"vrfs": [
{
"id": vrf.name,
"display_name": vrf.display_name,
"ipv4": True if vrf.ipv4 else False, # noqa: IF100
"ipv6": True if vrf.ipv6 else False, # noqa: IF100
}
2020-01-16 02:51:10 -07:00
for vrf in device.vrfs
],
}
)
elif device.name not in frontend_dict:
frontend_dict[device.name] = {
"network": device.network.display_name,
"display_name": device.display_name,
2020-01-16 02:51:10 -07:00
"vrfs": [
{
"id": vrf.name,
"display_name": vrf.display_name,
"ipv4": True if vrf.ipv4 else False, # noqa: IF100
"ipv6": True if vrf.ipv6 else False, # noqa: IF100
}
2020-01-16 02:51:10 -07:00
for vrf in device.vrfs
],
}
if not frontend_dict:
raise ConfigError(error_msg="Unable to build network to device mapping")
return frontend_dict
2019-12-31 11:08:30 -07:00
def _build_networks():
"""Build filtered JSON Structure of networks & devices for Jinja templates.
Raises:
ConfigError: Raised if parsing/building error occurs.
2019-12-31 11:08:30 -07:00
Returns:
{dict} -- Networks & devices
"""
2020-01-16 02:51:10 -07:00
networks = []
_networks = list(set({device.network.display_name for device in devices.routers}))
for _network in _networks:
network_def = {"display_name": _network, "locations": []}
for device in devices.routers:
if device.network.display_name == _network:
network_def["locations"].append(
{
"name": device.name,
"display_name": device.display_name,
"network": device.network.display_name,
"vrfs": [
{"id": vrf.name, "display_name": vrf.display_name}
for vrf in device.vrfs
],
}
)
networks.append(network_def)
if not networks:
raise ConfigError(error_msg="Unable to build network to device mapping")
2020-01-16 02:51:10 -07:00
return networks
2020-01-16 02:51:10 -07:00
def _build_vrfs():
vrfs = []
for device in devices.routers:
for vrf in device.vrfs:
vrf_dict = {
"id": vrf.name,
"display_name": vrf.display_name,
}
2020-01-16 02:51:10 -07:00
if vrf_dict not in vrfs:
vrfs.append(vrf_dict)
2020-01-16 02:51:10 -07:00
return vrfs
2020-01-17 02:50:57 -07:00
content_params = json.loads(
2020-01-28 08:59:27 -07:00
params.json(include={"primary_asn", "org_name", "site_title", "site_description"})
2020-01-17 02:50:57 -07:00
)
def _build_vrf_help():
"""Build a dict of vrfs as keys, help content as values.
Returns:
{dict} -- Formatted VRF help
"""
all_help = {}
for vrf in devices.vrf_objects:
2020-01-26 02:18:39 -07:00
2020-01-17 02:50:57 -07:00
vrf_help = {}
2020-01-26 02:18:39 -07:00
for command in SUPPORTED_QUERY_TYPES:
2020-01-17 02:50:57 -07:00
cmd = getattr(vrf.info, command)
if cmd.enable:
help_params = {**content_params, **cmd.params.dict()}
if help_params["title"] is None:
command_params = getattr(params.queries, command)
help_params[
"title"
] = f"{vrf.display_name}: {command_params.display_name}"
2020-04-13 01:04:28 -07:00
md = get_markdown(
config_path=cmd,
default=DEFAULT_DETAILS[command],
params=help_params,
2020-01-17 02:50:57 -07:00
)
2020-01-26 02:18:39 -07:00
vrf_help.update(
{
command: {
"content": md,
"enable": cmd.enable,
"params": help_params,
}
}
)
2020-01-26 02:18:39 -07:00
2020-01-17 02:50:57 -07:00
all_help.update({vrf.name: vrf_help})
2020-01-26 02:18:39 -07:00
2020-01-17 02:50:57 -07:00
return all_help
2020-04-16 00:26:23 -07:00
content_greeting = get_markdown(
config_path=params.web.greeting,
default="",
params={"title": params.web.greeting.title},
)
2020-01-17 02:50:57 -07:00
content_vrf = _build_vrf_help()
2020-01-26 02:18:39 -07:00
content_help_params = copy.copy(content_params)
content_help_params["title"] = params.web.help_menu.title
2020-04-13 01:04:28 -07:00
content_help = get_markdown(
config_path=params.web.help_menu, default=DEFAULT_HELP, params=content_help_params
2020-01-17 02:50:57 -07:00
)
2020-01-26 02:18:39 -07:00
content_terms_params = copy.copy(content_params)
content_terms_params["title"] = params.web.terms.title
2020-04-13 01:04:28 -07:00
content_terms = get_markdown(
config_path=params.web.terms, default=DEFAULT_TERMS, params=content_terms_params
2020-01-17 02:50:57 -07:00
)
2020-03-23 01:10:27 -07:00
content_credit = CREDIT.format(version=__version__)
2020-01-17 02:50:57 -07:00
2020-01-16 02:51:10 -07:00
vrfs = _build_vrfs()
2019-12-31 11:08:30 -07:00
networks = _build_networks()
frontend_networks = _build_frontend_networks()
frontend_devices = _build_frontend_devices()
2020-06-22 18:07:00 -07:00
_include_fields = {
2020-04-16 23:43:02 -07:00
"cache": {"show_text", "timeout"},
2020-01-28 09:52:54 -07:00
"debug": ...,
"developer_mode": ...,
2020-01-28 09:52:54 -07:00
"primary_asn": ...,
"request_timeout": ...,
"org_name": ...,
"google_analytics": ...,
2020-06-12 18:49:48 -07:00
"site_title": ...,
2020-01-28 09:52:54 -07:00
"site_description": ...,
2020-04-11 16:48:20 -07:00
"site_keywords": ...,
2020-01-28 09:52:54 -07:00
"web": ...,
2020-01-16 02:51:10 -07:00
"messages": ...,
}
2020-06-22 18:07:00 -07:00
_frontend_params = params.dict(include=_include_fields)
_frontend_params["web"]["logo"]["light_format"] = params.web.logo.light.suffix
_frontend_params["web"]["logo"]["dark_format"] = params.web.logo.dark.suffix
2020-01-16 02:51:10 -07:00
_frontend_params.update(
{
2020-03-22 18:05:41 -07:00
"hyperglass_version": __version__,
2020-01-28 12:03:47 -07:00
"queries": {**params.queries.map, "list": params.queries.list},
2020-01-16 02:51:10 -07:00
"devices": frontend_devices,
"networks": networks,
"vrfs": vrfs,
"parsed_data_fields": PARSED_RESPONSE_FIELDS,
2020-01-17 02:50:57 -07:00
"content": {
"help_menu": content_help,
"terms": content_terms,
"credit": content_credit,
"vrf": content_vrf,
2020-04-16 00:26:23 -07:00
"greeting": content_greeting,
2020-01-17 02:50:57 -07:00
},
2020-01-16 02:51:10 -07:00
}
)
frontend_params = _frontend_params
2020-01-21 17:32:31 -07:00
URL_DEV = f"http://localhost:{str(params.listen_port)}/"
2020-01-21 17:32:31 -07:00
URL_PROD = "/api/"
REDIS_CONFIG = {
2020-01-28 09:52:54 -07:00
"host": str(params.cache.host),
"port": params.cache.port,
2020-01-21 17:32:31 -07:00
"decode_responses": True,
}