#!/usr/bin/env python3 """Repository entry for Canger's persona to request and use approved access.""" from __future__ import annotations import argparse import json import os import sys import time from pathlib import Path from typing import Any from urllib.error import HTTPError, URLError from cangerctl import request ROOT = Path(__file__).resolve().parents[1] DEFAULT_PROFILE = ROOT / "config" / "pufferfish-persona.json" def load_profile(path: str) -> dict[str, Any]: with open(path, encoding="utf-8") as handle: profile = json.load(handle) required = { "persona_id", "human_owner", "node_id", "control_url", "default_hours", "minimum_hours", "maximum_hours", } missing = sorted(required - set(profile)) if missing: raise RuntimeError("persona profile is missing: " + ", ".join(missing)) return profile def persona_token() -> str: value = os.environ.get("CANGER_PERSONA_TOKEN", "") if len(value) < 24: raise RuntimeError( "CANGER_PERSONA_TOKEN is unavailable; run from the registered " "Singapore persona environment" ) return value def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="苍耳人格体的受控运维申请与执行入口" ) parser.add_argument("--profile", default=str(DEFAULT_PROFILE)) parser.add_argument("--url") parser.add_argument( "--requester-name", default=os.environ.get("GUANGHU_PERSONA_NAME", ""), help="本次实际申请者名称;也可用 GUANGHU_PERSONA_NAME", ) parser.add_argument( "--requester-id", default=os.environ.get("GUANGHU_PERSONA_ID", ""), help="本次实际申请者正式编号;也可用 GUANGHU_PERSONA_ID", ) sub = parser.add_subparsers(dest="command", required=True) request_access = sub.add_parser("request-access") request_access.add_argument("--hours", type=int) request_access.add_argument("--reason", required=True) task = sub.add_parser("task") task.add_argument("--action", required=True) task.add_argument("--args", default="{}") show = sub.add_parser("show") show.add_argument("task_id") watch = sub.add_parser("watch") watch.add_argument("task_id") sub.add_parser("grants") return parser.parse_args() def main() -> None: args = parse_args() profile = load_profile(args.profile) base = args.url or os.environ.get("CANGER_CONTROL_URL") or profile["control_url"] token = persona_token() node_id = profile["node_id"] if args.command in {"request-access", "task"}: if not args.requester_name or not args.requester_id: raise RuntimeError( "requester name and id are required; provide --requester-name " "and --requester-id, or GUANGHU_PERSONA_NAME/GUANGHU_PERSONA_ID" ) if args.command == "request-access": hours = args.hours or int(profile["default_hours"]) minimum = int(profile["minimum_hours"]) maximum = int(profile["maximum_hours"]) if not minimum <= hours <= maximum: raise RuntimeError(f"hours must be between {minimum} and {maximum}") result = request( base, token, "POST", "/v1/grants", { "node_id": node_id, "duration_seconds": hours * 3600, "reason": args.reason, "requester_name": args.requester_name, "requester_id": args.requester_id, }, ) elif args.command == "task": result = request( base, token, "POST", "/v1/tasks", { "node_id": node_id, "action": args.action, "args": json.loads(args.args), "requester_name": args.requester_name, "requester_id": args.requester_id, }, ) elif args.command == "show": result = request( base, token, "GET", f"/v1/tasks/{args.task_id}", ) elif args.command == "grants": result = request(base, token, "GET", "/v1/grants") else: after = 0 while True: events = request( base, token, "GET", f"/v1/tasks/{args.task_id}/events?after={after}", ) for event in events["events"]: after = max(after, event["seq"]) print( f"[{event['seq']}] {event['kind']}: " + json.dumps(event["payload"], ensure_ascii=False), flush=True, ) state = request( base, token, "GET", f"/v1/tasks/{args.task_id}", ) if state["status"] in {"completed", "rejected"}: result = state break time.sleep(1) print(json.dumps(result, ensure_ascii=False, indent=2)) if __name__ == "__main__": try: main() except HTTPError as exc: print(f"error: control plane returned HTTP {exc.code}", file=sys.stderr) raise SystemExit(2) except URLError as exc: print(f"error: cannot reach control plane: {exc.reason}", file=sys.stderr) raise SystemExit(2) except (RuntimeError, ValueError, json.JSONDecodeError, OSError) as exc: print(f"error: {exc}", file=sys.stderr) raise SystemExit(2)