Полнота бэкапа: SSH, весь UFW, свои юниты, конфиги пакетов, системные параметры
CI / test (push) Successful in 1m27s
Release / release (push) Successful in 1m26s

Агент:
- ssh: sshd_config(.d), ключи хоста, authorized_keys root и пользователей
- system: весь /etc/ufw и /etc/default/ufw, /etc/sysctl.conf, /etc/hosts,
  свои файлы в /etc/cron.d (пакетные отсекаются через dpkg -S)
- certs: /etc/letsencrypt, /root/.acme.sh; xui: сертификаты по путям из базы
- docker: daemon.json и логины в registry; telegraf: /etc/default/telegraf
- caddy: бинарник бэкапится, если Caddy собран с плагинами
- units: свои systemd-юниты с состоянием enabled/active и drop-in *.service.d
- etc: изменённые конфиги любых пакетов (dpkg conffiles) и свои файлы в /etc,
  кроме привязанного к железу и облаку
- в манифесте: часовой пояс, swap, вручную поставленные пакеты; версия
  apt-пакета в строке категории
- предупреждения: программы юнитов вне бэкапа, каталоги /opt и /usr/local/bin
  без категории, контейнеры без compose

Restore:
- часовой пояс, swap-файл, доустановка вручную поставленных пакетов
- свои юниты включаются и запускаются как на старом сервере

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-17 15:19:34 +03:00
1 parent cbd3cde01d
commit 1947b26913
14 files changed
+747 -45

No files matched your search

+42
View File
@@ -51,3 +51,45 @@ func TestXUIGeoExcludes(t *testing.T) {
t.Fatalf("категория fail2ban: %+v", f2b)
}
}
func TestUnitExecPaths(t *testing.T) {
f := filepath.Join(t.TempDir(), "x.service")
unit := "[Service]\nExecStartPre=-/opt/app/prepare.sh --x\nExecStart=/usr/local/bin/app run\nExecReload=/bin/kill -HUP $MAINPID\nEnvironment=A=/tmp/x\n"
if err := os.WriteFile(f, []byte(unit), 0o644); err != nil {
t.Fatal(err)
}
got := unitExecPaths(f)
want := []string{"/opt/app/prepare.sh", "/usr/local/bin/app", "/bin/kill"}
if !slices.Equal(got, want) {
t.Fatalf("unitExecPaths = %v, want %v", got, want)
}
}
func TestEtcCategoryExcludes(t *testing.T) {
cat, err := catalog.Default()
if err != nil {
t.Fatal(err)
}
s := cat.Find("etc")
if s == nil || !s.ModifiedConffiles {
t.Fatal("нет категории etc")
}
skip := archive.NewMatcher(cat.Exclude, s.Exclude)
for p, want := range map[string]bool{
"/etc/netplan/50-cloud-init.yaml": true,
"/etc/apt/sources.list.d/ubuntu.sources": true,
"/etc/systemd/system/my.service": true,
"/etc/fstab": true,
"/etc/wireguard/wg0.conf": false,
"/etc/nginx/sites-available/site": false,
"/etc/security/limits.conf": false,
"/etc/systemd/journald.conf": false,
} {
if got := skip.Match(p); got != want {
t.Errorf("etc exclude(%s) = %v, want %v", p, got, want)
}
}
if cat.Find("units") == nil || cat.Find("ssh") == nil || cat.Find("docker") == nil {
t.Fatal("нет категорий units/ssh/docker")
}
}