- 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 启停脚本
129 lines
4.9 KiB
Bash
Executable File
129 lines
4.9 KiB
Bash
Executable File
#!/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
|