Files
NodeFlare/agent/agent.sh
T

392 lines
13 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
set -eu
SERVICE_NAME="nodeflare-agent"
INSTALL_DIR="/opt/nodeflare"
AGENT_FILE="$INSTALL_DIR/agent"
STATE_ROOT="/etc/nodeflare"
STATE_DIR="$STATE_ROOT/agent"
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
OPENRC_FILE="/etc/init.d/$SERVICE_NAME"
temporary=""
backup_agent=""
backup_service=""
rollback_agent=false
had_agent=false
had_service=false
usage() {
cat <<'EOF'
NodeFlare Agent 安装脚本
用法:
agent.sh -e <NodeFlare URL> -t <Agent Token> [-i <历史保存间隔>] [-m <下载加速前缀>]
agent.sh --status
agent.sh --uninstall
参数:
-e NodeFlare 服务地址(必填)
-t 后台生成的 Agent Token(必填,请勿泄露)
-i 初始历史保存间隔,15-3600 秒(默认 60)
-m GitHub 下载加速前缀(可选,仅作用于 Release 下载;
形如 https://ghproxy.net,脚本会自动拼接完整地址,摘要校验不受影响)
EOF
}
log() {
printf '[NodeFlare] %s\n' "$1"
}
fail() {
printf '[NodeFlare] 错误:%s\n' "$1" >&2
exit 1
}
cleanup_agent_install() {
if [ "$rollback_agent" = true ]; then
rollback_agent=false
log "安装未完成,正在恢复上一版本"
case "$init_system" in
systemd) systemctl stop "$SERVICE_NAME" 2>/dev/null || true ;;
openrc) rc-service "$SERVICE_NAME" stop 2>/dev/null || true ;;
esac
if [ "$had_agent" = true ]; then
cp -p "$backup_agent" "$AGENT_FILE" 2>/dev/null || true
else
rm -f "$AGENT_FILE"
fi
service_path=$SERVICE_FILE
[ "$init_system" != openrc ] || service_path=$OPENRC_FILE
if [ "$had_service" = true ]; then
cp -p "$backup_service" "$service_path" 2>/dev/null || true
else
rm -f "$service_path"
fi
case "$init_system" in
systemd)
systemctl daemon-reload 2>/dev/null || true
[ "$had_agent" != true ] || systemctl restart "$SERVICE_NAME" 2>/dev/null || true
;;
openrc) [ "$had_agent" != true ] || rc-service "$SERVICE_NAME" restart 2>/dev/null || true ;;
esac
fi
[ -z "$temporary" ] || rm -f "$temporary"
[ -z "$backup_agent" ] || rm -f "$backup_agent"
[ -z "$backup_service" ] || rm -f "$backup_service"
}
detect_glibc_version() {
[ -x "$glibc_loader" ] || return
LC_ALL=C "$glibc_loader" --version 2>/dev/null | awk '
tolower($0) ~ /glibc|gnu libc|gnu c library/ {
sub(/\.$/, "")
if (match($0, /[0-9]+\.[0-9]+(\.[0-9]+)?$/)) {
print substr($0, RSTART, RLENGTH)
exit
}
}
'
}
glibc_is_supported() {
glibc_major=${1%%.*}
glibc_minor=${1#*.}
glibc_minor=${glibc_minor%%.*}
case "$glibc_major:$glibc_minor" in
*[!0-9:]*|:*) return 1 ;;
esac
[ "$glibc_major" -gt 2 ] \
|| { [ "$glibc_major" -eq 2 ] && [ "$glibc_minor" -ge 28 ]; }
}
safe_value() {
case "$1" in *[!A-Za-z0-9_./:@-]*|'') return 1 ;; esac
}
detect_init_system() {
if [ -f /etc/alpine-release ] && command -v rc-service >/dev/null 2>&1; then
printf 'openrc'
elif [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then
printf 'systemd'
elif command -v rc-service >/dev/null 2>&1 && [ -d /etc/init.d ]; then
printf 'openrc'
else
printf 'unknown'
fi
}
ensure_curl() {
command -v curl >/dev/null 2>&1 && return
log "未找到 curl,正在通过系统包管理器安装"
if command -v apt-get >/dev/null 2>&1; then
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y curl
elif command -v dnf >/dev/null 2>&1; then
dnf install -y curl
elif command -v yum >/dev/null 2>&1; then
yum install -y curl
elif command -v apk >/dev/null 2>&1; then
apk add --no-cache curl
else
fail "缺少 curl,且未找到受支持的包管理器"
fi
command -v curl >/dev/null 2>&1 || fail "curl 安装失败"
}
explain_exec_failure() {
target="$1"
if [ ! -f "$target" ]; then
echo "下载文件在校验前丢失:$target" >&2
elif [ ! -x "$target" ]; then
echo "下载文件不可执行:$target" >&2
elif [ "$(dd if="$target" bs=4 count=1 2>/dev/null)" != "$(printf '\177ELF')" ]; then
echo "下载文件不是有效的 Linux ELF 可执行文件" >&2
else
echo "下载的 Agent 无法在当前系统运行,请确认系统架构与 Release 文件匹配" >&2
fi
}
install_agent() {
[ "$(id -u)" -eq 0 ] || fail "请使用 root 权限运行安装命令"
log "正在检查运行环境"
ensure_curl
command -v sha256sum >/dev/null || fail "缺少 sha256sum,无法校验下载文件"
token=""
endpoint=""
interval=60
interval_set=false
mirror=""
while [ "$#" -gt 0 ]; do
option="$1"
case "$option" in
-t|-e|-i|-m)
[ "$#" -ge 2 ] || { echo "参数 $option 缺少值" >&2; usage; exit 1; }
value="$2"
shift 2
;;
*) echo "未知参数:$option" >&2; usage; exit 1 ;;
esac
case "$option" in
-t) [ -z "$token" ] || fail "参数 $option 重复"; token="$value" ;;
-e) [ -z "$endpoint" ] || fail "参数 $option 重复"; endpoint="$value" ;;
-i) [ "$interval_set" = false ] || fail "参数 $option 重复"; interval="$value"; interval_set=true ;;
-m) [ -z "$mirror" ] || fail "参数 $option 重复"; mirror="$value" ;;
esac
done
[ -n "$token" ] && [ -n "$endpoint" ] || { usage; exit 1; }
endpoint=${endpoint%/}
[ ${#token} -le 512 ] && [ ${#endpoint} -le 2048 ] || fail "安装参数长度超出限制"
if ! safe_value "$token" || ! safe_value "$endpoint"; then
fail "服务地址或 Agent Token 格式无效"
fi
case "$endpoint" in
https://?*|http://localhost|http://localhost/*|http://localhost:*|http://127.0.0.1|http://127.0.0.1/*|http://127.0.0.1:*) ;;
*) fail "服务地址必须使用 HTTPS;仅本机调试可使用 HTTP" ;;
esac
case "$endpoint" in *@*) fail "服务地址不能包含用户信息" ;; esac
case "$interval" in ''|*[!0-9]*) fail "历史保存间隔必须是整数" ;; esac
[ "$interval" -ge 15 ] && [ "$interval" -le 3600 ] || fail "历史保存间隔必须在 15-3600 秒之间"
mirror=${mirror%/}
if [ -n "$mirror" ]; then
[ ${#mirror} -le 2048 ] || fail "下载加速前缀长度超出限制"
safe_value "$mirror" || fail "下载加速前缀格式无效"
case "$mirror" in
https://?*) ;;
http://localhost|http://localhost/*|http://localhost:*|http://127.0.0.1|http://127.0.0.1/*|http://127.0.0.1:*) ;;
*) fail "下载加速前缀必须使用 HTTPS;仅本机调试可使用 HTTP" ;;
esac
case "$mirror" in *@*) fail "下载加速前缀不能包含用户信息" ;; esac
fi
case "$(uname -m)" in
x86_64|amd64) arch="x64"; glibc_loader=/lib64/ld-linux-x86-64.so.2 ;;
aarch64|arm64) arch="aarch64"; glibc_loader=/lib/ld-linux-aarch64.so.1 ;;
*) fail "暂不支持当前 CPU 架构:$(uname -m)" ;;
esac
libc=musl
glibc_version=$(detect_glibc_version || true)
if [ -n "$glibc_version" ] && glibc_is_supported "$glibc_version"; then
libc=glibc
fi
init_system=$(detect_init_system)
[ "$init_system" != "unknown" ] || fail "未检测到正在运行的 systemd 或 OpenRC"
mkdir -p "$INSTALL_DIR" "$STATE_ROOT" "$STATE_DIR"
chmod 755 "$INSTALL_DIR"
chmod 700 "$STATE_ROOT"
chmod 750 "$STATE_DIR"
temporary="$INSTALL_DIR/.agent.$$.download"
trap cleanup_agent_install EXIT HUP INT TERM
artifact="agent-linux-$arch-$libc"
release_api="https://api.github.com/repos/imengying/NodeFlare/releases/latest"
log "正在获取 GitHub 最新正式版本($artifact)"
release_json=$(curl --fail --location --silent --show-error --max-time 30 \
-H 'Accept: application/vnd.github+json' \
-H 'User-Agent: nodeflare-installer' \
"$release_api")
release_tag=$(printf '%s\n' "$release_json" | tr ',' '\n' | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)
printf '%s\n' "$release_tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' || {
fail "GitHub 最新 Release 标签无效:${release_tag:-未找到}"
}
expected=$(printf '%s\n' "$release_json" | tr '{' '\n' | awk -v name="$artifact" '
{
compact = $0
gsub(/[[:space:]]/, "", compact)
if (index(compact, "\"name\":\"" name "\"") > 0) selected = 1
else if (index(compact, "\"name\":") > 0) selected = 0
}
selected {
marker = "\"digest\":\"sha256:"
position = index(compact, marker)
if (position == 0) next
digest = substr(compact, position + length(marker), 64)
if (length(digest) == 64 && digest !~ /[^0-9a-fA-F]/) {
print tolower(digest)
exit
}
}
')
[ -n "$expected" ] || fail "Release 缺少 $artifact 的 SHA-256 摘要"
release_base="https://github.com/imengying/NodeFlare/releases/download/$release_tag"
download_url="$release_base/$artifact"
if [ -n "$mirror" ]; then
download_url="$mirror/$release_base/$artifact"
log "正在通过下载加速前缀拉取 Agent $release_tag"
else
log "正在下载 NodeFlare Agent $release_tag"
fi
curl --fail --location --silent --show-error --max-time 120 \
"$download_url" \
-o "$temporary"
actual=$(sha256sum "$temporary" | awk '{ print $1 }')
[ -n "$expected" ] && [ "$actual" = "$expected" ] || fail "Agent SHA-256 校验失败,已停止安装"
log "下载校验通过,正在验证可执行文件"
chmod 755 "$temporary"
if ! installed_version=$("$temporary" --version); then
explain_exec_failure "$temporary"
exit 1
fi
installed_version=${installed_version##* }
printf '%s\n' "$installed_version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' || {
fail "Agent 返回了无效版本号:$installed_version"
}
[ "$installed_version" = "${release_tag#v}" ] || {
fail "Release $release_tag 与 Agent 版本 $installed_version 不一致"
}
backup_agent="$STATE_DIR/.agent.$$.previous"
service_path=$SERVICE_FILE
[ "$init_system" != openrc ] || service_path=$OPENRC_FILE
backup_service="$STATE_DIR/.service.$$.previous"
if [ -f "$AGENT_FILE" ]; then
cp -p "$AGENT_FILE" "$backup_agent"
had_agent=true
fi
if [ -f "$service_path" ]; then
cp -p "$service_path" "$backup_service"
had_service=true
fi
rollback_agent=true
case "$init_system" in
systemd) systemctl stop "$SERVICE_NAME" 2>/dev/null || true ;;
openrc) rc-service "$SERVICE_NAME" stop 2>/dev/null || true ;;
esac
mv "$temporary" "$AGENT_FILE"
log "正在配置并启动 $init_system 服务"
if [ "$init_system" = "systemd" ]; then
printf '%s\n' \
'[Unit]' \
'Description=NodeFlare Agent' \
'After=network-online.target' \
'Wants=network-online.target' \
'' \
'[Service]' \
'Type=simple' \
"ExecStart=$AGENT_FILE -e $endpoint -i $interval" \
'Restart=always' \
'RestartSec=10' \
'NoNewPrivileges=true' \
"Environment=NODEFLARE_AGENT_TOKEN=$token" \
"Environment=NODEFLARE_STATE_DIR=$STATE_DIR" \
'' \
'[Install]' \
'WantedBy=multi-user.target' > "$SERVICE_FILE"
chmod 600 "$SERVICE_FILE"
systemctl daemon-reload
systemctl enable --now "$SERVICE_NAME"
systemctl is-active --quiet "$SERVICE_NAME" || {
systemctl status "$SERVICE_NAME" --no-pager >&2 || true
fail "NodeFlare 服务启动失败,请查看上方状态信息"
}
elif [ "$init_system" = "openrc" ]; then
printf '%s\n' \
'#!/sbin/openrc-run' \
"name=\"$SERVICE_NAME\"" \
"command=\"$AGENT_FILE\"" \
"command_args=\"-e $endpoint -i $interval\"" \
"command_user=\"root\"" \
"export NODEFLARE_AGENT_TOKEN=\"$token\"" \
"export NODEFLARE_STATE_DIR=\"$STATE_DIR\"" \
"supervisor=\"supervise-daemon\"" \
"respawn_delay=10" \
'depend() { need net; }' > "$OPENRC_FILE"
chmod 700 "$OPENRC_FILE"
rc-update add "$SERVICE_NAME" default
rc-service "$SERVICE_NAME" restart
rc-service "$SERVICE_NAME" status >/dev/null || {
rc-service "$SERVICE_NAME" status >&2 || true
fail "NodeFlare 服务启动失败,请查看上方状态信息"
}
fi
rollback_agent=false
cleanup_agent_install
trap - EXIT HUP INT TERM
printf '\nNodeFlare Agent 安装完成\n'
printf ' 版本:%s\n' "$installed_version"
printf ' 服务:%s(%s)\n' "$SERVICE_NAME" "$init_system"
if [ "$init_system" = "systemd" ]; then
printf ' 查看状态:systemctl status %s\n' "$SERVICE_NAME"
else
printf ' 查看状态:rc-service %s status\n' "$SERVICE_NAME"
fi
}
status_agent() {
init_system=$(detect_init_system)
if [ "$init_system" = "systemd" ]; then
if systemctl status "$SERVICE_NAME" --no-pager; then return 0; else return $?; fi
fi
if [ "$init_system" = "openrc" ]; then
rc-service "$SERVICE_NAME" status
return $?
fi
echo "未检测到 NodeFlare Agent 服务" >&2
return 1
}
uninstall_agent() {
[ "$(id -u)" -eq 0 ] || fail "请使用 root 权限执行卸载"
log "正在停止并移除 NodeFlare Agent"
init_system=$(detect_init_system)
case "$init_system" in
systemd) systemctl disable --now "$SERVICE_NAME" 2>/dev/null || true ;;
openrc)
rc-service "$SERVICE_NAME" stop 2>/dev/null || true
rc-update del "$SERVICE_NAME" default 2>/dev/null || true
;;
esac
rm -f "$SERVICE_FILE"
rm -f "$OPENRC_FILE"
[ "$init_system" != "systemd" ] || systemctl daemon-reload 2>/dev/null || true
rm -f "$AGENT_FILE"
rm -rf "$STATE_DIR"
rmdir "$STATE_ROOT" 2>/dev/null || true
rmdir "$INSTALL_DIR" 2>/dev/null || true
echo "NodeFlare Agent 已卸载"
}
case "${1:-}" in
--uninstall) [ "$#" -eq 1 ] || { usage; exit 1; }; uninstall_agent ;;
--status) [ "$#" -eq 1 ] || { usage; exit 1; }; status_agent ;;
-h|--help|'') usage; exit 0 ;;
*) install_agent "$@" ;;
esac