aboutsummaryrefslogtreecommitdiffstats
path: root/jprov
diff options
context:
space:
mode:
Diffstat (limited to 'jprov')
-rw-r--r--jprov/config.py29
-rw-r--r--jprov/jailconf.py62
-rw-r--r--jprov/main.py9
-rw-r--r--jprov/provision.py2
-rw-r--r--jprov/runner.py7
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)