- 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 启停脚本
184 lines
7.0 KiB
Python
Executable File
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()
|