"""Main entrypoint for jprov.""" from __future__ import annotations import argparse import os import sys from datetime import datetime, timezone from .config import ConfigError, jail_conf_path, load_jail_config, load_main_config, resolve_overlay_path, validate_configs from .jailconf import render_jail_conf from .mounts import mount_all, unmount_all from .overlay import copy_overlay from .provision import run_provision from .runner import log_line, run from .zfs import clone_dataset, dataset_exists, destroy_dataset, get_mountpoint def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(prog="jprov", description="FreeBSD jail provisioning tool") parser.add_argument("-y", "--yes", action="store_true", help="skip confirmations") parser.add_argument("--dry-run", action="store_true", help="log actions without executing") parser.add_argument("jailname", help="name of the jail to provision") return parser def confirm(prompt: str) -> bool: try: return input(f"{prompt} [y/N]: ").strip().lower() in {"y", "yes"} except EOFError: return False def jail_exists(name: str, *, log_file: str | None = None) -> bool: result = run(["jls", "-j", name], allow_fail=True, log_file=log_file) return result.returncode == 0 and result.stdout.strip() != "" def main(argv: list[str] | None = None) -> int: parser = build_parser() args = parser.parse_args(argv) log_file = None dry_run = args.dry_run try: main_cfg = load_main_config() jail_cfg = load_jail_config(main_cfg, args.jailname) validate_configs(main_cfg, jail_cfg) os.makedirs(main_cfg.log_dir, exist_ok=True) ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") log_file = os.path.join(main_cfg.log_dir, f"{args.jailname}-{ts}.log") log_line("INFO", "validate", "OK (config and paths)") template_ds = f"{main_cfg.templates_prefix}{jail_cfg.template}" jail_ds = f"{main_cfg.containers_prefix}{args.jailname}" if not dataset_exists(template_ds, log_file=log_file, dry_run=dry_run): raise RuntimeError(f"template dataset not found: {template_ds}") exists = dataset_exists(jail_ds, log_file=log_file, dry_run=dry_run) or jail_exists( args.jailname, log_file=log_file ) if exists and not args.yes: if not confirm(f"Jail {args.jailname} exists and will be destroyed. Continue?"): log_line("INFO", "confirm", "aborted by user") return 1 if exists: log_line("INFO", "destroy", f"stopping jail {args.jailname}") run(["service", "jail", "onestop", args.jailname], log_file=log_file, allow_fail=True, dry_run=dry_run) root = ( get_mountpoint(jail_ds, log_file=log_file) if dataset_exists(jail_ds, log_file=log_file, dry_run=dry_run) else "" ) if root: log_line("INFO", "destroy", "unmounting volumes") unmount_all(root, jail_cfg.mounts, log_file=log_file) log_line("INFO", "destroy", f"destroying dataset {jail_ds}") destroy_dataset(jail_ds, log_file=log_file) if not dry_run else None log_line("INFO", "create", f"cloning {template_ds} -> {jail_ds}") if not dry_run: clone_dataset(template_ds, jail_ds, log_file=log_file) root = get_mountpoint(jail_ds, log_file=log_file) else: root = f"" os.makedirs(main_cfg.jail_conf_dir, exist_ok=True) conf_path = jail_conf_path(main_cfg, args.jailname) tmp_conf = conf_path + ".tmp" if not dry_run: with open(tmp_conf, "w", encoding="utf-8") as fh: fh.write(render_jail_conf(main_cfg, jail_cfg, root=root)) os.replace(tmp_conf, conf_path) log_line("INFO", "jailconf", f"wrote {conf_path}") else: log_line("INFO", "jailconf", f"[dry-run] would write {conf_path}") log_line("INFO", "start", f"starting jail {args.jailname}") run(["service", "jail", "start", args.jailname], log_file=log_file, dry_run=dry_run) if jail_cfg.mounts: log_line("INFO", "mounts", "mounting nullfs volumes") if not dry_run: mount_all(root, jail_cfg.mounts, log_file=log_file) overlay_path = resolve_overlay_path(main_cfg, jail_cfg) if overlay_path and os.path.isdir(overlay_path): log_line("INFO", "overlay", f"copying overlay from {overlay_path}") if not dry_run: copy_overlay(overlay_path, root) log_line("INFO", "provision", "running provisioning command") if not dry_run: run_provision(jail_cfg, log_file=log_file) else: log_line("INFO", "provision", f"[dry-run] jexec {args.jailname} {jail_cfg.cmd}") log_line("INFO", "done", "provisioning complete") return 0 except (ConfigError, RuntimeError, OSError) as exc: log_line("ERROR", "fatal", str(exc)) if log_file: print(f"script logs written to {log_file}") return 1 if __name__ == "__main__": raise SystemExit(main())