1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00
2021-09-15 00:57:45 -07:00

215 lines
6.5 KiB
Python

"""Import configuration files and returns default values if undefined."""
# Standard Library
import os
from typing import Dict, List, Generator
from pathlib import Path
# Third Party
import yaml
from pydantic import ValidationError
# Project
from hyperglass.log import log, enable_file_logging, enable_syslog_logging
from hyperglass.util import set_app_path, set_cache_env
from hyperglass.defaults import CREDIT
from hyperglass.constants import PARSED_RESPONSE_FIELDS, __version__
from hyperglass.models.ui import UIParameters
from hyperglass.util.files import check_path
from hyperglass.exceptions.private import ConfigError, ConfigMissing
from hyperglass.models.config.params import Params
from hyperglass.models.config.devices import Devices
from hyperglass.models.commands.generic import Directive
# Local
from .markdown import get_markdown
from .validation import validate_config
set_app_path(required=True)
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),
)
def _check_config_files(directory: Path):
"""Verify config files exist and are readable."""
files = ()
for file in CONFIG_FILES:
file_name, required = file
file_path = directory / file_name
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 += (checked,)
return files
STATIC_PATH = CONFIG_PATH / "static"
CONFIG_MAIN, CONFIG_DEVICES, CONFIG_COMMANDS = _check_config_files(CONFIG_PATH)
def _config_required(config_path: Path) -> Dict:
try:
with config_path.open("r") as cf:
config = yaml.safe_load(cf)
except (yaml.YAMLError, yaml.MarkedYAMLError) as yaml_error:
raise ConfigError(message="Error reading YAML file: '{e}'", e=yaml_error)
if config is None:
raise ConfigMissing(missing_item=config_path.name)
return config
def _config_optional(config_path: Path) -> Dict:
config = {}
if config_path is None:
return config
else:
try:
with config_path.open("r") as cf:
config = yaml.safe_load(cf) or {}
except (yaml.YAMLError, yaml.MarkedYAMLError) as yaml_error:
raise ConfigError(message="Error reading YAML file: '{e}'", e=yaml_error)
return config
def _get_commands(data: Dict) -> List[Directive]:
commands = []
for name, command in data.items():
try:
commands.append(Directive(id=name, **command))
except ValidationError as err:
raise ConfigError(
message="Validation error in command '{c}': '{e}'", c=name, e=err
) from err
return commands
def _device_commands(device: Dict, directives: List[Directive]) -> Generator[Directive, None, None]:
device_commands = device.get("commands", [])
for directive in directives:
if directive.id in device_commands:
yield directive
def _get_devices(data: List[Dict], directives: List[Directive]) -> Devices:
for device in data:
device_commands = list(_device_commands(device, directives))
device["commands"] = device_commands
return Devices(data)
user_config = _config_optional(CONFIG_MAIN)
# Read raw debug value from config to enable debugging quickly.
# Map imported user configuration to expected schema.
log.debug("Unvalidated configuration from {}: {}", CONFIG_MAIN, user_config)
params = validate_config(config=user_config, importer=Params)
# Map imported user commands to expected schema.
_user_commands = _config_optional(CONFIG_COMMANDS)
log.debug("Unvalidated commands from {}: {}", CONFIG_COMMANDS, _user_commands)
commands = _get_commands(_user_commands)
# Map imported user devices to expected schema.
_user_devices = _config_required(CONFIG_DEVICES)
log.debug("Unvalidated devices from {}: {}", CONFIG_DEVICES, _user_devices)
devices: Devices = _get_devices(_user_devices.get("routers", []), commands)
# Set cache configurations to environment variables, so they can be
# used without importing this module (Gunicorn, etc).
set_cache_env(db=params.cache.database, host=params.cache.host, port=params.cache.port)
# Set up file logging once configuration parameters are initialized.
enable_file_logging(
logger=log,
log_directory=params.logging.directory,
log_format=params.logging.format,
log_max_size=params.logging.max_size,
)
# 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,
)
if params.logging.http is not None and params.logging.http.enable:
log.debug("HTTP logging is enabled")
# 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.
try:
params.web.text.subtitle = params.web.text.subtitle.format(
**params.dict(exclude={"web", "queries", "messages"})
)
# If keywords are unmodified (default), add the org name &
# site_title.
if Params().site_keywords == params.site_keywords:
params.site_keywords = sorted({*params.site_keywords, params.org_name, params.site_title})
except KeyError:
pass
content_greeting = get_markdown(
config_path=params.web.greeting, default="", params={"title": params.web.greeting.title},
)
content_credit = CREDIT.format(version=__version__)
_ui_params = params.frontend()
_ui_params["web"]["logo"]["light_format"] = params.web.logo.light.suffix
_ui_params["web"]["logo"]["dark_format"] = params.web.logo.dark.suffix
ui_params = UIParameters(
**_ui_params,
version=__version__,
networks=devices.networks(params),
parsed_data_fields=PARSED_RESPONSE_FIELDS,
content={"credit": content_credit, "greeting": content_greeting},
)
URL_DEV = f"http://localhost:{str(params.listen_port)}/"
URL_PROD = "/api/"
REDIS_CONFIG = {
"host": str(params.cache.host),
"port": params.cache.port,
"decode_responses": True,
"password": params.cache.password,
}