#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ remote-host-agent 机器A端 CLI(供大模型智能体 / 人调用) 用法: hostctl.py status # 查看某台主机实时状态 hostctl.py hosts # 列出所有主机 hostctl.py run "<命令>" [--timeout N] [--wait] # 下发命令并取回结果 hostctl.py history [--limit N] # 历史指标 hostctl.py commands [--host H] [--limit N] # 命令记录 hostctl.py health # 健康检查 hostctl.py config # 查看配置 零第三方依赖(标准库 urllib)。 """ import argparse import base64 import json import os import sys import time import urllib.parse import urllib.request from pathlib import Path BASE_DIR = Path(__file__).resolve().parent TOKEN_FILE = BASE_DIR / "data" / "token.txt" SERVER = os.environ.get("HOST_AGENT_SERVER", "http://127.0.0.1:16018") def get_token(): if TOKEN_FILE.exists(): tok = TOKEN_FILE.read_text().strip() if tok: return tok tok = os.environ.get("HOST_AGENT_TOKEN", "") if not tok: print("[错误] 未找到 token 文件 (data/token.txt)", file=sys.stderr) sys.exit(1) return tok def api(method, path, params=None, payload=None, timeout=70): token = get_token() url = SERVER.rstrip("/") + path if params: url += "?" + urllib.parse.urlencode(params) data = None headers = {"Authorization": f"Bearer {token}"} if payload is not None: data = json.dumps(payload).encode() headers["Content-Type"] = "application/json" req = urllib.request.Request(url, data=data, headers=headers, method=method) try: with urllib.request.urlopen(req, timeout=timeout) as resp: return json.loads(resp.read().decode()) except urllib.error.HTTPError as e: try: detail = json.loads(e.read().decode()).get("detail", str(e)) except Exception: detail = str(e) print(f"[错误] HTTP {e.code}: {detail}", file=sys.stderr) sys.exit(1) except Exception as e: print(f"[错误] 无法连接 {SERVER}: {e}", file=sys.stderr) sys.exit(1) # ---------- 输出 ---------- def fmt_pct(v): try: f = float(v) return f"{f:.1f}%" except Exception: return str(v) if v is not None else "-" def print_status(d): st = "🟢 在线" if d.get("online") else "🔴 离线" print(f"主机: {d.get('host')} ({st})") print(f"最近心跳: {time.strftime('%F %T', time.localtime(d.get('last_seen', 0)))} (距今 {d.get('age_sec')}s)") l = d.get("latest") or {} print(f"CPU: {fmt_pct(l.get('cpu'))}") print(f"内存: {fmt_pct(l.get('mem'))}") print(f"磁盘: {fmt_pct(l.get('disk'))}") print(f"负载: {l.get('load', '-')}") if l.get("uptime") is not None: print(f"开机: {int(l['uptime']) // 86400} 天 {int(l['uptime']) % 86400 // 3600} 小时") lc = d.get("last_command") if lc: print(f"\n最近命令 #{lc.get('id')}: {lc.get('cmd')} [{lc.get('status')}]") if lc.get("result"): print("结果:", lc["result"][:500]) # ---------- 子命令 ---------- def cmd_status(args): print_status(api("GET", "/api/status", {"host": args.host})) def cmd_hosts(args): d = api("GET", "/api/hosts") if not d.get("hosts"): print("(暂无主机)") return print(f"{'主机':<20} {'状态':<4} {'CPU':>8} {'内存':>8} {'磁盘':>8} {'最近心跳'}") for h in d["hosts"]: st = "🟢" if h.get("online") else "🔴" l = h.get("latest") or {} ts = time.strftime("%m-%d %H:%M", time.localtime(h.get("last_seen", 0))) print(f"{h.get('host',''):<20} {st:<4} {fmt_pct(l.get('cpu')):>8} {fmt_pct(l.get('mem')):>8} {fmt_pct(l.get('disk')):>8} {ts}") def cmd_run(args): r = api("POST", "/api/command", payload={ "host": args.host, "cmd": args.cmd, "timeout": args.timeout, "note": args.note, }) print(f"命令 #{r['cmd_id']} 已下发 -> {args.host}") if not args.wait: return # 轮询等结果 for _ in range(args.timeout + 20): time.sleep(2) d = api("GET", "/api/commands", {"host": args.host, "limit": 20}) for c in d["commands"]: if c["id"] == r["cmd_id"] and c["status"] in ("done", "failed"): print("=" * 40) print(c.get("result") or "(无输出)") print("=" * 40) print(f"[退出码: {'非0' if c['status']=='failed' else 0}]") return print("[超时] 等待结果超时,可稍后执行 hostctl.py commands 查看") def cmd_history(args): d = api("GET", "/api/history", {"host": args.host, "limit": args.limit}) if not d.get("points"): print(f"({args.host} 暂无历史数据)") return print(f"{'时间':<20} {'CPU':>8} {'内存':>8} {'磁盘':>8} 负载") for p in reversed(d["points"]): ts = time.strftime("%m-%d %H:%M:%S", time.localtime(p["ts"])) print(f"{ts:<20} {fmt_pct(p['cpu']):>8} {fmt_pct(p['mem']):>8} {fmt_pct(p['disk']):>8} {p.get('load','-')}") def cmd_commands(args): params = {"limit": args.limit} if args.host: params["host"] = args.host d = api("GET", "/api/commands", params) if not d.get("commands"): print("(无命令记录)") return for c in reversed(d["commands"]): ts = time.strftime("%m-%d %H:%M", time.localtime(c["created_at"])) print(f"#{c['id']:<4} [{ts}] {c['host']:<16} {c['status']:<7} {c['cmd']}") if c.get("note"): print(f" 备注: {c['note']}") def cmd_health(args): print(api("GET", "/api/health")) def cmd_config(args): d = api("GET", "/api/config") for k, v in d.items(): print(f"{k}: {v}") def main(): p = argparse.ArgumentParser(description="remote-host-agent CLI (机器A端)") sub = p.add_subparsers(dest="cmd", required=True) sp = sub.add_parser("status", help="查看主机状态"); sp.add_argument("host"); sp.set_defaults(fn=cmd_status) sp = sub.add_parser("hosts", help="列出所有主机"); sp.set_defaults(fn=cmd_hosts) sp = sub.add_parser("run", help="下发命令") sp.add_argument("host"); sp.add_argument("cmd") sp.add_argument("--timeout", type=int, default=30) sp.add_argument("--note", default="") sp.add_argument("--wait", action="store_true", help="等待执行结果") sp.set_defaults(fn=cmd_run) sp = sub.add_parser("history", help="历史指标") sp.add_argument("host"); sp.add_argument("--limit", type=int, default=30) sp.set_defaults(fn=cmd_history) sp = sub.add_parser("commands", help="命令记录") sp.add_argument("--host", default=None); sp.add_argument("--limit", type=int, default=30) sp.set_defaults(fn=cmd_commands) sp = sub.add_parser("health", help="健康检查"); sp.set_defaults(fn=cmd_health) sp = sub.add_parser("config", help="查看配置"); sp.set_defaults(fn=cmd_config) args = p.parse_args() args.fn(args) if __name__ == "__main__": main()