"""Configuration resolution. Precedence (highest wins): 1. command line flags 2. environment variables (GRAYLOG_URL, GRAYLOG_TOKEN, ...) 3. JSON config file (--config, ./graylog-ops.json, ~/.config/graylog-ops/config.json) File format:: { "url": "http://edu-master:9000", "token": "xxxxxxxx", "insecure": false, "timeout": 30 } """ import json import os ENV_KEYS = { "url": "GRAYLOG_URL", "token": "GRAYLOG_TOKEN", "user": "GRAYLOG_USER", "password": "GRAYLOG_PASSWORD", "insecure": "GRAYLOG_INSECURE", "timeout": "GRAYLOG_TIMEOUT", "retries": "GRAYLOG_RETRIES", } DEFAULT_CONFIG_PATHS = ( os.path.expanduser("~/.config/graylog-ops/config.json"), "graylog-ops.json", ) class ConfigError(Exception): """User facing configuration problem.""" def _truthy(value): return str(value).strip().lower() in ("1", "true", "yes", "on", "y") def _read_file(path): try: with open(path, "r", encoding="utf-8") as handle: data = json.load(handle) except OSError as exc: raise ConfigError("cannot read config %s: %s" % (path, exc)) except ValueError as exc: raise ConfigError("config %s is not valid JSON: %s" % (path, exc)) if not isinstance(data, dict): raise ConfigError("config %s must contain a JSON object" % path) return data class Config: def __init__(self, url, token=None, user=None, password=None, insecure=False, timeout=30.0, retries=2, source=None): self.url = url self.token = token self.user = user self.password = password self.insecure = bool(insecure) self.timeout = float(timeout) self.retries = int(retries) self.source = source @property def credentials(self): """(username, password) pair for HTTP Basic auth.""" if self.token: # Graylog token auth: username is the token, password is "token". return self.token, "token" return self.user, self.password @property def auth_kind(self): return "token" if self.token else "user/password" def _pick(values, key, default=None): value = values.get(key, default) return value def load_config(args): """Build a Config from CLI args, env vars and the config file.""" values = {} chosen = None explicit = getattr(args, "config", None) if explicit: if not os.path.exists(explicit): raise ConfigError("config file not found: %s" % explicit) values.update(_read_file(explicit)) chosen = explicit else: for candidate in DEFAULT_CONFIG_PATHS: if os.path.exists(candidate): values.update(_read_file(candidate)) chosen = candidate break for key, env_name in ENV_KEYS.items(): env_value = os.environ.get(env_name) if env_value is not None and env_value.strip() != "": values[key] = env_value for key in ("url", "token", "user", "password", "timeout", "retries"): flag_value = getattr(args, key, None) if flag_value is not None and flag_value != "": values[key] = flag_value if getattr(args, "insecure", False): values["insecure"] = True url = values.get("url") if not url: raise ConfigError( "no Graylog URL: pass --url, set GRAYLOG_URL, or add \"url\" to the config file" ) url = str(url).strip() if "://" not in url: url = "http://" + url token = (values.get("token") or "").strip() or None user = (values.get("user") or "").strip() or None password = values.get("password") if not token and not (user and password): raise ConfigError( "no credentials: pass --token or --user/--password " "(env GRAYLOG_TOKEN / GRAYLOG_USER + GRAYLOG_PASSWORD)" ) timeout = values.get("timeout", 30) retries = values.get("retries", 2) try: timeout = float(timeout) except (TypeError, ValueError): raise ConfigError("timeout must be a number, got %r" % (timeout,)) try: retries = int(retries) except (TypeError, ValueError): raise ConfigError("retries must be an integer, got %r" % (retries,)) insecure = values.get("insecure", False) if isinstance(insecure, str): insecure = _truthy(insecure) return Config( url=url, token=token, user=user, password=password, insecure=bool(insecure), timeout=timeout, retries=retries, source=chosen, )