diff options
Diffstat (limited to 'jprov/config.py')
| -rw-r--r-- | jprov/config.py | 175 |
1 files changed, 175 insertions, 0 deletions
diff --git a/jprov/config.py b/jprov/config.py new file mode 100644 index 0000000..e7fc504 --- /dev/null +++ b/jprov/config.py @@ -0,0 +1,175 @@ +"""Configuration loading and validation.""" + +from __future__ import annotations + +from dataclasses import dataclass +import os +from typing import Any + +import tomllib + + +@dataclass +class MainConfig: + base_dir: str + templates_prefix: str + containers_prefix: str + jail_conf_dir: str + log_dir: str + + +@dataclass +class Mount: + host: str + jail: str + readonly: bool = False + + +@dataclass +class JailConfig: + name: str + template: str + cmd: str + overlay: str | None + mounts: list[Mount] + env: dict[str, str] + extra_jail_conf: str | None + + +class ConfigError(RuntimeError): + pass + + +def load_main_config() -> MainConfig: + """Load main config from /usr/local/etc/jprov.conf or /etc/jprov.conf.""" + candidates = ["/usr/local/etc/jprov.conf", "/etc/jprov.conf"] + path = next((p for p in candidates if os.path.exists(p)), None) + if not path: + raise ConfigError("main config not found (expected /usr/local/etc/jprov.conf or /etc/jprov.conf)") + + with open(path, "rb") as fh: + data = tomllib.load(fh) + + try: + base_dir = data["base_dir"] + datasets = data["datasets"] + templates_prefix = datasets["templates_prefix"] + containers_prefix = datasets["containers_prefix"] + jail_conf_dir = data["jail_conf_dir"] + log_dir = data["log_dir"] + except KeyError as exc: + raise ConfigError(f"missing main config key: {exc}") from exc + + for key, val in [ + ("base_dir", base_dir), + ("templates_prefix", templates_prefix), + ("containers_prefix", containers_prefix), + ("jail_conf_dir", jail_conf_dir), + ("log_dir", log_dir), + ]: + if not isinstance(val, str) or not val: + raise ConfigError(f"invalid main config value for {key}") + + if not templates_prefix.endswith("/"): + raise ConfigError("datasets.templates_prefix must end with '/'") + if not containers_prefix.endswith("/"): + raise ConfigError("datasets.containers_prefix must end with '/'") + + return MainConfig( + base_dir=base_dir, + templates_prefix=templates_prefix, + containers_prefix=containers_prefix, + jail_conf_dir=jail_conf_dir, + log_dir=log_dir, + ) + + +def load_jail_config(main: MainConfig, jailname: str) -> JailConfig: + """Load per-jail TOML config for the given jail name.""" + jail_dir = os.path.join(main.base_dir, "defs", jailname) + path = os.path.join(jail_dir, "jprov.conf") + if not os.path.exists(path): + raise ConfigError(f"jail config not found: {path}") + + with open(path, "rb") as fh: + data = tomllib.load(fh) + + try: + template = data["template"] + cmd = data["cmd"] + except KeyError as exc: + raise ConfigError(f"missing jail config key: {exc}") from exc + + overlay = data.get("overlay") + env = data.get("env", {}) + extra_jail_conf = data.get("extra_jail_conf") + mounts_raw = data.get("mounts", []) + + if not isinstance(template, str) or not template: + raise ConfigError("invalid jail config template") + if not isinstance(cmd, str) or not cmd: + raise ConfigError("invalid jail config cmd") + if overlay is not None and not isinstance(overlay, str): + raise ConfigError("invalid jail config overlay") + if env and not isinstance(env, dict): + raise ConfigError("invalid jail config env") + if extra_jail_conf is not None and not isinstance(extra_jail_conf, str): + raise ConfigError("invalid jail config extra_jail_conf") + if mounts_raw and not isinstance(mounts_raw, list): + raise ConfigError("invalid jail config mounts") + + mounts: list[Mount] = [] + for item in mounts_raw: + if not isinstance(item, dict): + raise ConfigError("invalid mount entry") + try: + host = item["host"] + jail = item["jail"] + except KeyError as exc: + raise ConfigError(f"missing mount key: {exc}") from exc + readonly = bool(item.get("readonly", False)) + if not isinstance(host, str) or not isinstance(jail, str): + raise ConfigError("invalid mount host/jail") + mounts.append(Mount(host=host, jail=jail, readonly=readonly)) + + return JailConfig( + name=jailname, + template=template, + cmd=cmd, + overlay=overlay, + mounts=mounts, + env={str(k): str(v) for k, v in env.items()} if env else {}, + extra_jail_conf=extra_jail_conf, + ) + + +def validate_configs(main: MainConfig, jail: JailConfig) -> None: + """Validate referenced paths and dataset names exist.""" + if not os.path.isdir(main.base_dir): + raise ConfigError(f"base_dir does not exist: {main.base_dir}") + + jail_dir = os.path.join(main.base_dir, "defs", jail.name) + if not os.path.isdir(jail_dir): + raise ConfigError(f"jail defs dir does not exist: {jail_dir}") + + overlay_path = resolve_overlay_path(main, jail) + if overlay_path and not os.path.isdir(overlay_path): + raise ConfigError(f"overlay dir does not exist: {overlay_path}") + + for mnt in jail.mounts: + if not os.path.exists(mnt.host): + raise ConfigError(f"mount host path does not exist: {mnt.host}") + + +def resolve_overlay_path(main: MainConfig, jail: JailConfig) -> str: + """Return resolved overlay path for the jail.""" + if jail.overlay is None or jail.overlay == "": + return os.path.join(main.base_dir, "defs", jail.name, "overlay") + if os.path.isabs(jail.overlay): + return jail.overlay + return os.path.join(main.base_dir, "defs", jail.name, jail.overlay) + + +def jail_conf_path(main: MainConfig, jailname: str) -> str: + """Return path to the generated jail.conf.d file for this jail.""" + return os.path.join(main.jail_conf_dir, f"{jailname}.conf") |
