# SPDX-License-Identifier: AGPL-3.0-or-later """ `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, Optional import typer from jinja2 import Environment, FileSystemLoader, StrictUndefined import decnet as _decnet_pkg from .gating import _require_master_mode from .utils import console, log _CONFIG_PLACEHOLDER = """\ # /etc/decnet/decnet.ini — DECNET host config. # # Every key is OPTIONAL. Absent keys fall through to env-var defaults # defined in decnet/env.py. Real env vars always win over this file # (precedence: env > INI > default), so systemd EnvironmentFile= and # one-off `DECNET_FOO=bar decnet ...` invocations always take effect. # # Secrets (JWT, admin password, DB password) intentionally DO NOT # live here. Put them in /opt/decnet/.env.local or the systemd # EnvironmentFile= — never in a group-readable INI. [decnet] # DECNET-service user/group as configured at `decnet init` time. # Resolved to a uid/gid on each host at deploy time via pwd.getpwnam, # so the same user name can have different numeric uids on master vs # agents without breaking artifact ownership. api-user = {api_user} api-group = {api_group} # mode = master # or "agent" # [api] # host = 127.0.0.1 # port = 8000 # [web] # host = 127.0.0.1 # port = 8080 # admin-user = admin # cors-origins = http://localhost:8080 # comma-separated # [database] # type = sqlite # or "mysql" # url = mysql+asyncmy://user@host:3306/decnet # if set, wins over host/port/name/user # host = localhost # port = 3306 # name = decnet # user = decnet # [bus] # enabled = true # type = unix # or "fake" # socket = /run/decnet/bus.sock # group = decnet # [swarm] # master-host = 10.0.0.1 # syslog-port = 6514 # swarmctl-port = 8770 # swarmctl-host = 127.0.0.1 # [logging] # system-log = /var/log/decnet/decnet.system.log # ingest-log = /var/log/decnet/decnet.log # agent-log = /var/log/decnet/agent.log # [ingester] # batch-size = 100 # batch-max-wait-ms = 250 # [tracing] # enabled = false # otel-endpoint = http://localhost:4317 # [agent] # Managed by the enroll bundle — do NOT edit by hand on an agent host. """ 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 ]