package agent import ( "bufio" "os" "path/filepath" "slices" "strings" "gitea.hsrv.site/mrleo1nid/backup-master/internal/archive" "gitea.hsrv.site/mrleo1nid/backup-master/internal/catalog" "gitea.hsrv.site/mrleo1nid/backup-master/internal/manifest" "gitea.hsrv.site/mrleo1nid/backup-master/internal/sysx" ) // collectUnits собирает свои systemd-юниты (не из пакетов и не описанные другими // категориями) вместе с состоянием enabled/active, а также drop-in каталоги *.d — // переопределения пакетных юнитов (например, docker.service.d/override.conf). func (c *collector) collectUnits(s catalog.Spec) *manifest.Category { mc := &manifest.Category{Name: s.Name, Type: catalog.TypeUnits, PostRestore: s.PostRestore, RestoreAfter: s.RestoreAfter} covered := c.covered() services := map[string]bool{} for _, cat := range c.plan.Manifest.Categories { if cat.Service != nil { services[cat.Service.Unit] = true } } skip := archive.NewMatcher(c.cat.Exclude, s.Exclude) for _, f := range c.unowned(s.Unowned) { unit := filepath.Base(f) name := strings.TrimSuffix(strings.TrimSuffix(unit, ".service"), ".timer") // юниты самого агента не бэкапим: на новый сервер его ставит restore с новым токеном, // иначе восстановленная копия слала бы бэкапы со старым токеном параллельно старому серверу if strings.HasPrefix(unit, "backup-agent.") || archive.Covers(covered, filepath.ToSlash(f)) || services[name] || services[unit] { continue } mc.Paths = append(mc.Paths, f) c.add(manifest.MainPart, filesItem(s.Name, f, skip)) st := manifest.ServiceState{Unit: unit} if sysx.UnitLoaded(c.ctx, unit) { st.Enabled, st.Active = sysx.UnitState(c.ctx, unit) } mc.Units = append(mc.Units, st) c.checkExec(unit, f, covered) } for _, d := range expandPaths(s.Paths) { if fi, err := os.Stat(d); err != nil || !fi.IsDir() || archive.Covers(covered, filepath.ToSlash(d)) { continue } mc.Paths = append(mc.Paths, d) c.add(manifest.MainPart, filesItem(s.Name, d, skip)) } if len(mc.Paths) == 0 { return nil } return mc } // checkExec предупреждает, если юнит запускает программу, которая не попадает в бэкап // и не ставится пакетом: после восстановления такой сервис не стартует. func (c *collector) checkExec(unit, file string, covered []string) { var missing []string for _, p := range unitExecPaths(file) { if archive.Covers(covered, p) || archive.Covers(systemBinDirs, p) || slices.Contains(missing, p) { continue } if !sysx.Exists(p) { continue } if sysx.DpkgOwned(c.ctx, []string{p})[p] { continue } missing = append(missing, p) } for _, p := range missing { c.warn("юнит %s запускает %s, но он не попадает в бэкап — добавьте путь в категорию", unit, p) } } // unitExecPaths извлекает пути программ из Exec*= строк unit-файла. func unitExecPaths(file string) []string { f, err := os.Open(file) if err != nil { return nil } defer f.Close() var out []string sc := bufio.NewScanner(f) for sc.Scan() { k, v, ok := strings.Cut(strings.TrimSpace(sc.Text()), "=") if !ok || !strings.HasPrefix(k, "Exec") { continue } // префиксы systemd: -, @, :, +, ! v = strings.TrimLeft(strings.TrimSpace(v), "-@:+!") if fields := strings.Fields(v); len(fields) > 0 && strings.HasPrefix(fields[0], "/") { out = append(out, fields[0]) } } return out } // checkCoverage ищет типичные места, где лежит что-то своё, не попавшее в бэкап: // каталоги в /opt, программы в /usr/local/bin и sbin, контейнеры без compose. func (c *collector) checkCoverage() { covered := c.covered() ignore := []string{"/opt/containerd", "/usr/local/bin/backupctl"} for _, s := range c.cat.Categories { if s.Disabled && s.Dir != "" { ignore = append(ignore, filepath.ToSlash(s.Dir)) // отключено сознательно } } check := func(pattern, what string) { matches, _ := filepath.Glob(pattern) for _, p := range matches { ps := filepath.ToSlash(p) if archive.Covers(covered, ps) || slices.Contains(ignore, ps) || c.global.Match(ps) { continue } c.warn("%s %s не попадает в бэкап — добавьте его в категорию или в глобальный exclude, если он не нужен", what, p) } } check("/opt/*", "каталог") check("/usr/local/bin/*", "программа") check("/usr/local/sbin/*", "программа") if sysx.HasCommand("docker") { out, err := sysx.Output(c.ctx, "docker", "ps", "--format", `{{.Names}} {{.Label "com.docker.compose.project"}}`) if err == nil { for _, line := range strings.Split(out, "\n") { name, project, _ := strings.Cut(line, "\t") if name != "" && project == "" { c.warn("контейнер %s запущен без docker compose (docker run) — его настройки и данные не бэкапятся", name) } } } } }