platform: modularize api/gui, add docs-tests-web foundation, and refresh root config
This commit is contained in:
168
scripts/transport_runbook.py
Executable file
168
scripts/transport_runbook.py
Executable file
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
|
||||
def request_json(api_url: str, method: str, path: str, payload: Optional[Dict] = None) -> Tuple[int, Dict]:
|
||||
data = None
|
||||
headers = {"Accept": "application/json"}
|
||||
if payload is not None:
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
req = urllib.request.Request(
|
||||
f"{api_url.rstrip('/')}{path}",
|
||||
data=data,
|
||||
method=method.upper(),
|
||||
headers=headers,
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30.0) as resp:
|
||||
raw = resp.read().decode("utf-8", errors="replace")
|
||||
status = int(resp.getcode() or 200)
|
||||
except urllib.error.HTTPError as e:
|
||||
raw = e.read().decode("utf-8", errors="replace")
|
||||
status = int(e.code or 500)
|
||||
except Exception as e:
|
||||
return 0, {"ok": False, "message": str(e), "code": "HTTP_CLIENT_ERROR"}
|
||||
|
||||
try:
|
||||
parsed = json.loads(raw) if raw else {}
|
||||
except Exception:
|
||||
parsed = {"raw": raw}
|
||||
if not isinstance(parsed, dict):
|
||||
parsed = {"raw": parsed}
|
||||
return status, parsed
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Transport runbook helper for /api/v1/transport/*")
|
||||
parser.add_argument("--api-url", default=os.environ.get("API_URL", "http://127.0.0.1:8080"))
|
||||
parser.add_argument("--client-id", default="")
|
||||
parser.add_argument("--kind", default="singbox", choices=["singbox", "dnstt", "phoenix"])
|
||||
parser.add_argument("--name", default="")
|
||||
parser.add_argument("--enabled", action="store_true")
|
||||
parser.add_argument("--config-json", default='{"runner":"mock","runtime_mode":"exec"}')
|
||||
parser.add_argument("--actions", default="capabilities")
|
||||
parser.add_argument("--force-delete", action="store_true")
|
||||
parser.add_argument("--allow-fail", default="")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def require_client_id(action: str, client_id: str) -> None:
|
||||
if action == "capabilities":
|
||||
return
|
||||
if not client_id.strip():
|
||||
raise ValueError(f"--client-id is required for action '{action}'")
|
||||
|
||||
|
||||
def format_summary(action: str, status: int, payload: Dict) -> str:
|
||||
ok = payload.get("ok")
|
||||
code = payload.get("code")
|
||||
msg = payload.get("message")
|
||||
extras: List[str] = []
|
||||
if "status" in payload:
|
||||
extras.append(f"status={payload.get('status')}")
|
||||
if "status_before" in payload:
|
||||
extras.append(f"before={payload.get('status_before')}")
|
||||
if "status_after" in payload:
|
||||
extras.append(f"after={payload.get('status_after')}")
|
||||
extra_text = f" {' '.join(extras)}" if extras else ""
|
||||
return f"[transport_runbook] {action}: http={status} ok={ok} code={code} message={msg}{extra_text}"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
api_url = args.api_url.strip()
|
||||
if not api_url:
|
||||
print("[transport_runbook] ERROR: empty --api-url")
|
||||
return 1
|
||||
|
||||
actions = [a.strip().lower() for a in args.actions.split(",") if a.strip()]
|
||||
if not actions:
|
||||
print("[transport_runbook] ERROR: --actions must not be empty")
|
||||
return 1
|
||||
allow_fail = {a.strip().lower() for a in args.allow_fail.split(",") if a.strip()}
|
||||
|
||||
try:
|
||||
cfg = json.loads(args.config_json)
|
||||
except Exception as e:
|
||||
print(f"[transport_runbook] ERROR: invalid --config-json: {e}")
|
||||
return 1
|
||||
if not isinstance(cfg, dict):
|
||||
print("[transport_runbook] ERROR: --config-json must be a JSON object")
|
||||
return 1
|
||||
|
||||
for action in actions:
|
||||
try:
|
||||
require_client_id(action, args.client_id)
|
||||
except ValueError as e:
|
||||
print(f"[transport_runbook] ERROR: {e}")
|
||||
return 1
|
||||
|
||||
path = ""
|
||||
method = "GET"
|
||||
payload: Optional[Dict] = None
|
||||
|
||||
if action == "capabilities":
|
||||
path = "/api/v1/transport/capabilities"
|
||||
method = "GET"
|
||||
elif action == "create":
|
||||
path = "/api/v1/transport/clients"
|
||||
method = "POST"
|
||||
payload = {
|
||||
"id": args.client_id,
|
||||
"name": args.name.strip() or f"Runbook {args.client_id}",
|
||||
"kind": args.kind,
|
||||
"enabled": bool(args.enabled),
|
||||
"config": cfg,
|
||||
}
|
||||
elif action == "provision":
|
||||
path = f"/api/v1/transport/clients/{urllib.parse.quote(args.client_id)}/provision"
|
||||
method = "POST"
|
||||
elif action == "start":
|
||||
path = f"/api/v1/transport/clients/{urllib.parse.quote(args.client_id)}/start"
|
||||
method = "POST"
|
||||
elif action == "health":
|
||||
path = f"/api/v1/transport/clients/{urllib.parse.quote(args.client_id)}/health"
|
||||
method = "GET"
|
||||
elif action == "metrics":
|
||||
path = f"/api/v1/transport/clients/{urllib.parse.quote(args.client_id)}/metrics"
|
||||
method = "GET"
|
||||
elif action == "restart":
|
||||
path = f"/api/v1/transport/clients/{urllib.parse.quote(args.client_id)}/restart"
|
||||
method = "POST"
|
||||
elif action == "stop":
|
||||
path = f"/api/v1/transport/clients/{urllib.parse.quote(args.client_id)}/stop"
|
||||
method = "POST"
|
||||
elif action == "delete":
|
||||
q = "?force=true" if args.force_delete else ""
|
||||
path = f"/api/v1/transport/clients/{urllib.parse.quote(args.client_id)}{q}"
|
||||
method = "DELETE"
|
||||
else:
|
||||
print(f"[transport_runbook] ERROR: unsupported action '{action}'")
|
||||
return 1
|
||||
|
||||
status, res = request_json(api_url, method, path, payload)
|
||||
print(format_summary(action, status, res))
|
||||
if status != 200:
|
||||
if action in allow_fail:
|
||||
continue
|
||||
return 1
|
||||
if not bool(res.get("ok", False)) and action not in allow_fail:
|
||||
return 1
|
||||
|
||||
print("[transport_runbook] done")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user