diff options
| author | Jan Tuomi <jan@jantuomi.fi> | 2026-02-16 14:46:24 +0200 |
|---|---|---|
| committer | Jan Tuomi <jan@jantuomi.fi> | 2026-02-16 14:46:24 +0200 |
| commit | 34f5f546841c8e9022ac0ccf957f636907e643bb (patch) | |
| tree | 8885e5f2209baa04128d75ca9bb340a999723828 /jprov | |
| parent | 5f46fde558c467414fa151ad962caaf7f07628fa (diff) | |
Diffstat (limited to 'jprov')
| -rw-r--r-- | jprov/config.py | 29 | ||||
| -rw-r--r-- | jprov/jailconf.py | 62 | ||||
| -rw-r--r-- | jprov/main.py | 9 | ||||
| -rw-r--r-- | jprov/provision.py | 2 | ||||
| -rw-r--r-- | jprov/runner.py | 7 |
5 files changed, 90 insertions, 19 deletions
diff --git a/jprov/config.py b/jprov/config.py index e7fc504..d4eb5dc 100644 --- a/jprov/config.py +++ b/jprov/config.py @@ -28,12 +28,13 @@ class Mount: @dataclass class JailConfig: name: str + config_path: str template: str - cmd: str overlay: str | None mounts: list[Mount] env: dict[str, str] - extra_jail_conf: str | None + jailconf: dict[str, object] + cmds: dict[str, str] class ConfigError(RuntimeError): @@ -96,28 +97,34 @@ def load_jail_config(main: MainConfig, jailname: str) -> JailConfig: 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") + jailconf = data.get("jailconf", {}) + cmds = data.get("cmds", {}) 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 cmds and not isinstance(cmds, dict): + raise ConfigError("invalid jail config cmds") 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 jailconf and not isinstance(jailconf, dict): + raise ConfigError("invalid jail config jailconf") if mounts_raw and not isinstance(mounts_raw, list): raise ConfigError("invalid jail config mounts") + if not isinstance(cmds, dict) or "provision" not in cmds or not isinstance(cmds["provision"], str): + raise ConfigError("missing or invalid cmds.provision") + for key, val in cmds.items(): + if not isinstance(val, str) or not val: + raise ConfigError(f"invalid cmds.{key}") + mounts: list[Mount] = [] for item in mounts_raw: if not isinstance(item, dict): @@ -134,12 +141,13 @@ def load_jail_config(main: MainConfig, jailname: str) -> JailConfig: return JailConfig( name=jailname, + config_path=path, 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, + jailconf=jailconf, + cmds={str(k): str(v) for k, v in cmds.items()} if cmds else {}, ) @@ -161,6 +169,7 @@ def validate_configs(main: MainConfig, jail: JailConfig) -> None: 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 == "": diff --git a/jprov/jailconf.py b/jprov/jailconf.py index 0478cbd..ba304ae 100644 --- a/jprov/jailconf.py +++ b/jprov/jailconf.py @@ -3,6 +3,25 @@ from __future__ import annotations from .config import JailConfig, MainConfig + + +def _format_value(value: object) -> str: + if isinstance(value, str): + return f"\"{value}\"" + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, (int, float)): + return str(value) + raise ValueError(f"unsupported jailconf value type: {type(value).__name__}") + + +def _flatten(prefix: str, value: object, out: list[tuple[str, object]]) -> None: + if isinstance(value, dict): + for key, val in value.items(): + new_prefix = f"{prefix}.{key}" if prefix else str(key) + _flatten(new_prefix, val, out) + else: + out.append((prefix, value)) def render_jail_conf( main: MainConfig, jail: JailConfig, @@ -18,14 +37,43 @@ def render_jail_conf( f"{name} {{", f" host.hostname = \"{name}\";", f" path = \"{root}\";", - " mount.devfs;", - " exec.clean;", " exec.start = \"/bin/sh /etc/rc\";", " exec.stop = \"/bin/sh /etc/rc.shutdown jail\";", - "}", ] - base = "\n".join(lines) - if jail.extra_jail_conf: - base += "\n" + jail.extra_jail_conf.rstrip() + "\n" - return base + if jail.jailconf: + flattened: list[tuple[str, object]] = [] + _flatten("", jail.jailconf, flattened) + for key, value in flattened: + if value is True: + lines.append(f" {key};") + continue + if value is False: + continue + lines.append(f" {key} = {_format_value(value)};") + + # Exec commands from [cmds], with env prefix + env_prefix = "" + if jail.env: + pairs = [f"{k}={v}" for k, v in jail.env.items()] + env_prefix = "env " + " ".join(pairs) + " " + + cmd_map = { + "prestart": "exec.prestart", + "poststart": "exec.poststart", + "prestop": "exec.prestop", + "poststop": "exec.poststop", + "start": "exec.start", + "stop": "exec.stop", + } + for key, jailconf_key in cmd_map.items(): + cmd = jail.cmds.get(key) + if cmd: + if env_prefix and not cmd.startswith("env "): + cmd_value = f"{env_prefix}{cmd}" + else: + cmd_value = cmd + lines.append(f" {jailconf_key} = \"{cmd_value}\";") + + lines.append("}") + return "\n".join(lines) + "\n" diff --git a/jprov/main.py b/jprov/main.py index a66892c..40cf715 100644 --- a/jprov/main.py +++ b/jprov/main.py @@ -5,6 +5,7 @@ from __future__ import annotations import argparse import os import sys +import time from datetime import datetime, timezone from .config import ConfigError, jail_conf_path, load_jail_config, load_main_config, resolve_overlay_path, validate_configs @@ -52,6 +53,7 @@ def main(argv: list[str] | None = None) -> int: log_file = os.path.join(main_cfg.log_dir, f"{args.jailname}-{ts}.log") log_line("INFO", "validate", "OK (config and paths)") + log_line("INFO", "config", f"loaded {jail_cfg.config_path}") template_ds = f"{main_cfg.templates_prefix}{jail_cfg.template}" jail_ds = f"{main_cfg.containers_prefix}{args.jailname}" @@ -101,6 +103,13 @@ def main(argv: list[str] | None = None) -> int: log_line("INFO", "start", f"starting jail {args.jailname}") run(["service", "jail", "start", args.jailname], log_file=log_file, dry_run=dry_run) + if not dry_run: + for attempt in range(5): + if jail_exists(args.jailname, log_file=log_file): + break + time.sleep(1) + else: + raise RuntimeError(f"jail failed to start: {args.jailname}") if jail_cfg.mounts: log_line("INFO", "mounts", "mounting nullfs volumes") diff --git a/jprov/provision.py b/jprov/provision.py index f3352e9..f6c7678 100644 --- a/jprov/provision.py +++ b/jprov/provision.py @@ -10,6 +10,6 @@ from .runner import run def run_provision(jail: JailConfig, *, log_file: str | None = None) -> int: """Run the provisioning command inside the jail.""" - cmd = ["jexec", jail.name] + shlex.split(jail.cmd) + cmd = ["jexec", jail.name] + shlex.split(jail.cmds["provision"]) result = run(cmd, env=jail.env, log_file=log_file) return result.returncode diff --git a/jprov/runner.py b/jprov/runner.py index 88dfe7f..0f0a629 100644 --- a/jprov/runner.py +++ b/jprov/runner.py @@ -58,6 +58,11 @@ def run( fh.write(stderr) if proc.returncode != 0 and not allow_fail: - raise RuntimeError(f"command failed: {' '.join(cmd_list)} (exit {proc.returncode})") + detail = "" + if stderr: + detail = f" stderr: {stderr.strip()}" + raise RuntimeError( + f"command failed: {' '.join(cmd_list)} (exit {proc.returncode}){detail}" + ) return RunResult(cmd=cmd_list, returncode=proc.returncode, stdout=stdout, stderr=stderr) |
