diff --git a/decnet/cli/__init__.py b/decnet/cli/__init__.py index 493db82e..3576daa4 100644 --- a/decnet/cli/__init__.py +++ b/decnet/cli/__init__.py @@ -25,6 +25,7 @@ from . import ( db, deploy, forwarder, + init, inventory, lifecycle, listener, @@ -52,7 +53,7 @@ for _mod in ( swarm, deploy, lifecycle, workers, inventory, web, profiler, sniffer, db, - topology, bus, + topology, bus, init, ): _mod.register(app) diff --git a/decnet/cli/gating.py b/decnet/cli/gating.py index af724b22..3da191a9 100644 --- a/decnet/cli/gating.py +++ b/decnet/cli/gating.py @@ -29,7 +29,7 @@ MASTER_ONLY_COMMANDS: frozenset[str] = frozenset({ "api", "swarmctl", "deploy", "redeploy", "teardown", "mutate", "listener", "profiler", "services", "distros", "correlate", "archetypes", "web", - "db-reset", + "db-reset", "init", }) MASTER_ONLY_GROUPS: frozenset[str] = frozenset({"swarm", "topology"}) diff --git a/decnet/cli/init.py b/decnet/cli/init.py new file mode 100644 index 00000000..e97975b8 --- /dev/null +++ b/decnet/cli/init.py @@ -0,0 +1,361 @@ +""" +`decnet init` — one-shot master-host bootstrap. + +Idempotent: running it twice is a no-op on already-configured items. +Takes a freshly ``pip install``'d DECNET and turns it into a ready-to- +run master host: creates the ``decnet`` system user/group, installs +the systemd units + polkit rule + tmpfiles.d entry, seeds the +directory layout, drops a placeholder config, and starts the +``decnet.target`` grouping unit. + +Requires root. Uses ``subprocess.run`` (never ``shell=True``) for every +privileged call so the full argv surface is auditable. +""" +from __future__ import annotations + +import grp +import hashlib +import os +import pwd +import shutil +import subprocess # nosec B404 +import sys +from pathlib import Path +from typing import Callable, List + +import typer + +import decnet as _decnet_pkg +from .gating import _require_master_mode +from .utils import console, log + + +_CONFIG_PLACEHOLDER = """\ +# /etc/decnet/config.ini — DECNET master-host config. +# Placeholder; reserved for future structured settings. +# Today, most knobs live in /opt/decnet/.env.local as env vars. +[decnet] +""" + + +def _deploy_root() -> Path: + """Resolve the on-disk ``deploy/`` directory of the installed package. + + Editable install (``pip install -e .``): sibling of the ``decnet`` + package at repo root. Wheel installs aren't supported yet — the + error message tells the operator to use an editable install. + """ + root = Path(_decnet_pkg.__file__).resolve().parent.parent / "deploy" + if not (root / "decnet.target").is_file(): + raise RuntimeError( + f"cannot locate deploy/ directory (looked at {root}); " + "are you on a wheel install that didn't bundle deploy/? " + "use `pip install -e .` from a git checkout" + ) + return root + + +def _sha256(path: Path) -> str: + h = hashlib.sha256() + h.update(path.read_bytes()) + return h.hexdigest() + + +def _run(argv: List[str], *, dry_run: bool) -> None: + if dry_run: + console.print(f" [dim]would run:[/] {' '.join(argv)}") + return + log.info("init: exec %s", argv) + subprocess.run(argv, check=True) # nosec B603 + + +def _step(label: str, action: Callable[[], str]) -> bool: + """Run ``action``, print a checklist line. + + The callable returns the human-readable outcome verb: + ``"ok"`` → ``[ OK ]