Полнота бэкапа: SSH, весь UFW, свои юниты, конфиги пакетов, системные параметры
Агент: - 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:
1 parent
cbd3cde01d
commit
1947b26913
14 files changed
+747
-45
No files matched your search
@@ -106,6 +106,7 @@ func Run(ctx context.Context, opts Options) error {
|
||||
return fmt.Errorf("подготовка сервера: %w", err)
|
||||
}
|
||||
}
|
||||
r.restoreSystem()
|
||||
if err := r.prepareVolumes(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -403,6 +404,13 @@ func (r *restorer) restoreCategory(c manifest.Category) error {
|
||||
if err := r.restoreCompose(c); err != nil {
|
||||
return err
|
||||
}
|
||||
case catalog.TypeUnits:
|
||||
_ = sysx.Run(r.ctx, "systemctl", "daemon-reload")
|
||||
for i := range c.Units {
|
||||
if err := r.applyService(&c.Units[i]); err != nil {
|
||||
r.warn("%s: %v", c.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.Name == "system" {
|
||||
r.fixCrontabs()
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package restore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"gitea.hsrv.site/mrleo1nid/backup-master/internal/sysx"
|
||||
)
|
||||
|
||||
// restoreSystem повторяет системные параметры старого сервера: часовой пояс
|
||||
// (от него зависит время cron-заданий), swap-файл и вручную поставленные пакеты.
|
||||
func (r *restorer) restoreSystem() {
|
||||
step("Системные параметры")
|
||||
r.restoreTimezone()
|
||||
r.restoreSwap()
|
||||
r.restorePackages()
|
||||
}
|
||||
|
||||
func (r *restorer) restoreTimezone() {
|
||||
tz := r.m.Timezone
|
||||
if tz == "" {
|
||||
return
|
||||
}
|
||||
if cur := sysx.Timezone(r.ctx); cur == tz {
|
||||
info("часовой пояс %s — как на старом сервере", tz)
|
||||
return
|
||||
}
|
||||
if err := sysx.Run(r.ctx, "timedatectl", "set-timezone", tz); err != nil {
|
||||
r.warn("часовой пояс %s не установлен: %v", tz, err)
|
||||
return
|
||||
}
|
||||
info("часовой пояс: %s", tz)
|
||||
}
|
||||
|
||||
func (r *restorer) restoreSwap() {
|
||||
if len(r.m.Swap) == 0 {
|
||||
return
|
||||
}
|
||||
if len(sysx.SwapFiles()) > 0 {
|
||||
info("swap уже есть — не трогаю")
|
||||
return
|
||||
}
|
||||
if out, _ := sysx.Output(r.ctx, "swapon", "--show", "--noheadings"); out != "" {
|
||||
info("swap уже есть (раздел) — не трогаю")
|
||||
return
|
||||
}
|
||||
for _, sw := range r.m.Swap {
|
||||
if sysx.Exists(sw.Path) {
|
||||
r.warn("swap: %s уже существует, но не подключён — пропускаю", sw.Path)
|
||||
continue
|
||||
}
|
||||
mb := sw.Size / (1 << 20)
|
||||
script := fmt.Sprintf(`set -e
|
||||
fallocate -l %dM %[2]q || dd if=/dev/zero of=%[2]q bs=1M count=%[1]d status=none
|
||||
chmod 600 %[2]q
|
||||
mkswap %[2]q >/dev/null
|
||||
swapon %[2]q
|
||||
grep -qF %[2]q /etc/fstab || echo %[3]q >> /etc/fstab`, mb, sw.Path, sw.Path+" none swap sw 0 0")
|
||||
if err := sysx.Shell(r.ctx, script); err != nil {
|
||||
r.warn("swap %s: %v", sw.Path, err)
|
||||
continue
|
||||
}
|
||||
info("swap %s, %d МБ", sw.Path, mb)
|
||||
}
|
||||
}
|
||||
|
||||
// Пакеты ядра конкретных версий и метапакеты облачных образов ставить не нужно.
|
||||
var skipPackageRe = regexp.MustCompile(`^linux-(image|headers|modules|tools|cloud-tools|hwe)-\d|^linux-(image|headers|modules)-extra`)
|
||||
|
||||
func (r *restorer) restorePackages() {
|
||||
if len(r.m.ManualPackages) == 0 {
|
||||
return
|
||||
}
|
||||
installed := map[string]bool{}
|
||||
if out, err := sysx.Output(r.ctx, "dpkg-query", "-W", "-f=${Package} ${db:Status-Status}\n"); err == nil {
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
if name, st, ok := strings.Cut(line, " "); ok && st == "installed" {
|
||||
installed[name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
available := map[string]bool{}
|
||||
if out, err := sysx.Output(r.ctx, "apt-cache", "pkgnames"); err == nil {
|
||||
for _, n := range strings.Fields(out) {
|
||||
available[n] = true
|
||||
}
|
||||
}
|
||||
var install, missing []string
|
||||
for _, p := range r.m.ManualPackages {
|
||||
name, _, _ := strings.Cut(p, ":") // pkg:amd64
|
||||
if installed[name] || skipPackageRe.MatchString(name) {
|
||||
continue
|
||||
}
|
||||
if available[name] {
|
||||
install = append(install, name)
|
||||
} else {
|
||||
missing = append(missing, name)
|
||||
}
|
||||
}
|
||||
if len(install) > 0 {
|
||||
info("доустанавливаю пакеты со старого сервера: %s", strings.Join(install, " "))
|
||||
_ = os.Setenv("DEBIAN_FRONTEND", "noninteractive")
|
||||
args := append([]string{"install", "-y", "-q", "-o", "Dpkg::Options::=--force-confdef", "-o", "Dpkg::Options::=--force-confold"}, install...)
|
||||
if err := sysx.Run(r.ctx, "apt-get", args...); err != nil {
|
||||
r.warn("часть пакетов не установилась: %v", err)
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
r.warn("пакетов нет в репозиториях этого сервера (ставились из своих репозиториев или .deb): %s", strings.Join(missing, " "))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user