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 启停脚本
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
data/
|
||||||
|
logs/
|
||||||
|
config.sh
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# remote-host-agent
|
||||||
|
|
||||||
|
大模型智能体(机器A,有固定 IP)查看/控制机器B(无固定 IP)状态的轻量方案。
|
||||||
|
|
||||||
|
**方案A:HTTP 上报 + 命令队列长轮询**。B 端只需 bash + curl(系统自带,零安装)。
|
||||||
|
|
||||||
|
```
|
||||||
|
机器B (无固定IP) 机器A (121.40.164.32:16018)
|
||||||
|
┌───────────────────┐ HTTPS ┌──────────────────────────┐
|
||||||
|
│ agent.sh (轻量) │ ──────────→ │ collector.py (FastAPI) │ ←── hostctl.py / 智能体
|
||||||
|
│ · 采集CPU/内存/磁盘 │ ←───────── │ · /api/report 收指标 │
|
||||||
|
│ · 长轮询命令执行 │ 命令+结果 │ · /api/poll 下发命令 │
|
||||||
|
└───────────────────┘ │ · /api/status 查状态 │
|
||||||
|
└──────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
remote-host-agent/
|
||||||
|
├── collector.py 机器A 端服务(FastAPI, 端口 16018)
|
||||||
|
├── start.sh 机器A 服务启停(PID 管理)
|
||||||
|
├── hostctl.py 机器A 端 CLI(智能体/人调用)
|
||||||
|
├── agent.sh 机器B 端轻量 agent(核心)
|
||||||
|
├── config.sh.example 机器B 配置模板(复制为 config.sh)
|
||||||
|
├── host-agent.service 机器B 端 systemd 服务
|
||||||
|
└── data/ token.txt + host_agent.db(自动生成,勿提交)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 一、机器A 部署(本机)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd works/remote-host-agent
|
||||||
|
./start.sh # 启动(端口 16018,首次自动生成 data/token.txt)
|
||||||
|
./start.sh stop # 停止
|
||||||
|
```
|
||||||
|
|
||||||
|
启动后查看 token:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat data/token.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常用 CLI(hostctl.py)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 hostctl.py hosts # 列出所有主机+在线状态
|
||||||
|
python3 hostctl.py status <host> # 查看单台主机实时状态
|
||||||
|
python3 hostctl.py run <host> "df -h" --wait # 下发命令并等待结果
|
||||||
|
python3 hostctl.py run <host> "cat /proc/cpuinfo | head -20" --wait
|
||||||
|
python3 hostctl.py history <host> --limit 50 # 历史指标
|
||||||
|
python3 hostctl.py commands --host <host> # 命令执行记录
|
||||||
|
```
|
||||||
|
|
||||||
|
### API 一览
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| POST | /api/report | B 端上报指标(host/cpu/mem/disk/load/uptime) |
|
||||||
|
| GET | /api/poll | B 端长轮询取命令(阻塞 ≤65s) |
|
||||||
|
| POST | /api/result | B 端回传执行结果(base64) |
|
||||||
|
| GET | /api/status?host=X | 实时状态 |
|
||||||
|
| GET | /api/hosts | 主机列表 |
|
||||||
|
| GET | /api/history?host=X | 历史指标 |
|
||||||
|
| POST | /api/command | 下发命令 {host, cmd, timeout, note} |
|
||||||
|
| GET | /api/commands | 命令记录 |
|
||||||
|
| GET | /api/health / /api/config | 健康/配置 |
|
||||||
|
|
||||||
|
所有 API 需 `Authorization: Bearer <token>`。
|
||||||
|
|
||||||
|
## 二、机器B 部署(零安装,就 2 个文件)
|
||||||
|
|
||||||
|
1. 将 `agent.sh` 和 `config.sh`(由 `config.sh.example` 复制)放到 B 端任意目录(如 `/opt/host-agent/`)
|
||||||
|
|
||||||
|
2. 配置 `config.sh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SERVER="http://121.40.164.32:16018"
|
||||||
|
TOKEN="<机器A data/token.txt 里的值>"
|
||||||
|
HOST_NAME="web-server-01" # 自定义主机标识
|
||||||
|
INTERVAL=10
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 方式一(前台测试):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x agent.sh
|
||||||
|
./agent.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 方式二(systemd 常驻,推荐):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp host-agent.service /etc/systemd/system/
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now host-agent
|
||||||
|
journalctl -u host-agent -f # 看日志
|
||||||
|
```
|
||||||
|
|
||||||
|
## 三、大模型智能体集成
|
||||||
|
|
||||||
|
机器A 上智能体(OpenClaw)直接调用 CLI 即可,无需改 agent 代码:
|
||||||
|
|
||||||
|
```
|
||||||
|
"查机器B的CPU和内存" → exec: python3 hostctl.py status web-server-01
|
||||||
|
"在机器B上跑一下 df -h" → exec: python3 hostctl.py run web-server-01 "df -h" --wait
|
||||||
|
```
|
||||||
|
|
||||||
|
## 四、安全设计
|
||||||
|
|
||||||
|
- **Token 认证**:所有 API 必须带 `Authorization: Bearer <token>`,token 在 A 端首次启动自动生成(32字节随机)
|
||||||
|
- **命令白名单**:默认 `HOST_AGENT_CMD_MODE=strict`,只允许只读命令(df/free/top/ps/uptime/cat/ls/netstat 等),可用 `HOST_AGENT_CMD_MODE=open` 放开全部
|
||||||
|
- **超时保护**:B 端 `timeout` 强制命令超时(默认 30s),防挂死
|
||||||
|
- **离线检测**:A 端 3 分钟无心跳标记离线
|
||||||
|
- **审计**:所有命令记录落库(hostctl.py commands 可查)
|
||||||
|
|
||||||
|
## 五、版本
|
||||||
|
|
||||||
|
- v1.0.0:方案A 落地(监控上报 + 命令控制 + CLI + systemd + 白名单)
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# remote-host-agent 机器B端 agent.sh(轻量,仅需 bash+curl+base64)
|
||||||
|
# 功能:定时上报 CPU/内存/磁盘 等指标 + 长轮询执行机器A下发的命令
|
||||||
|
# 用法:
|
||||||
|
# 前台运行 ./agent.sh
|
||||||
|
# 后台常驻 nohup ./agent.sh > /var/log/host-agent.log 2>&1 &
|
||||||
|
# 系统服务 cp host-agent.service /etc/systemd/system/ && systemctl daemon-reload && systemctl enable --now host-agent
|
||||||
|
# 配置:同目录 config.sh(SERVER / TOKEN / HOST_NAME / INTERVAL)
|
||||||
|
# ============================================================
|
||||||
|
set -u
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# ---------- 加载配置 ----------
|
||||||
|
if [ -f "$SCRIPT_DIR/config.sh" ]; then
|
||||||
|
. "$SCRIPT_DIR/config.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SERVER="${SERVER:-http://121.40.164.32:16018}"
|
||||||
|
TOKEN="${TOKEN:-}"
|
||||||
|
HOST_NAME="${HOST_NAME:-$(hostname)}"
|
||||||
|
INTERVAL="${INTERVAL:-10}" # 上报间隔(秒)
|
||||||
|
POLL_TIMEOUT="${POLL_TIMEOUT:-65}" # 命令长轮询超时(秒),略大于服务端阻塞时间
|
||||||
|
CMD_TIMEOUT="${CMD_TIMEOUT:-30}" # 单条命令执行超时(秒)
|
||||||
|
|
||||||
|
if [ -z "$TOKEN" ]; then
|
||||||
|
echo "[错误] 未配置 TOKEN,请在 config.sh 中填写机器A生成的 token" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
UA="host-agent/1.0"
|
||||||
|
|
||||||
|
# ---------- 通用请求封装(保留备用;上报/结果用 curl -G 保证参数编码) ----------
|
||||||
|
req() { # req <method> <path> [--data ...]
|
||||||
|
local method="$1" path="$2"; shift 2
|
||||||
|
curl -s -m 15 -A "$UA" -X "$method" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
"$@" \
|
||||||
|
"http://${SERVER#http://}${path}" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 采集指标(全部系统自带命令) ----------
|
||||||
|
collect_cpu() {
|
||||||
|
# 取两次采样差值,避免 top 首次采样不准(awk 用 %d 强制整数,防科学计数法)
|
||||||
|
local c1 c2 id1 id2
|
||||||
|
c1=$(grep '^cpu ' /proc/stat | awk '{printf "%d", $2+$3+$4+$5+$6+$7+$8}')
|
||||||
|
id1=$(grep '^cpu ' /proc/stat | awk '{printf "%d", $5}')
|
||||||
|
sleep 1
|
||||||
|
c2=$(grep '^cpu ' /proc/stat | awk '{printf "%d", $2+$3+$4+$5+$6+$7+$8}')
|
||||||
|
id2=$(grep '^cpu ' /proc/stat | awk '{printf "%d", $5}')
|
||||||
|
local total=$((c2 - c1 + id2 - id1))
|
||||||
|
local idle=$((id2 - id1))
|
||||||
|
if [ "$total" -le 0 ]; then echo 0; else
|
||||||
|
awk -v t="$total" -v i="$idle" 'BEGIN{printf "%.1f", 100*(t-i)/t}'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_metrics() {
|
||||||
|
# CPU 使用率 %
|
||||||
|
CPU="$(collect_cpu)"
|
||||||
|
# 内存使用率 %(用行号取数据行,兼容中英文 locale)
|
||||||
|
local mt mu
|
||||||
|
read mt mu < <(free -m 2>/dev/null | awk 'NR==2{print $2, $3}')
|
||||||
|
if [ -n "$mt" ] && [ "$mt" -gt 0 ]; then
|
||||||
|
MEM=$(( mu * 100 / mt ))
|
||||||
|
else
|
||||||
|
MEM=0
|
||||||
|
fi
|
||||||
|
# 根分区磁盘使用率 %
|
||||||
|
DISK="$(df -h / 2>/dev/null | awk 'NR==2{gsub("%","",$5); print $5}')"
|
||||||
|
[ -z "$DISK" ] && DISK=0
|
||||||
|
# 负载
|
||||||
|
LOAD="$(cat /proc/loadavg 2>/dev/null | awk '{print $1" "$2" "$3}')"
|
||||||
|
# 开机秒数
|
||||||
|
UPTIME="$(awk '{print int($1)}' /proc/uptime 2>/dev/null)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 上报 ----------
|
||||||
|
report() {
|
||||||
|
collect_metrics
|
||||||
|
curl -s -m 15 -A "$UA" -X POST -G \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
--data-urlencode "host=${HOST_NAME}" \
|
||||||
|
--data-urlencode "cpu=${CPU}" \
|
||||||
|
--data-urlencode "mem=${MEM}" \
|
||||||
|
--data-urlencode "disk=${DISK}" \
|
||||||
|
--data-urlencode "load=${LOAD}" \
|
||||||
|
--data-urlencode "uptime=${UPTIME}" \
|
||||||
|
"http://${SERVER#http://}/api/report" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 长轮询取命令并执行 ----------
|
||||||
|
poll_and_run() {
|
||||||
|
local resp
|
||||||
|
resp=$(curl -s -m "$POLL_TIMEOUT" -A "$UA" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
"http://${SERVER#http://}/api/poll?host=${HOST_NAME}" 2>/dev/null) || return 0
|
||||||
|
# 解析 cmd_id / cmd(用 grep 轻量解析,避免依赖 jq)
|
||||||
|
local cmd_id cmd
|
||||||
|
cmd_id=$(echo "$resp" | grep -o '"cmd_id":[0-9]*' | head -1 | cut -d: -f2)
|
||||||
|
cmd=$(echo "$resp" | sed -n 's/.*"cmd":"\([^"]*\)".*/\1/p')
|
||||||
|
if [ -n "$cmd_id" ] && [ -n "$cmd" ]; then
|
||||||
|
# 还原 JSON 转义
|
||||||
|
cmd=$(printf '%b' "$cmd" | sed 's/\\n/\n/g; s/\\"/"/g; s/\\\\/\\/g')
|
||||||
|
local out exitcode
|
||||||
|
out=$(timeout "$CMD_TIMEOUT" bash -c "$cmd" 2>&1)
|
||||||
|
exitcode=$?
|
||||||
|
local b64
|
||||||
|
b64=$(printf '%s' "$out" | base64 -w0 2>/dev/null || printf '%s' "$out" | base64)
|
||||||
|
# 用 curl -G + --data-urlencode,避免 base64 中的 + / = 被 URL 解码破坏
|
||||||
|
curl -s -m 15 -A "$UA" -X POST -G \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
--data-urlencode "host=${HOST_NAME}" \
|
||||||
|
--data-urlencode "cmd_id=${cmd_id}" \
|
||||||
|
--data-urlencode "output=${b64}" \
|
||||||
|
--data-urlencode "exit_code=${exitcode}" \
|
||||||
|
"http://${SERVER#http://}/api/result" >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 主循环(常驻,永不退出) ----------
|
||||||
|
echo "[$(date '+%F %T')] host-agent 启动: host=${HOST_NAME} server=${SERVER} interval=${INTERVAL}s"
|
||||||
|
while true; do
|
||||||
|
report
|
||||||
|
poll_and_run
|
||||||
|
sleep "$INTERVAL"
|
||||||
|
done
|
||||||
+403
@@ -0,0 +1,403 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
remote-host-agent - 机器B状态监控/控制 Collector 服务(运行在机器A,固定IP端)
|
||||||
|
方案A:HTTP 上报 + 命令队列长轮询
|
||||||
|
- POST /api/report B 端定时上报 CPU/内存/磁盘等指标
|
||||||
|
- GET /api/poll B 端长轮询取命令(阻塞最多 65s)
|
||||||
|
- POST /api/result B 端回传命令执行结果(base64)
|
||||||
|
- GET /api/status 查询某台主机实时状态
|
||||||
|
- GET /api/hosts 列出所有已注册主机
|
||||||
|
- GET /api/history 查询历史指标
|
||||||
|
- POST /api/command 下发命令到某台主机的队列
|
||||||
|
- GET /api/health 健康检查
|
||||||
|
- GET /api/config 查看当前配置(不含 token)
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import base64
|
||||||
|
import secrets
|
||||||
|
import sqlite3
|
||||||
|
import threading
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Request, HTTPException, Depends, Query
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
# ---------- 基础配置 ----------
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
DATA_DIR = BASE_DIR / "data"
|
||||||
|
LOG_DIR = BASE_DIR / "logs"
|
||||||
|
DATA_DIR.mkdir(exist_ok=True)
|
||||||
|
LOG_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
DB_PATH = DATA_DIR / "host_agent.db"
|
||||||
|
TOKEN_FILE = DATA_DIR / "token.txt"
|
||||||
|
|
||||||
|
HOST = os.environ.get("HOST_AGENT_HOST", "0.0.0.0")
|
||||||
|
PORT = int(os.environ.get("HOST_AGENT_PORT", "16018"))
|
||||||
|
|
||||||
|
# 命令控制模式:strict = 白名单(默认,安全);open = 允许任意命令(仅审计)
|
||||||
|
CMD_MODE = os.environ.get("HOST_AGENT_CMD_MODE", "strict").lower()
|
||||||
|
|
||||||
|
# B 端上报心跳超时(秒),超过则标记离线
|
||||||
|
OFFLINE_AFTER = int(os.environ.get("HOST_AGENT_OFFLINE_AFTER", "180"))
|
||||||
|
|
||||||
|
# 长轮询最长阻塞时间(秒)
|
||||||
|
POLL_WAIT = 65
|
||||||
|
|
||||||
|
# ---------- 命令白名单(strict 模式) ----------
|
||||||
|
# 只允许以这些命令名开头的命令(按空格分隔的第一个 token 精确匹配)
|
||||||
|
CMD_WHITELIST = {
|
||||||
|
"df", "free", "top", "ps", "uptime", "uname", "hostname", "whoami",
|
||||||
|
"cat", "ls", "find", "du", "netstat", "ss", "sar", "vmstat", "iostat",
|
||||||
|
"mpstat", "pgrep", "lsof", "date", "echo", "ip", "ifconfig", "mount",
|
||||||
|
"sysctl", "lscpu", "lsblk", "nproc", "getconf", "id", "dmesg", "lspci",
|
||||||
|
"pwd", "head", "tail", "grep", "wc", "file", "stat", "readlink",
|
||||||
|
"hostnamectl", "systemctl", "journalctl", "nvidia-smi", "psutil",
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 日志 ----------
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||||
|
handlers=[
|
||||||
|
logging.StreamHandler(),
|
||||||
|
logging.FileHandler(LOG_DIR / "collector.log", encoding="utf-8"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
log = logging.getLogger("collector")
|
||||||
|
|
||||||
|
# ---------- Token ----------
|
||||||
|
def load_or_create_token():
|
||||||
|
if TOKEN_FILE.exists():
|
||||||
|
token = TOKEN_FILE.read_text().strip()
|
||||||
|
if token:
|
||||||
|
return token
|
||||||
|
token = os.environ.get("HOST_AGENT_TOKEN") or secrets.token_urlsafe(32)
|
||||||
|
TOKEN_FILE.write_text(token)
|
||||||
|
log.info("已%s token 文件: %s", "更新" if os.environ.get("HOST_AGENT_TOKEN") else "生成", TOKEN_FILE)
|
||||||
|
return token
|
||||||
|
|
||||||
|
TOKEN = load_or_create_token()
|
||||||
|
|
||||||
|
# ---------- 数据库 ----------
|
||||||
|
def get_conn():
|
||||||
|
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.executescript("""
|
||||||
|
CREATE TABLE IF NOT EXISTS hosts (
|
||||||
|
host TEXT PRIMARY KEY,
|
||||||
|
name TEXT,
|
||||||
|
first_seen REAL,
|
||||||
|
last_seen REAL,
|
||||||
|
online INTEGER DEFAULT 0,
|
||||||
|
latest TEXT
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS metrics (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
host TEXT,
|
||||||
|
ts REAL,
|
||||||
|
cpu REAL,
|
||||||
|
mem REAL,
|
||||||
|
disk REAL,
|
||||||
|
load TEXT,
|
||||||
|
uptime INTEGER,
|
||||||
|
extra TEXT
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_metrics_host_ts ON metrics(host, ts);
|
||||||
|
CREATE TABLE IF NOT EXISTS commands (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
host TEXT,
|
||||||
|
cmd TEXT,
|
||||||
|
timeout INTEGER DEFAULT 30,
|
||||||
|
status TEXT DEFAULT 'pending', -- pending/sent/done/failed
|
||||||
|
note TEXT,
|
||||||
|
created_at REAL,
|
||||||
|
sent_at REAL,
|
||||||
|
result TEXT,
|
||||||
|
result_at REAL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_commands_host_status ON commands(host, status);
|
||||||
|
""")
|
||||||
|
|
||||||
|
# ---------- 命令队列唤醒(长轮询用) ----------
|
||||||
|
_host_events = {}
|
||||||
|
_host_events_lock = threading.Lock()
|
||||||
|
|
||||||
|
def event_for(host):
|
||||||
|
with _host_events_lock:
|
||||||
|
if host not in _host_events:
|
||||||
|
_host_events[host] = threading.Event()
|
||||||
|
return _host_events[host]
|
||||||
|
|
||||||
|
def notify_host(host):
|
||||||
|
event_for(host).set()
|
||||||
|
|
||||||
|
def reset_host_event(host):
|
||||||
|
event_for(host).clear()
|
||||||
|
|
||||||
|
# ---------- 认证 ----------
|
||||||
|
def check_auth(authorization: str):
|
||||||
|
if not authorization or not authorization.startswith("Bearer "):
|
||||||
|
raise HTTPException(status_code=401, detail="缺少 Bearer token")
|
||||||
|
if authorization[len("Bearer "):].strip() != TOKEN:
|
||||||
|
raise HTTPException(status_code=401, detail="token 无效")
|
||||||
|
|
||||||
|
# ---------- 命令校验 ----------
|
||||||
|
def validate_command(cmd: str) -> str:
|
||||||
|
cmd = cmd.strip()
|
||||||
|
if not cmd:
|
||||||
|
raise HTTPException(status_code=400, detail="命令不能为空")
|
||||||
|
if len(cmd) > 2048:
|
||||||
|
raise HTTPException(status_code=400, detail="命令过长")
|
||||||
|
if CMD_MODE == "strict":
|
||||||
|
first = cmd.split()[0].lstrip("$").strip()
|
||||||
|
if first not in CMD_WHITELIST:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail=f"strict 模式下命令被白名单拦截: {first}。可用 HOST_AGENT_CMD_MODE=open 放开",
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
# ---------- FastAPI ----------
|
||||||
|
app = FastAPI(title="Remote Host Agent", version="1.0.0")
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
def on_startup():
|
||||||
|
init_db()
|
||||||
|
log.info("Collector 启动: %s:%s CMD_MODE=%s", HOST, PORT, CMD_MODE)
|
||||||
|
log.info("Token 已写入 %s(agent.sh / hostctl 需使用)", TOKEN_FILE)
|
||||||
|
|
||||||
|
@app.get("/api/health")
|
||||||
|
def health():
|
||||||
|
return {"status": "ok", "time": time.time()}
|
||||||
|
|
||||||
|
@app.get("/api/config")
|
||||||
|
def config():
|
||||||
|
return {
|
||||||
|
"port": PORT,
|
||||||
|
"cmd_mode": CMD_MODE,
|
||||||
|
"offline_after": OFFLINE_AFTER,
|
||||||
|
"whitelist_count": len(CMD_WHITELIST) if CMD_MODE == "strict" else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- B 端上报 ----------
|
||||||
|
@app.post("/api/report")
|
||||||
|
def report(
|
||||||
|
request: Request,
|
||||||
|
host: str = Query(..., description="主机标识"),
|
||||||
|
cpu: float = Query(None),
|
||||||
|
mem: float = Query(None),
|
||||||
|
disk: float = Query(None),
|
||||||
|
load: str = Query(None),
|
||||||
|
uptime: int = Query(None),
|
||||||
|
):
|
||||||
|
check_auth(request.headers.get("authorization", ""))
|
||||||
|
host = host.strip()[:64]
|
||||||
|
now = time.time()
|
||||||
|
extra = {}
|
||||||
|
for k, v in request.query_params.items():
|
||||||
|
if k.startswith("x_"):
|
||||||
|
extra[k[2:]] = v
|
||||||
|
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO metrics(host,ts,cpu,mem,disk,load,uptime,extra) VALUES(?,?,?,?,?,?,?,?)",
|
||||||
|
(host, now, cpu, mem, disk, load, uptime, json.dumps(extra, ensure_ascii=False)),
|
||||||
|
)
|
||||||
|
conn.execute("""
|
||||||
|
INSERT INTO hosts(host,name,first_seen,last_seen,online,latest)
|
||||||
|
VALUES(?,?,?,?,1,?)
|
||||||
|
ON CONFLICT(host) DO UPDATE SET
|
||||||
|
last_seen=excluded.last_seen, online=1, latest=excluded.latest
|
||||||
|
""", (host, host, now, now, json.dumps({
|
||||||
|
"cpu": cpu, "mem": mem, "disk": disk, "load": load,
|
||||||
|
"uptime": uptime, "extra": extra, "ts": now,
|
||||||
|
}, ensure_ascii=False)))
|
||||||
|
return {"ok": True, "time": now}
|
||||||
|
|
||||||
|
# ---------- B 端长轮询取命令 ----------
|
||||||
|
@app.get("/api/poll")
|
||||||
|
def poll(request: Request, host: str = Query(...)):
|
||||||
|
check_auth(request.headers.get("authorization", ""))
|
||||||
|
host = host.strip()[:64]
|
||||||
|
# 先标记在线
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.execute("UPDATE hosts SET last_seen=?, online=1 WHERE host=?", (time.time(), host))
|
||||||
|
deadline = time.time() + POLL_WAIT
|
||||||
|
ev = event_for(host)
|
||||||
|
while time.time() < deadline:
|
||||||
|
with get_conn() as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT id,host,cmd,timeout FROM commands WHERE host=? AND status='pending' ORDER BY id LIMIT 1",
|
||||||
|
(host,),
|
||||||
|
).fetchone()
|
||||||
|
if row:
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE commands SET status='sent', sent_at=? WHERE id=?",
|
||||||
|
(time.time(), row["id"]),
|
||||||
|
)
|
||||||
|
reset_host_event(host)
|
||||||
|
return {
|
||||||
|
"cmd_id": row["id"],
|
||||||
|
"cmd": row["cmd"],
|
||||||
|
"timeout": row["timeout"],
|
||||||
|
}
|
||||||
|
# 等待新的命令(被 POST /api/command 唤醒)
|
||||||
|
ev.wait(timeout=min(2.0, deadline - time.time()))
|
||||||
|
reset_host_event(host)
|
||||||
|
return {"cmd_id": None, "cmd": None}
|
||||||
|
|
||||||
|
# ---------- B 端回传结果 ----------
|
||||||
|
@app.post("/api/result")
|
||||||
|
def result(
|
||||||
|
request: Request,
|
||||||
|
host: str = Query(...),
|
||||||
|
cmd_id: int = Query(...),
|
||||||
|
output: str = Query(..., description="命令输出(base64 编码)"),
|
||||||
|
exit_code: int = Query(0),
|
||||||
|
):
|
||||||
|
check_auth(request.headers.get("authorization", ""))
|
||||||
|
host = host.strip()[:64]
|
||||||
|
try:
|
||||||
|
out_text = base64.b64decode(output).decode("utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
out_text = "[解码失败]"
|
||||||
|
with get_conn() as conn:
|
||||||
|
cur = conn.execute(
|
||||||
|
"UPDATE commands SET status=?, result=?, result_at=? WHERE id=? AND host=?",
|
||||||
|
("done" if exit_code == 0 else "failed", out_text, time.time(), cmd_id, host),
|
||||||
|
)
|
||||||
|
if cur.rowcount == 0:
|
||||||
|
return {"ok": False, "detail": "未找到对应命令"}
|
||||||
|
log.info("命令 #%s (%s) 完成 exit=%s", cmd_id, host, exit_code)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
# ---------- 查询 ----------
|
||||||
|
@app.get("/api/hosts")
|
||||||
|
def hosts(request: Request):
|
||||||
|
check_auth(request.headers.get("authorization", ""))
|
||||||
|
now = time.time()
|
||||||
|
with get_conn() as conn:
|
||||||
|
rows = conn.execute("SELECT * FROM hosts").fetchall()
|
||||||
|
result = []
|
||||||
|
for r in rows:
|
||||||
|
latest = json.loads(r["latest"]) if r["latest"] else {}
|
||||||
|
online = (now - r["last_seen"]) < OFFLINE_AFTER
|
||||||
|
result.append({
|
||||||
|
"host": r["host"],
|
||||||
|
"name": r["name"],
|
||||||
|
"online": online,
|
||||||
|
"last_seen": r["last_seen"],
|
||||||
|
"first_seen": r["first_seen"],
|
||||||
|
"latest": latest,
|
||||||
|
})
|
||||||
|
# 后台清理离线标记(延迟处理,不影响返回)
|
||||||
|
if result:
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.execute("UPDATE hosts SET online=0 WHERE ? - last_seen > ?", (now, OFFLINE_AFTER))
|
||||||
|
return {"hosts": result}
|
||||||
|
|
||||||
|
@app.get("/api/status")
|
||||||
|
def status(request: Request, host: str = Query(...)):
|
||||||
|
check_auth(request.headers.get("authorization", ""))
|
||||||
|
now = time.time()
|
||||||
|
with get_conn() as conn:
|
||||||
|
row = conn.execute("SELECT * FROM hosts WHERE host=?", (host,)).fetchone()
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail=f"主机 {host} 未注册")
|
||||||
|
latest = json.loads(row["latest"]) if row["latest"] else {}
|
||||||
|
online = (now - row["last_seen"]) < OFFLINE_AFTER
|
||||||
|
last_cmd = conn.execute(
|
||||||
|
"SELECT id,cmd,status,result,created_at,result_at FROM commands "
|
||||||
|
"WHERE host=? AND status IN ('done','failed') ORDER BY id DESC LIMIT 1",
|
||||||
|
(host,),
|
||||||
|
).fetchone()
|
||||||
|
data = {
|
||||||
|
"host": row["host"],
|
||||||
|
"name": row["name"],
|
||||||
|
"online": online,
|
||||||
|
"last_seen": row["last_seen"],
|
||||||
|
"age_sec": round(now - row["last_seen"], 1),
|
||||||
|
"latest": latest,
|
||||||
|
}
|
||||||
|
if last_cmd:
|
||||||
|
data["last_command"] = dict(last_cmd)
|
||||||
|
return data
|
||||||
|
|
||||||
|
@app.get("/api/history")
|
||||||
|
def history(
|
||||||
|
request: Request,
|
||||||
|
host: str = Query(...),
|
||||||
|
limit: int = Query(30, ge=1, le=1000),
|
||||||
|
):
|
||||||
|
check_auth(request.headers.get("authorization", ""))
|
||||||
|
with get_conn() as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT ts,cpu,mem,disk,load,uptime FROM metrics WHERE host=? ORDER BY ts DESC LIMIT ?",
|
||||||
|
(host, limit),
|
||||||
|
).fetchall()
|
||||||
|
return {"host": host, "count": len(rows), "points": [dict(r) for r in rows]}
|
||||||
|
|
||||||
|
@app.get("/api/commands")
|
||||||
|
def list_commands(
|
||||||
|
request: Request,
|
||||||
|
host: str = Query(None),
|
||||||
|
limit: int = Query(30, ge=1, le=200),
|
||||||
|
):
|
||||||
|
check_auth(request.headers.get("authorization", ""))
|
||||||
|
with get_conn() as conn:
|
||||||
|
if host:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM commands WHERE host=? ORDER BY id DESC LIMIT ?", (host, limit),
|
||||||
|
).fetchall()
|
||||||
|
else:
|
||||||
|
rows = conn.execute("SELECT * FROM commands ORDER BY id DESC LIMIT ?", (limit,)).fetchall()
|
||||||
|
return {"commands": [dict(r) for r in rows]}
|
||||||
|
|
||||||
|
# ---------- 下发命令 ----------
|
||||||
|
@app.post("/api/command")
|
||||||
|
def command(
|
||||||
|
request: Request,
|
||||||
|
payload: dict,
|
||||||
|
):
|
||||||
|
check_auth(request.headers.get("authorization", ""))
|
||||||
|
host = str(payload.get("host", "")).strip()[:64]
|
||||||
|
cmd = str(payload.get("cmd", "")).strip()
|
||||||
|
timeout = int(payload.get("timeout", 30))
|
||||||
|
note = str(payload.get("note", ""))[:200]
|
||||||
|
if not host:
|
||||||
|
raise HTTPException(status_code=400, detail="缺少 host")
|
||||||
|
cmd = validate_command(cmd)
|
||||||
|
timeout = min(max(timeout, 1), 300)
|
||||||
|
now = time.time()
|
||||||
|
with get_conn() as conn:
|
||||||
|
cur = conn.execute(
|
||||||
|
"INSERT INTO commands(host,cmd,timeout,status,note,created_at) VALUES(?,?,?,?,?,?)",
|
||||||
|
(host, cmd, timeout, "pending", note, now),
|
||||||
|
)
|
||||||
|
cmd_id = cur.lastrowid
|
||||||
|
notify_host(host)
|
||||||
|
log.info("下发命令 #%s -> %s: %s", cmd_id, host, cmd)
|
||||||
|
return {"ok": True, "cmd_id": cmd_id}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
uvicorn.run(app, host=HOST, port=PORT, log_level="info")
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# ============================================================
|
||||||
|
# remote-host-agent 机器B端配置
|
||||||
|
# 复制为 config.sh 并填写后,与 agent.sh 放在同一目录
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# 机器A 的 collector 服务地址(A 有固定 IP/域名)
|
||||||
|
SERVER="http://121.40.164.32:16018"
|
||||||
|
|
||||||
|
# 认证 token(在机器A 执行: cat data/token.txt 获取)
|
||||||
|
TOKEN=""
|
||||||
|
|
||||||
|
# 本机标识(默认取 hostname,建议设为有意义的名称如 web-server-01)
|
||||||
|
HOST_NAME="$(hostname)"
|
||||||
|
|
||||||
|
# 上报间隔(秒)
|
||||||
|
INTERVAL=10
|
||||||
|
|
||||||
|
# 命令执行超时(秒)
|
||||||
|
CMD_TIMEOUT=30
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# ============================================================
|
||||||
|
# remote-host-agent 机器B端 systemd 服务
|
||||||
|
# 部署步骤(B 端 root):
|
||||||
|
# mkdir -p /opt/host-agent
|
||||||
|
# 将 agent.sh / config.sh 放入 /opt/host-agent/
|
||||||
|
# cp host-agent.service /etc/systemd/system/
|
||||||
|
# systemctl daemon-reload
|
||||||
|
# systemctl enable --now host-agent
|
||||||
|
# 常用:
|
||||||
|
# systemctl status host-agent
|
||||||
|
# journalctl -u host-agent -f
|
||||||
|
# ============================================================
|
||||||
|
[Unit]
|
||||||
|
Description=Remote Host Agent (machine B -> machine A)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=/opt/host-agent
|
||||||
|
ExecStart=/bin/bash /opt/host-agent/agent.sh
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=append:/var/log/host-agent.log
|
||||||
|
StandardError=append:/var/log/host-agent.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Executable
+183
@@ -0,0 +1,183 @@
|
|||||||
|
#!/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()
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# remote-host-agent collector 启停脚本(机器A端)
|
||||||
|
# ./start.sh 启动 | ./start.sh stop 停止 | ./start.sh restart 重启
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
PY=/home/hz1/miniconda3/envs/openclaw/bin/python3
|
||||||
|
PORT=${HOST_AGENT_PORT:-16018}
|
||||||
|
PID_FILE="logs/collector.pid"
|
||||||
|
mkdir -p logs
|
||||||
|
|
||||||
|
case "${1:-start}" in
|
||||||
|
stop)
|
||||||
|
if [ -f "$PID_FILE" ]; then
|
||||||
|
kill "$(cat "$PID_FILE")" 2>/dev/null && echo "已停止 collector (PID $(cat "$PID_FILE"))" || echo "进程不存在"
|
||||||
|
rm -f "$PID_FILE"
|
||||||
|
else
|
||||||
|
echo "未找到 PID 文件"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
"$0" stop; sleep 1; "$0" start
|
||||||
|
;;
|
||||||
|
start)
|
||||||
|
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
||||||
|
echo "collector 已在运行 (PID $(cat "$PID_FILE"), 端口 $PORT)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
nohup "$PY" collector.py > logs/collector.out 2>&1 &
|
||||||
|
echo $! > "$PID_FILE"
|
||||||
|
sleep 2
|
||||||
|
if kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
||||||
|
echo "collector 启动成功 (PID $(cat "$PID_FILE"), 端口 $PORT)"
|
||||||
|
echo "Token 文件: $(pwd)/data/token.txt"
|
||||||
|
if [ -f data/token.txt ]; then
|
||||||
|
echo "Token: $(cat data/token.txt)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "启动失败,查看 logs/collector.out"; exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "用法: $0 [start|stop|restart]"; exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user