Files
backup-master/internal/restore/takeover.go
T
mrleo1nidandClaude Opus 5.5 97c7051021
CI / test (push) Successful in 1m36s
Release / release (push) Successful in 1m33s
Restore: перевод бэкапов на восстановленный сервер
Раньше восстановленная копия получала конфиг и таймер агента со старым токеном
(через категории etc и units), и бэкапы шли с двух серверов одновременно.

- конфиг и юниты агента больше не бэкапятся
- в конце restore спрашивает, сделать ли сервер основным: сервер бэкапов
  выпускает новый токен (POST /api/v1/restore/takeover), restore ставит
  агента; токен старого сервера отозван — его агент получает отказ
- --takeover / --no-takeover; без них — вопрос (с --yes — да)
- уведомление «Сервер восстановлен» в ntfy/Telegram

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:29:34 +03:00

99 lines
3.7 KiB
Go

package restore
import (
"context"
"fmt"
"os"
"strings"
"gitea.hsrv.site/mrleo1nid/backup-master/internal/sysx"
)
const (
agentConfigPath = "/etc/backup-agent/agent.yaml"
agentService = `[Unit]
Description=backup-master agent
Wants=network-online.target
After=network-online.target docker.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backupctl backup
Nice=10
IOSchedulingClass=idle
TimeoutStartSec=6h
`
agentTimer = `[Unit]
Description=Ежедневный бэкап backup-master
[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=30m
Persistent=true
[Install]
WantedBy=timers.target
`
)
// Takeover — делать ли восстановленный сервер основным для бэкапов.
const (
TakeoverAsk = ""
TakeoverYes = "yes"
TakeoverNo = "no"
)
// takeover переводит бэкапы клиента на этот сервер: сервер бэкапов выпускает новый
// токен (старый отзывается — агент старого сервера получит отказ) и здесь ставится агент.
func (r *restorer) takeover() {
mode := r.opts.Takeover
if mode == TakeoverAsk {
mode = TakeoverNo
if r.opts.Yes || confirmDefaultYes("Сделать этот сервер основным для бэкапов? Агент старого сервера перестанет их присылать") {
mode = TakeoverYes
}
}
step("Бэкапы этого сервера")
if mode != TakeoverYes {
info("агент не установлен — бэкапы по-прежнему присылает старый сервер (пробное восстановление)")
r.todo = append(r.todo, "Чтобы этот сервер стал основным: в карточке клиента «Новый токен агента» и выполнить выданную команду здесь")
return
}
token, err := r.client.Takeover(r.ctx)
if err != nil {
r.warn("не удалось перевести бэкапы на этот сервер: %v — установите агента вручную (карточка клиента → «Новый токен агента»)", err)
return
}
if err := installAgent(r.ctx, r.opts.Server, token); err != nil {
r.warn("агент не установлен: %v — токен уже перевыпущен, установите агента вручную (карточка клиента → «Новый токен агента»)", err)
return
}
info("агент установлен, бэкапы теперь присылает этот сервер; токен старого сервера отозван")
r.todo = append(r.todo,
"Старый сервер больше не может слать бэкапы (токен отозван). При выводе из работы: sudo systemctl disable --now backup-agent.timer",
"Первый бэкап с нового сервера сразу: sudo systemctl start backup-agent")
}
func installAgent(ctx context.Context, server, token string) error {
if err := os.MkdirAll("/etc/backup-agent", 0o700); err != nil {
return err
}
cfg := fmt.Sprintf("server: %s\ntoken: %s\nspool_dir: /var/tmp/backup-agent\n", strings.TrimRight(server, "/"), token)
if err := os.WriteFile(agentConfigPath, []byte(cfg), 0o600); err != nil {
return err
}
for path, body := range map[string]string{
"/etc/systemd/system/backup-agent.service": agentService,
"/etc/systemd/system/backup-agent.timer": agentTimer,
} {
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
return err
}
}
if err := sysx.Run(ctx, "systemctl", "daemon-reload"); err != nil {
return err
}
return sysx.Run(ctx, "systemctl", "enable", "--now", "backup-agent.timer")
}