mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-05-11 18:10:23 +08:00
基础修复: - 新增节点离线检测:每 15s 扫描,超 45s 未心跳的远程节点自动置离线 - 节点删除前检查关联任务,避免孤立备份任务 - BackupTaskRepository 新增 CountByNodeID/ListByNodeID Master 端 Agent 协议: - 新增 AgentCommand 模型与命令队列仓储(pending/dispatched/succeeded/failed/timeout) - 新增 AgentService:任务下发、命令轮询、结果回收、超时扫描 - 新增专用 Agent HTTP API(X-Agent-Token 认证): /api/agent/heartbeat /api/agent/commands/poll /api/agent/commands/:id/result /api/agent/tasks/:id /api/agent/records/:id - BackupExecutionService 支持 node 路由:task.NodeID 指向远程节点时自动入队派发 Agent CLI(backupx agent 子命令): - 配置:YAML 文件 / 环境变量 / CLI 参数,优先级 CLI > 文件 > 环境 - 心跳循环 + 命令轮询循环 + 优雅退出 - 本地复用 BackupRunner 与 storage registry 执行备份并直接上传 - 支持 run_task 和 list_dir 两种命令 远程目录浏览: - NodeService 支持通过 Agent RPC 列出远程节点目录(15s 超时) 前端: - NodesPage 添加节点后展示 Agent 启动命令和环境变量配置 文档: - README 中英文重写"多节点集群"章节,含架构图、步骤、限制、CLI 参考
102 lines
2.5 KiB
Go
102 lines
2.5 KiB
Go
package agent
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestLoadConfigFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "agent.yaml")
|
|
content := `master: http://master.example.com:8340/
|
|
token: abc123
|
|
heartbeatInterval: 20s
|
|
pollInterval: 3s
|
|
tempDir: /var/backupx-agent
|
|
insecureSkipTlsVerify: true
|
|
`
|
|
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg, err := LoadConfigFile(path)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.Master != "http://master.example.com:8340" {
|
|
t.Errorf("trailing slash should be trimmed: %q", cfg.Master)
|
|
}
|
|
if cfg.Token != "abc123" {
|
|
t.Errorf("token: %q", cfg.Token)
|
|
}
|
|
if cfg.HeartbeatInterval != "20s" || cfg.PollInterval != "3s" {
|
|
t.Errorf("intervals: %+v", cfg)
|
|
}
|
|
if !cfg.InsecureSkipTLSVerify {
|
|
t.Errorf("insecure should be true")
|
|
}
|
|
}
|
|
|
|
func TestLoadConfigDefaults(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "agent.yaml")
|
|
if err := os.WriteFile(path, []byte("master: http://m\ntoken: t\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg, err := LoadConfigFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.HeartbeatInterval != "15s" || cfg.PollInterval != "5s" {
|
|
t.Errorf("default intervals not applied: %+v", cfg)
|
|
}
|
|
if cfg.TempDir != "/tmp/backupx-agent" {
|
|
t.Errorf("default tempdir: %q", cfg.TempDir)
|
|
}
|
|
}
|
|
|
|
func TestConfigValidate(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
cfg Config
|
|
wantErr bool
|
|
}{
|
|
{"valid", Config{Master: "http://m", Token: "t"}, false},
|
|
{"missing master", Config{Token: "t"}, true},
|
|
{"missing token", Config{Master: "http://m"}, true},
|
|
}
|
|
for _, c := range cases {
|
|
err := c.cfg.Validate()
|
|
if (err != nil) != c.wantErr {
|
|
t.Errorf("%s: err=%v wantErr=%v", c.name, err, c.wantErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMergeWithFlags(t *testing.T) {
|
|
cfg := &Config{Master: "http://old", Token: "old"}
|
|
cfg.MergeWithFlags("http://new", "", "/tmp/x")
|
|
if cfg.Master != "http://new" {
|
|
t.Errorf("master not overridden: %q", cfg.Master)
|
|
}
|
|
if cfg.Token != "old" {
|
|
t.Errorf("empty flag should not override: %q", cfg.Token)
|
|
}
|
|
if cfg.TempDir != "/tmp/x" {
|
|
t.Errorf("tempDir: %q", cfg.TempDir)
|
|
}
|
|
}
|
|
|
|
func TestLoadConfigFromEnv(t *testing.T) {
|
|
t.Setenv("BACKUPX_AGENT_MASTER", "http://env-master")
|
|
t.Setenv("BACKUPX_AGENT_TOKEN", "env-token")
|
|
t.Setenv("BACKUPX_AGENT_INSECURE_TLS", "true")
|
|
cfg, err := LoadConfigFromEnv()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.Master != "http://env-master" || cfg.Token != "env-token" || !cfg.InsecureSkipTLSVerify {
|
|
t.Errorf("env not picked up: %+v", cfg)
|
|
}
|
|
}
|