"""`monky-deployd run [--once] [--prune]` · `status [--json]` · `bootstrap` · `version`.""" from __future__ import annotations import argparse import json import logging import os import random import signal import sys import time from pathlib import Path from monky_deployd import __version__ from monky_deployd import state as statemod from monky_deployd.config import DEFAULT_CONFIG_PATH, Config, ConfigError, load from monky_deployd.redact import install as install_redactor EX_OK, EX_FAIL, EX_TEMPFAIL, EX_ENV_MISMATCH = 0, 1, 75, 78 _PRIO = {"DEBUG": 7, "INFO": 6, "WARNING": 4, "ERROR": 3, "CRITICAL": 2} class _JournalFormatter(logging.Formatter): """sd-daemon priority prefix; journald strips it and files the level (no timestamp: journald has one).""" def format(self, record: logging.LogRecord) -> str: return f"<{_PRIO.get(record.levelname, 6)}>{record.name}: {record.getMessage()}" def setup_logging(level: str = "INFO") -> None: root = logging.getLogger() root.handlers.clear() h = logging.StreamHandler(sys.stderr) if os.environ.get("JOURNAL_STREAM"): h.setFormatter(_JournalFormatter()) else: h.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")) root.addHandler(h) root.setLevel(getattr(logging, level.upper(), logging.INFO)) install_redactor(root) def _load(args) -> Config: cfg = load(args.config) if args.verbose: cfg.log_level = "DEBUG" return cfg def cmd_run(args) -> int: cfg = _load(args) setup_logging(cfg.log_level) from monky_deployd.agent import Agent if args.once: return Agent(cfg, prune=args.prune).run_once() stop = {"now": False} def _sig(*_): stop["now"] = True signal.signal(signal.SIGTERM, _sig) signal.signal(signal.SIGINT, _sig) log = logging.getLogger("monky-deployd") log.info("loop mode (interval %ss%s)", cfg.interval_s, ", laptop" if cfg.laptop_mode else "") rc = EX_OK while not stop["now"]: rc = Agent(cfg, prune=args.prune).run_once() if rc == EX_ENV_MISMATCH: return rc # no retry storm delay = cfg.interval_s + random.uniform(0, min(10, cfg.interval_s / 6)) for _ in range(int(delay)): if stop["now"]: break time.sleep(1) return rc def cmd_bootstrap(args) -> int: cfg = _load(args) setup_logging(cfg.log_level) from monky_deployd.agent import Agent, NoCredentials from monky_deployd.bao import BaoError from monky_deployd.tenancy import EnvMismatch from monky_deployd.transport import TransportError agent = Agent(cfg) if cfg.token_path.exists() and not args.force: print(f"already bootstrapped ({cfg.token_path} exists); use --force to log in again") return EX_OK if args.force: statemod.delete(cfg.token_path) try: agent.state = statemod.load(cfg.state_path, cfg.env_id) agent._ensure_token() statemod.save(cfg.state_path, agent.state) except TransportError as exc: print(f"network: {exc}", file=sys.stderr) return EX_TEMPFAIL except EnvMismatch as exc: print(f"AGENT_ENV_MISMATCH: {exc.detail}", file=sys.stderr) return EX_ENV_MISMATCH except (NoCredentials, BaoError) as exc: print(f"bootstrap failed: {exc}", file=sys.stderr) return EX_FAIL print(f"bootstrapped: token stored at {cfg.token_path} (accessor {agent.state.token.accessor})") return EX_OK def _fmt_ts(ts: float | None) -> str: if not ts: return "-" return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(ts)) + f" ({int(time.time() - ts)}s ago)" def cmd_status(args) -> int: cfg = _load(args) st = statemod.load(cfg.state_path, cfg.env_id) token_present = cfg.token_path.exists() bootstrap_present = Path(cfg.bootstrap_path).exists() current = Path(cfg.deploy_dir) / "current" containers: list[dict] = [] try: from monky_deployd.compose import Compose, Docker d = Docker(cfg.docker_bin) if d.available() and current.exists(): containers = [c.as_dict() for c in Compose(d, current, cfg.compose_project).ps()] except Exception: # status must never crash on docker trouble containers = [] healthy = bool(containers) and all( c["state"] == "running" and c["health"] in ("", "healthy", "none") for c in containers ) out = { "env_id": cfg.env_id, "site": cfg.site, "transport": cfg.transport, "agent_version": __version__, "laptop_mode": cfg.laptop_mode, "token_present": token_present, "bootstrap_grant_present": bootstrap_present, "token": {"accessor": st.token.accessor, "issued_at": st.token.issued_at, "source": st.token.source}, "applied_sha": st.applied_sha, "desired_sha": st.desired_sha, "in_sync": bool(st.applied_sha) and st.applied_sha == st.desired_sha, "last_checkin_at": st.last_checkin_at, "last_report_at": st.last_report_at, "last_action": st.last_action, "last_result": st.last_result, "last_error": st.last_error, "current_release": os.path.realpath(current) if current.exists() else None, "containers": containers, "healthy": healthy, } if args.json: print(json.dumps(out, indent=2, sort_keys=True)) return EX_OK print( f"monky-deployd {__version__} — {cfg.env_id} ({cfg.site}, transport {cfg.transport}{', laptop' if cfg.laptop_mode else ''})" ) print( f" credentials : token {'present' if token_present else 'ABSENT'}" f"{' (accessor ' + st.token.accessor + ')' if st.token.accessor else ''}" f"{'; bootstrap grant waiting' if bootstrap_present else ''}" ) print(f" applied sha : {st.applied_sha or '-'}") print(f" desired sha : {st.desired_sha or '-'} {'in sync' if out['in_sync'] else 'NOT in sync'}") print(f" last checkin: {_fmt_ts(st.last_checkin_at)} action={st.last_action or '-'}") print(f" last report : {_fmt_ts(st.last_report_at)} result={st.last_result or '-'}") if st.last_error: print(f" last error : {st.last_error}") print(f" release : {out['current_release'] or '-'}") if containers: print(f" containers : {'healthy' if healthy else 'UNHEALTHY'}") for c in containers: print(f" - {c['name']}: {c['state']} {c['health'] or ''}".rstrip()) else: print(" containers : none (docker unavailable or nothing deployed)") return EX_OK if (token_present and (out["in_sync"] or not st.desired_sha)) else EX_FAIL def cmd_version(_args) -> int: print(__version__) return EX_OK def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser(prog="monky-deployd", description="Monky backend pull agent (ADR-0028)") p.add_argument("-c", "--config", default=os.environ.get("MONKY_DEPLOYD_CONFIG", DEFAULT_CONFIG_PATH)) p.add_argument("-v", "--verbose", action="store_true") sub = p.add_subparsers(dest="cmd", required=True) r = sub.add_parser("run", help="reconcile: --once for the timer/oneshot, otherwise loop") r.add_argument("--once", action="store_true") r.add_argument( "--prune", action="store_true", help="after a successful apply, drop old releases + docker image prune" ) r.set_defaults(fn=cmd_run) s = sub.add_parser("status", help="what this box has applied vs what tenancy wants") s.add_argument("--json", action="store_true") s.set_defaults(fn=cmd_status) b = sub.add_parser("bootstrap", help="log in to OpenBao with the install kit's grant; store the token") b.add_argument("--force", action="store_true") b.set_defaults(fn=cmd_bootstrap) sub.add_parser("version").set_defaults(fn=cmd_version) return p def main(argv: list[str] | None = None) -> int: args = build_parser().parse_args(argv) try: return int(args.fn(args)) except ConfigError as exc: print(f"config error: {exc}", file=sys.stderr) return EX_ENV_MISMATCH if "env_id" in str(exc) else EX_FAIL except KeyboardInterrupt: return 130