From 69d66cb394fd189d83575397ca06cfaa4204a82e Mon Sep 17 00:00:00 2001 From: checktheroads Date: Tue, 28 Jan 2020 14:11:34 -0700 Subject: [PATCH] add support for multiple config file locations --- hyperglass/configuration/__init__.py | 135 +++++++++++++++++++-------- hyperglass/util.py | 32 +++++++ 2 files changed, 130 insertions(+), 37 deletions(-) diff --git a/hyperglass/configuration/__init__.py b/hyperglass/configuration/__init__.py index 3de1edd..61cf04d 100644 --- a/hyperglass/configuration/__init__.py +++ b/hyperglass/configuration/__init__.py @@ -3,7 +3,9 @@ # Standard Library Imports import asyncio import copy +import getpass import math +import os from pathlib import Path # Third Party Imports @@ -28,15 +30,87 @@ from hyperglass.constants import SUPPORTED_QUERY_TYPES from hyperglass.exceptions import ConfigError from hyperglass.exceptions import ConfigInvalid from hyperglass.exceptions import ConfigMissing +from hyperglass.util import check_path from hyperglass.util import log # Project Directories -working_dir = Path(__file__).resolve().parent +WORKING_DIR = Path(__file__).resolve().parent +CONFIG_PATHS = ( + Path("/etc/hyperglass/"), + Path.home() / "hyperglass", + WORKING_DIR.parent.parent, + WORKING_DIR.parent, + WORKING_DIR, +) +CONFIG_FILES = ( + ("hyperglass.yaml", False), + ("devices.yaml", True), + ("commands.yaml", False), +) -# Config Files -config_file_main = working_dir / "hyperglass.yaml" -config_file_devices = working_dir / "devices.yaml" -config_file_commands = working_dir / "commands.yaml" + +async def _check_config_paths(): + """Verify supported configuration directories exist and are readable.""" + config_path = None + for path in CONFIG_PATHS: + checked = await check_path(path) + if checked is not None: + config_path = checked + break + if config_path is None: + raise ConfigError( + """ +No configuration directories were determined to both exist and be readable +by hyperglass. hyperglass is running as user '{un}' (UID '{uid}'), and tried to access +the following directories: +{dir}""".format( + un=getpass.getuser(), + uid=os.getuid(), + dir="\n".join([" - " + str(p) for p in CONFIG_PATHS]), + ) + ) + log.info("Configuration directory: {d}", d=str(config_path)) + return config_path + + +async 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 + + checked = await 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 + + +CONFIG_PATH = asyncio.run(_check_config_paths()) + +CONFIG_MAIN, CONFIG_DEVICES, CONFIG_COMMANDS = asyncio.run( + _check_config_files(CONFIG_PATH) +) def _set_log_level(debug, log_file=None): @@ -75,13 +149,11 @@ async def _config_main(): Returns: {dict} -- Main config file """ + config = {} try: - async with AIOFile(config_file_main, "r") as cf: + async with AIOFile(CONFIG_MAIN, "r") as cf: raw = await cf.read() config = yaml.safe_load(raw) - except FileNotFoundError as nf: - config = None - log.warning(f"{str(nf)} - Default configuration will be used") except (yaml.YAMLError, yaml.MarkedYAMLError) as yaml_error: raise ConfigError(error_msg=str(yaml_error)) from None return config @@ -93,16 +165,16 @@ async def _config_commands(): Returns: {dict} -- Commands config file """ - try: - async with AIOFile(config_file_commands, "r") as cf: - raw = await cf.read() - config = yaml.safe_load(raw) - log.debug(f"Unvalidated commands: {config}") - except FileNotFoundError as nf: - config = None - log.warning(f"{str(nf)} - Default commands will be used.") - except (yaml.YAMLError, yaml.MarkedYAMLError) as yaml_error: - raise ConfigError(error_msg=str(yaml_error)) from None + if CONFIG_COMMANDS is None: + config = {} + else: + try: + async with AIOFile(CONFIG_COMMANDS, "r") as cf: + raw = await cf.read() + config = yaml.safe_load(raw) or {} + log.debug(f"Unvalidated commands: {config}") + except (yaml.YAMLError, yaml.MarkedYAMLError) as yaml_error: + raise ConfigError(error_msg=str(yaml_error)) from None return config @@ -113,12 +185,10 @@ async def _config_devices(): {dict} -- Devices config file """ try: - async with AIOFile(config_file_devices, "r") as cf: + async with AIOFile(CONFIG_DEVICES, "r") as cf: raw = await cf.read() config = yaml.safe_load(raw) log.debug(f"Unvalidated device config: {config}") - except FileNotFoundError: - raise ConfigMissing(missing_item=str(config_file_devices)) from None except (yaml.YAMLError, yaml.MarkedYAMLError) as yaml_error: raise ConfigError(error_msg=str(yaml_error)) from None return config @@ -132,26 +202,17 @@ try: except KeyError: _debug = True -# Read raw debug value from config to enable debugging quickly needed. +# Read raw debug value from config to enable debugging quickly. _set_log_level(_debug) -user_commands = asyncio.run(_config_commands()) -user_devices = asyncio.run(_config_devices()) +_user_commands = asyncio.run(_config_commands()) +_user_devices = asyncio.run(_config_devices()) # Map imported user config files to expected schema: try: - if user_config: - params = _params.Params(**user_config) - elif not user_config: - params = _params.Params() - - if user_commands: - commands = _commands.Commands.import_params(user_commands) - elif not user_commands: - commands = _commands.Commands() - - devices = _routers.Routers._import(user_devices.get("routers", {})) - + params = _params.Params(**user_config) + commands = _commands.Commands.import_params(_user_commands) + devices = _routers.Routers._import(_user_devices.get("routers", {})) except ValidationError as validation_errors: errors = validation_errors.errors() log.error(errors) diff --git a/hyperglass/util.py b/hyperglass/util.py index d3e5db5..4c98460 100644 --- a/hyperglass/util.py +++ b/hyperglass/util.py @@ -27,6 +27,38 @@ def cpu_count(): return multiprocessing.cpu_count() +async def check_path(path, mode="r"): + """Verify if a path exists and is accessible. + + Arguments: + path {Path|str} -- Path object or string of path + mode {str} -- File mode, r or w + + Raises: + RuntimeError: Raised if file does not exist or is not accessible + + Returns: + {Path|None} -- Path object if checks pass, None if not. + """ + from pathlib import Path + from aiofile import AIOFile + + try: + if not isinstance(path, Path): + path = Path(path) + + if not path.exists(): + raise FileNotFoundError(f"{str(path)} does not exist.") + + async with AIOFile(path, mode): + result = path + + except Exception: + result = None + + return result + + def check_python(): """Verify Python Version.