cang-ying/remote-node/tools/canger_persona.py

183 lines
5.6 KiB
Python
Executable File

#!/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)