Агент: - 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>
161 lines
5.7 KiB
Go
161 lines
5.7 KiB
Go
// Package catalog описывает категории бэкапа. Встроенный каталог (default.yaml)
|
|
// покрывает типовой набор софта, конфиг клиента с сервера переопределяет его по имени.
|
|
package catalog
|
|
|
|
import (
|
|
_ "embed"
|
|
"fmt"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
//go:embed default.yaml
|
|
var defaultYAML []byte
|
|
|
|
const (
|
|
TypeFiles = "files"
|
|
TypeSystemd = "systemd"
|
|
TypeXUI = "xui"
|
|
TypeCompose = "compose"
|
|
// TypeUnits — свои systemd-юниты (не из пакетов) с состоянием enabled/active.
|
|
TypeUnits = "units"
|
|
)
|
|
|
|
type Config struct {
|
|
// AutoCompose — бэкапить compose-проекты, которых нет в каталоге.
|
|
AutoCompose *bool `yaml:"auto_compose,omitempty"`
|
|
Exclude []string `yaml:"exclude,omitempty"` // глобальные исключения
|
|
Categories []Spec `yaml:"categories"`
|
|
}
|
|
|
|
type Spec struct {
|
|
Name string `yaml:"name"`
|
|
Type string `yaml:"type"`
|
|
Disabled bool `yaml:"disabled,omitempty"`
|
|
Detect []string `yaml:"detect,omitempty"` // категория активна, если существует хоть один путь
|
|
Paths []string `yaml:"paths,omitempty"`
|
|
Exclude []string `yaml:"exclude,omitempty"`
|
|
// Unowned — маски файлов, которые берутся, только если они не принадлежат
|
|
// ни одному пакету (dpkg -S): свои файлы в /etc/cron.d, /etc/systemd/system и т.п.
|
|
Unowned []string `yaml:"unowned,omitempty"`
|
|
// ModifiedConffiles — добавить конфиги пакетов, изменённые относительно поставки.
|
|
ModifiedConffiles bool `yaml:"modified_conffiles,omitempty"`
|
|
|
|
Service string `yaml:"service,omitempty"`
|
|
VersionCmd []string `yaml:"version_cmd,omitempty"`
|
|
VersionRegex string `yaml:"version_regex,omitempty"`
|
|
Install string `yaml:"install,omitempty"` // recipe: nebula, xui, sing-box-extended
|
|
KeepBinaries []string `yaml:"keep_binaries,omitempty"` // fallback-копии бинарников
|
|
// KeepBinariesIf — shell-условие: keep_binaries берутся, только если оно истинно
|
|
// (например, Caddy собран с плагинами и из apt его не вернуть).
|
|
KeepBinariesIf string `yaml:"keep_binaries_if,omitempty"`
|
|
EnsureDirs []string `yaml:"ensure_dirs,omitempty"`
|
|
PostRestore []string `yaml:"post_restore,omitempty"`
|
|
Dumps []CmdDump `yaml:"dumps,omitempty"`
|
|
|
|
// compose
|
|
Dir string `yaml:"dir,omitempty"`
|
|
Project string `yaml:"project,omitempty"`
|
|
SkipServices []string `yaml:"skip_services,omitempty"`
|
|
HeavyPaths []string `yaml:"heavy_paths,omitempty"` // относительно dir (или абсолютные)
|
|
|
|
RestoreAfter []string `yaml:"restore_after,omitempty"`
|
|
}
|
|
|
|
// CmdDump — произвольный дамп: stdout команды cmd сохраняется в файл name.
|
|
type CmdDump struct {
|
|
Name string `yaml:"name"`
|
|
Cmd string `yaml:"cmd"`
|
|
RestoreCmd string `yaml:"restore_cmd,omitempty"`
|
|
}
|
|
|
|
func (c *Config) AutoComposeEnabled() bool { return c.AutoCompose == nil || *c.AutoCompose }
|
|
|
|
// Default возвращает встроенный каталог.
|
|
func Default() (*Config, error) {
|
|
var c Config
|
|
if err := yaml.Unmarshal(defaultYAML, &c); err != nil {
|
|
return nil, fmt.Errorf("встроенный каталог: %w", err)
|
|
}
|
|
return &c, c.Validate()
|
|
}
|
|
|
|
// Parse разбирает YAML-переопределения клиента.
|
|
func Parse(data []byte) (*Config, error) {
|
|
var c Config
|
|
if err := yaml.Unmarshal(data, &c); err != nil {
|
|
return nil, err
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
// Merge накладывает override на base: категории с тем же именем заменяются целиком,
|
|
// новые добавляются в конец, глобальные исключения складываются.
|
|
func Merge(base, override *Config) *Config {
|
|
out := &Config{AutoCompose: base.AutoCompose}
|
|
out.Exclude = append(out.Exclude, base.Exclude...)
|
|
out.Categories = append(out.Categories, base.Categories...)
|
|
if override == nil {
|
|
return out
|
|
}
|
|
if override.AutoCompose != nil {
|
|
out.AutoCompose = override.AutoCompose
|
|
}
|
|
out.Exclude = append(out.Exclude, override.Exclude...)
|
|
for _, s := range override.Categories {
|
|
replaced := false
|
|
for i := range out.Categories {
|
|
if out.Categories[i].Name == s.Name {
|
|
// "name + disabled: true" — короткая форма отключения встроенной категории
|
|
if s.Disabled && s.Type == "" {
|
|
out.Categories[i].Disabled = true
|
|
} else {
|
|
out.Categories[i] = s
|
|
}
|
|
replaced = true
|
|
break
|
|
}
|
|
}
|
|
if !replaced {
|
|
out.Categories = append(out.Categories, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (c *Config) Validate() error {
|
|
seen := map[string]bool{}
|
|
for _, s := range c.Categories {
|
|
if s.Name == "" {
|
|
return fmt.Errorf("категория без имени")
|
|
}
|
|
if seen[s.Name] {
|
|
return fmt.Errorf("категория %q описана дважды", s.Name)
|
|
}
|
|
seen[s.Name] = true
|
|
if s.Disabled {
|
|
continue
|
|
}
|
|
switch s.Type {
|
|
case TypeFiles, TypeSystemd, TypeXUI, TypeUnits:
|
|
case TypeCompose:
|
|
if s.Dir == "" && s.Project == "" {
|
|
return fmt.Errorf("категория %q: для compose нужен dir или project", s.Name)
|
|
}
|
|
default:
|
|
return fmt.Errorf("категория %q: неизвестный тип %q", s.Name, s.Type)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Find возвращает категорию по имени.
|
|
func (c *Config) Find(name string) *Spec {
|
|
for i := range c.Categories {
|
|
if c.Categories[i].Name == name {
|
|
return &c.Categories[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|