aboutsummaryrefslogtreecommitdiffstats
path: root/jprov/config.py
diff options
context:
space:
mode:
authorJan Tuomi <jan@jantuomi.fi>2026-02-16 12:59:14 +0200
committerJan Tuomi <jan@jantuomi.fi>2026-02-16 12:59:14 +0200
commit5f46fde558c467414fa151ad962caaf7f07628fa (patch)
tree247d6fd45ddbc811fb71d2009fbbd2042e158791 /jprov/config.py
Initial commit
Diffstat (limited to 'jprov/config.py')
-rw-r--r--jprov/config.py175
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")