# -*- coding: utf-8 -*- import os import os.path import re from typing import Any, Dict, List, Optional, Tuple, Union import contextvars from logzero import logger import yaml from chaoslib.types import Settings __all__ = ["get_loaded_settings", "load_settings", "save_settings", "locate_settings_entry"] CHAOSTOOLKIT_CONFIG_PATH = os.path.abspath( os.path.expanduser("~/.chaostoolkit/settings.yaml")) loaded_settings = contextvars.ContextVar('loaded_settings', default={}) def load_settings(settings_path: str = CHAOSTOOLKIT_CONFIG_PATH) -> Settings: """ Load chaostoolkit settings as a mapping of key/values or return `None` when the file could not be found. """ if not os.path.exists(settings_path): logger.debug("The Chaos Toolkit settings file could not be found at " "'{c}'.".format(c=settings_path)) return with open(settings_path) as f: try: settings = yaml.safe_load(f.read()) loaded_settings.set(settings) return settings except yaml.YAMLError as ye: logger.error("Failed parsing YAML settings: {}".format(str(ye))) def save_settings(settings: Settings, settings_path: str = CHAOSTOOLKIT_CONFIG_PATH): """ Save chaostoolkit settings as a mapping of key/values, overwriting any file that may already be present. """ loaded_settings.set(settings) settings_dir = os.path.dirname(settings_path) if not os.path.isdir(settings_dir): os.mkdir(settings_dir) with open(settings_path, 'w') as outfile: yaml.dump(settings, outfile, default_flow_style=False) def get_loaded_settings() -> Settings: """ Settings that have been loaded in the current context. """ return loaded_settings.get() def locate_settings_entry(settings: Settings, key: str) \ -> Optional[ Tuple[ Union[Dict[str, Any], List], Any, Optional[str], Optional[int] ] ]: """ Lookup the entry at the given dotted key in the provided settings and return a a tuple as follows: * the parent of the found entry (can be a list or a dict) * the entry (can eb anything: string, int, list, dict) * the key on the parent that has the entry (in case parent is a dict) * the index in the parent that has the entry (in case parent is a list) Otherwise, returns `None`. When the key in the settings has at least one dot, it must be escaped with two backslahes. Examples of valid keys: * auths * auths.example\\.com * auths.example\\.com:8443 * auths.example\\.com.type * controls[0].name """ array_index = re.compile(r"\[([0-9]*)\]$") # borrowed from https://github.com/carlosescri/DottedDict (MIT) # this kindly preserves escaped dots parts = [x for x in re.split(r"(?<!\\)(\.)", key) if x != "."] current = settings parent = settings last_key = None last_index = None for part in parts: # we don't know to escape now that we have our part part = part.replace('\\', '') m = array_index.search(part) if m: # this is part with an index part = part[:m.start()] if part not in current: return current = current.get(part) parent = current index = int(m.groups()[0]) last_key = None last_index = index try: current = current[index] except (KeyError, IndexError): return else: # this is just a regular key if part not in current: return parent = current last_key = part last_index = None current = current.get(part) return (parent, current, last_key, last_index)