From 451d4afa233e1bb9ab9f8531c47eaae4ac54a911 Mon Sep 17 00:00:00 2001 From: Awuqing <3184394176@qq.com> Date: Sun, 19 Apr 2026 16:19:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD:=20=E6=96=B0=E5=A2=9E=20inst?= =?UTF-8?q?allscript=20=E5=8C=85=EF=BC=8C=E6=B8=B2=E6=9F=93=20systemd/dock?= =?UTF-8?q?er/foreground=20=E5=AE=89=E8=A3=85=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/agent-compose.yml.tmpl | 13 ++ deploy/agent-install.sh.tmpl | 108 +++++++++++++++ server/internal/installscript/renderer.go | 79 +++++++++++ .../internal/installscript/renderer_test.go | 126 ++++++++++++++++++ .../templates/agent-compose.yml.tmpl | 13 ++ .../templates/agent-install.sh.tmpl | 108 +++++++++++++++ 6 files changed, 447 insertions(+) create mode 100644 deploy/agent-compose.yml.tmpl create mode 100644 deploy/agent-install.sh.tmpl create mode 100644 server/internal/installscript/renderer.go create mode 100644 server/internal/installscript/renderer_test.go create mode 100644 server/internal/installscript/templates/agent-compose.yml.tmpl create mode 100644 server/internal/installscript/templates/agent-install.sh.tmpl diff --git a/deploy/agent-compose.yml.tmpl b/deploy/agent-compose.yml.tmpl new file mode 100644 index 0000000..4a27919 --- /dev/null +++ b/deploy/agent-compose.yml.tmpl @@ -0,0 +1,13 @@ +# BackupX Agent docker-compose 片段 +# 生成于 {{.MasterURL}} · 节点 ID {{.NodeID}} +version: "3.8" +services: + backupx-agent: + image: awuqing/backupx:{{.AgentVersion}} + command: ["agent"] + restart: unless-stopped + environment: + BACKUPX_AGENT_MASTER: "{{.MasterURL}}" + BACKUPX_AGENT_TOKEN: "{{.AgentToken}}" + volumes: + - /var/lib/backupx-agent:/tmp/backupx-agent diff --git a/deploy/agent-install.sh.tmpl b/deploy/agent-install.sh.tmpl new file mode 100644 index 0000000..ed5f667 --- /dev/null +++ b/deploy/agent-install.sh.tmpl @@ -0,0 +1,108 @@ +#!/bin/sh +# BackupX Agent 一键安装脚本(由 Master 动态渲染) +# 模式: {{.Mode}} | 架构: {{.Arch}} | 版本: {{.AgentVersion}} +set -eu + +MASTER_URL="{{.MasterURL}}" +AGENT_TOKEN="{{.AgentToken}}" +AGENT_VERSION="{{.AgentVersion}}" +DOWNLOAD_BASE="{{.DownloadBase}}" +INSTALL_PREFIX="{{.InstallPrefix}}" +ARCH="{{.Arch}}" + +# 1. 前置检查 +[ "$(id -u)" -eq 0 ] || { echo "请使用 root 或 sudo 执行" >&2; exit 1; } +command -v curl >/dev/null || command -v wget >/dev/null \ + || { echo "需要 curl 或 wget" >&2; exit 1; } +{{if eq .Mode "systemd"}}command -v systemctl >/dev/null || { echo "不支持非 systemd 系统" >&2; exit 1; } +{{end}}{{if eq .Mode "docker"}}command -v docker >/dev/null || { echo "需要先安装 docker" >&2; exit 1; } +{{end}} +# 2. 架构检测 +if [ "$ARCH" = "auto" ]; then + case "$(uname -m)" in + x86_64|amd64) ARCH=amd64 ;; + aarch64|arm64) ARCH=arm64 ;; + *) echo "不支持的架构: $(uname -m)" >&2; exit 1 ;; + esac +fi + +{{if ne .Mode "docker"}} +# 3. 下载二进制(systemd / foreground 模式) +ARCHIVE="backupx-${AGENT_VERSION}-linux-${ARCH}.tar.gz" +URL="${DOWNLOAD_BASE}/${AGENT_VERSION}/${ARCHIVE}" +TMPDIR="$(mktemp -d)"; trap 'rm -rf "$TMPDIR"' EXIT +echo "[1/4] 下载 ${URL}" +if command -v curl >/dev/null; then + curl -fsSL "$URL" -o "$TMPDIR/pkg.tar.gz" +else + wget -qO "$TMPDIR/pkg.tar.gz" "$URL" +fi +tar xzf "$TMPDIR/pkg.tar.gz" -C "$TMPDIR" + +# 4. 安装二进制 + 用户 +echo "[2/4] 安装到 ${INSTALL_PREFIX}" +id backupx >/dev/null 2>&1 || useradd --system --home-dir "$INSTALL_PREFIX" --shell /usr/sbin/nologin backupx +install -d -o backupx -g backupx "$INSTALL_PREFIX" /var/lib/backupx-agent +install -m 0755 "$TMPDIR/backupx-${AGENT_VERSION}-linux-${ARCH}/backupx" "$INSTALL_PREFIX/backupx" +{{end}} + +{{if eq .Mode "systemd"}} +# 5. systemd unit +echo "[3/4] 配置 systemd" +cat > /etc/systemd/system/backupx-agent.service </dev/null \ + | grep -q '"status":"online"'; then + echo "✓ 节点已上线" + exit 0 + fi +done +echo "⚠ 30s 内未收到上线心跳,请检查防火墙或 journalctl -u backupx-agent" +exit 2 +{{end}} + +{{if eq .Mode "foreground"}} +# 5. 前台运行 +echo "[3/3] 前台启动 agent(Ctrl+C 退出)" +export BACKUPX_AGENT_MASTER="${MASTER_URL}" +export BACKUPX_AGENT_TOKEN="${AGENT_TOKEN}" +exec "${INSTALL_PREFIX}/backupx" agent --temp-dir /var/lib/backupx-agent +{{end}} + +{{if eq .Mode "docker"}} +# Docker 模式:直接用镜像启动容器 +echo "[1/2] 拉取镜像 awuqing/backupx:${AGENT_VERSION}" +docker pull "awuqing/backupx:${AGENT_VERSION}" +echo "[2/2] 启动容器 backupx-agent" +docker rm -f backupx-agent >/dev/null 2>&1 || true +docker run -d --name backupx-agent --restart=unless-stopped \ + -e "BACKUPX_AGENT_MASTER=${MASTER_URL}" \ + -e "BACKUPX_AGENT_TOKEN=${AGENT_TOKEN}" \ + -v /var/lib/backupx-agent:/tmp/backupx-agent \ + "awuqing/backupx:${AGENT_VERSION}" agent +echo "✓ 容器已启动" +{{end}} diff --git a/server/internal/installscript/renderer.go b/server/internal/installscript/renderer.go new file mode 100644 index 0000000..a88f984 --- /dev/null +++ b/server/internal/installscript/renderer.go @@ -0,0 +1,79 @@ +// Package installscript 负责把一次性安装令牌 + 节点配置渲染为可执行 shell 脚本或 docker-compose YAML。 +// +// 模板文件通过 go:embed 嵌入二进制,避免运行时依赖外部资源。 +package installscript + +import ( + "bytes" + _ "embed" + "fmt" + "text/template" + + "backupx/server/internal/model" +) + +//go:embed templates/agent-install.sh.tmpl +var installScriptTmpl string + +//go:embed templates/agent-compose.yml.tmpl +var composeYamlTmpl string + +// Context 是模板渲染输入。 +type Context struct { + MasterURL string + AgentToken string + AgentVersion string + Mode string // systemd|docker|foreground + Arch string // amd64|arm64|auto + DownloadBase string + InstallPrefix string + NodeID uint +} + +// DownloadBaseFor 将下载源枚举转换为具体 URL 前缀。 +func DownloadBaseFor(src string) string { + switch src { + case model.InstallSourceGhproxy: + return "https://ghproxy.com/https://github.com/Awuqing/BackupX/releases/download" + default: + return "https://github.com/Awuqing/BackupX/releases/download" + } +} + +// RenderScript 渲染目标机安装脚本。 +func RenderScript(ctx Context) (string, error) { + ctx = withDefaults(ctx) + tmpl, err := template.New("install").Parse(installScriptTmpl) + if err != nil { + return "", fmt.Errorf("parse template: %w", err) + } + var buf bytes.Buffer + if err := tmpl.Execute(&buf, ctx); err != nil { + return "", fmt.Errorf("execute template: %w", err) + } + return buf.String(), nil +} + +// RenderComposeYaml 渲染 docker-compose.yml 片段。 +func RenderComposeYaml(ctx Context) (string, error) { + ctx = withDefaults(ctx) + tmpl, err := template.New("compose").Parse(composeYamlTmpl) + if err != nil { + return "", fmt.Errorf("parse template: %w", err) + } + var buf bytes.Buffer + if err := tmpl.Execute(&buf, ctx); err != nil { + return "", fmt.Errorf("execute template: %w", err) + } + return buf.String(), nil +} + +func withDefaults(ctx Context) Context { + if ctx.InstallPrefix == "" { + ctx.InstallPrefix = "/opt/backupx-agent" + } + if ctx.DownloadBase == "" { + ctx.DownloadBase = DownloadBaseFor(model.InstallSourceGitHub) + } + return ctx +} diff --git a/server/internal/installscript/renderer_test.go b/server/internal/installscript/renderer_test.go new file mode 100644 index 0000000..b56e02f --- /dev/null +++ b/server/internal/installscript/renderer_test.go @@ -0,0 +1,126 @@ +package installscript + +import ( + "strings" + "testing" + + "backupx/server/internal/model" +) + +var testCtx = Context{ + MasterURL: "https://master.example.com", + AgentToken: "test-token-hex", + AgentVersion: "v1.7.0", + Mode: model.InstallModeSystemd, + Arch: model.InstallArchAuto, + DownloadBase: "https://github.com/Awuqing/BackupX/releases/download", + InstallPrefix: "/opt/backupx-agent", + NodeID: 42, +} + +func TestRenderScriptSystemd(t *testing.T) { + got, err := RenderScript(testCtx) + if err != nil { + t.Fatalf("render err: %v", err) + } + mustContain := []string{ + "BACKUPX_AGENT_MASTER=${MASTER_URL}", + `Environment="BACKUPX_AGENT_TOKEN=${AGENT_TOKEN}"`, + "systemctl daemon-reload", + "systemctl enable --now backupx-agent", + "X-Agent-Token: ${AGENT_TOKEN}", + "MASTER_URL=\"https://master.example.com\"", + "AGENT_TOKEN=\"test-token-hex\"", + } + for _, s := range mustContain { + if !strings.Contains(got, s) { + t.Errorf("systemd script missing %q", s) + } + } + mustNotContain := []string{"docker run", `exec "${INSTALL_PREFIX}/backupx" agent --temp-dir`} + for _, s := range mustNotContain { + if strings.Contains(got, s) { + t.Errorf("systemd script unexpectedly contains %q", s) + } + } +} + +func TestRenderScriptForeground(t *testing.T) { + ctx := testCtx + ctx.Mode = model.InstallModeForeground + got, err := RenderScript(ctx) + if err != nil { + t.Fatalf("render err: %v", err) + } + if !strings.Contains(got, `exec "${INSTALL_PREFIX}/backupx" agent`) { + t.Errorf("foreground script missing exec line:\n%s", got) + } + if strings.Contains(got, "systemctl daemon-reload") { + t.Errorf("foreground script should not reference systemctl:\n%s", got) + } + if strings.Contains(got, "docker run") { + t.Errorf("foreground script should not reference docker:\n%s", got) + } +} + +func TestRenderScriptDocker(t *testing.T) { + ctx := testCtx + ctx.Mode = model.InstallModeDocker + got, err := RenderScript(ctx) + if err != nil { + t.Fatalf("render err: %v", err) + } + if !strings.Contains(got, "docker run") { + t.Errorf("docker script missing `docker run`:\n%s", got) + } + if !strings.Contains(got, "awuqing/backupx:${AGENT_VERSION}") { + t.Errorf("docker script missing image tag reference:\n%s", got) + } + if strings.Contains(got, "systemctl daemon-reload") { + t.Errorf("docker script should not reference systemctl:\n%s", got) + } +} + +func TestRenderComposeYaml(t *testing.T) { + ctx := testCtx + ctx.Mode = model.InstallModeDocker + got, err := RenderComposeYaml(ctx) + if err != nil { + t.Fatalf("render err: %v", err) + } + if !strings.Contains(got, "image: awuqing/backupx:v1.7.0") { + t.Errorf("compose missing image:\n%s", got) + } + if !strings.Contains(got, `BACKUPX_AGENT_TOKEN: "test-token-hex"`) { + t.Errorf("compose missing token env:\n%s", got) + } +} + +func TestDownloadBaseMapping(t *testing.T) { + cases := map[string]string{ + model.InstallSourceGitHub: "https://github.com/Awuqing/BackupX/releases/download", + model.InstallSourceGhproxy: "https://ghproxy.com/https://github.com/Awuqing/BackupX/releases/download", + } + for src, want := range cases { + got := DownloadBaseFor(src) + if got != want { + t.Errorf("src=%s want=%s got=%s", src, want, got) + } + } +} + +func TestRenderScriptDefaultsApplied(t *testing.T) { + ctx := testCtx + ctx.InstallPrefix = "" // 应被默认为 /opt/backupx-agent + ctx.DownloadBase = "" // 应被默认为 github + got, err := RenderScript(ctx) + if err != nil { + t.Fatalf("render err: %v", err) + } + if !strings.Contains(got, "INSTALL_PREFIX=\"/opt/backupx-agent\"") { + t.Errorf("default InstallPrefix not applied:\n%s", got) + } + if !strings.Contains(got, "DOWNLOAD_BASE=\"https://github.com/Awuqing/BackupX/releases/download\"") { + t.Errorf("default DownloadBase not applied:\n%s", got) + } +} diff --git a/server/internal/installscript/templates/agent-compose.yml.tmpl b/server/internal/installscript/templates/agent-compose.yml.tmpl new file mode 100644 index 0000000..4a27919 --- /dev/null +++ b/server/internal/installscript/templates/agent-compose.yml.tmpl @@ -0,0 +1,13 @@ +# BackupX Agent docker-compose 片段 +# 生成于 {{.MasterURL}} · 节点 ID {{.NodeID}} +version: "3.8" +services: + backupx-agent: + image: awuqing/backupx:{{.AgentVersion}} + command: ["agent"] + restart: unless-stopped + environment: + BACKUPX_AGENT_MASTER: "{{.MasterURL}}" + BACKUPX_AGENT_TOKEN: "{{.AgentToken}}" + volumes: + - /var/lib/backupx-agent:/tmp/backupx-agent diff --git a/server/internal/installscript/templates/agent-install.sh.tmpl b/server/internal/installscript/templates/agent-install.sh.tmpl new file mode 100644 index 0000000..ed5f667 --- /dev/null +++ b/server/internal/installscript/templates/agent-install.sh.tmpl @@ -0,0 +1,108 @@ +#!/bin/sh +# BackupX Agent 一键安装脚本(由 Master 动态渲染) +# 模式: {{.Mode}} | 架构: {{.Arch}} | 版本: {{.AgentVersion}} +set -eu + +MASTER_URL="{{.MasterURL}}" +AGENT_TOKEN="{{.AgentToken}}" +AGENT_VERSION="{{.AgentVersion}}" +DOWNLOAD_BASE="{{.DownloadBase}}" +INSTALL_PREFIX="{{.InstallPrefix}}" +ARCH="{{.Arch}}" + +# 1. 前置检查 +[ "$(id -u)" -eq 0 ] || { echo "请使用 root 或 sudo 执行" >&2; exit 1; } +command -v curl >/dev/null || command -v wget >/dev/null \ + || { echo "需要 curl 或 wget" >&2; exit 1; } +{{if eq .Mode "systemd"}}command -v systemctl >/dev/null || { echo "不支持非 systemd 系统" >&2; exit 1; } +{{end}}{{if eq .Mode "docker"}}command -v docker >/dev/null || { echo "需要先安装 docker" >&2; exit 1; } +{{end}} +# 2. 架构检测 +if [ "$ARCH" = "auto" ]; then + case "$(uname -m)" in + x86_64|amd64) ARCH=amd64 ;; + aarch64|arm64) ARCH=arm64 ;; + *) echo "不支持的架构: $(uname -m)" >&2; exit 1 ;; + esac +fi + +{{if ne .Mode "docker"}} +# 3. 下载二进制(systemd / foreground 模式) +ARCHIVE="backupx-${AGENT_VERSION}-linux-${ARCH}.tar.gz" +URL="${DOWNLOAD_BASE}/${AGENT_VERSION}/${ARCHIVE}" +TMPDIR="$(mktemp -d)"; trap 'rm -rf "$TMPDIR"' EXIT +echo "[1/4] 下载 ${URL}" +if command -v curl >/dev/null; then + curl -fsSL "$URL" -o "$TMPDIR/pkg.tar.gz" +else + wget -qO "$TMPDIR/pkg.tar.gz" "$URL" +fi +tar xzf "$TMPDIR/pkg.tar.gz" -C "$TMPDIR" + +# 4. 安装二进制 + 用户 +echo "[2/4] 安装到 ${INSTALL_PREFIX}" +id backupx >/dev/null 2>&1 || useradd --system --home-dir "$INSTALL_PREFIX" --shell /usr/sbin/nologin backupx +install -d -o backupx -g backupx "$INSTALL_PREFIX" /var/lib/backupx-agent +install -m 0755 "$TMPDIR/backupx-${AGENT_VERSION}-linux-${ARCH}/backupx" "$INSTALL_PREFIX/backupx" +{{end}} + +{{if eq .Mode "systemd"}} +# 5. systemd unit +echo "[3/4] 配置 systemd" +cat > /etc/systemd/system/backupx-agent.service </dev/null \ + | grep -q '"status":"online"'; then + echo "✓ 节点已上线" + exit 0 + fi +done +echo "⚠ 30s 内未收到上线心跳,请检查防火墙或 journalctl -u backupx-agent" +exit 2 +{{end}} + +{{if eq .Mode "foreground"}} +# 5. 前台运行 +echo "[3/3] 前台启动 agent(Ctrl+C 退出)" +export BACKUPX_AGENT_MASTER="${MASTER_URL}" +export BACKUPX_AGENT_TOKEN="${AGENT_TOKEN}" +exec "${INSTALL_PREFIX}/backupx" agent --temp-dir /var/lib/backupx-agent +{{end}} + +{{if eq .Mode "docker"}} +# Docker 模式:直接用镜像启动容器 +echo "[1/2] 拉取镜像 awuqing/backupx:${AGENT_VERSION}" +docker pull "awuqing/backupx:${AGENT_VERSION}" +echo "[2/2] 启动容器 backupx-agent" +docker rm -f backupx-agent >/dev/null 2>&1 || true +docker run -d --name backupx-agent --restart=unless-stopped \ + -e "BACKUPX_AGENT_MASTER=${MASTER_URL}" \ + -e "BACKUPX_AGENT_TOKEN=${AGENT_TOKEN}" \ + -v /var/lib/backupx-agent:/tmp/backupx-agent \ + "awuqing/backupx:${AGENT_VERSION}" agent +echo "✓ 容器已启动" +{{end}}