Files
remote-host-agent/hostctl.py
T
hz4th_coder c877b25ff6 v1.0.0 remote-host-agent: 机器B监控/控制轻量方案(HTTP上报+命令长轮询)
- collector.py: 机器A端 FastAPI 服务(16018),token认证 + strict/open 命令白名单 + SQLite存储
- agent.sh: 机器B端轻量agent(仅bash+curl+base64,零安装),采集CPU/内存/磁盘/负载/开机时间 + 长轮询执行命令回传结果
- hostctl.py: 机器A端 CLI(status/hosts/run/history/commands)
- host-agent.service: 机器B端 systemd 服务
- start.sh: collector 启停脚本
2026-08-19 19:08:23 +08:00

184 lines
7.0 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
remote-host-agent 机器A端 CLI(供大模型智能体 / 人调用)
用法:
hostctl.py status <host> # 查看某台主机实时状态
hostctl.py hosts # 列出所有主机
hostctl.py run <host> "<命令>" [--timeout N] [--wait] # 下发命令并取回结果
hostctl.py history <host> [--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()