backup-master: сервер бэкапов, агент и восстановление VPS
Один бинарник backupctl: - server: API для агентов и restore, веб-морда (один админ), SQLite, ретеншн GFS, heavy-части, уведомления ntfy, раздача agent.sh/restore.sh; - backup/plan: сбор по каталогу категорий (files/systemd/xui/compose), горячие дампы Postgres/Mongo/MySQL, снимок x-ui.db, docker-тома, digest образов, проверка crontab, потоковая отправка tar.zst; - restore: провижининг на основе vps_installer с теми же версиями пакетов, recipes 3x-ui/nebula/sing-box нужных версий, восстановление compose с заливкой дампов и закреплением образов по digest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
1d44d067e9
53 files changed
+7278
No files matched your search
@@ -0,0 +1,164 @@
|
||||
// Package sysx — обёртки над системными командами и файлами Linux.
|
||||
package sysx
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Output выполняет команду и возвращает stdout без хвостовых пробелов.
|
||||
func Output(ctx context.Context, name string, args ...string) (string, error) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
msg := strings.TrimSpace(stderr.String())
|
||||
if msg == "" {
|
||||
msg = strings.TrimSpace(stdout.String())
|
||||
}
|
||||
return "", fmt.Errorf("%s %s: %w: %s", name, strings.Join(args, " "), err, msg)
|
||||
}
|
||||
return strings.TrimSpace(stdout.String()), nil
|
||||
}
|
||||
|
||||
// ToFile выполняет команду, направляя stdout в файл path.
|
||||
func ToFile(ctx context.Context, path string, name string, args ...string) error {
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var stderr bytes.Buffer
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
cmd.Stdout = f
|
||||
cmd.Stderr = &stderr
|
||||
runErr := cmd.Run()
|
||||
closeErr := f.Close()
|
||||
if runErr != nil {
|
||||
return fmt.Errorf("%s: %w: %s", name, runErr, strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
return closeErr
|
||||
}
|
||||
|
||||
// FromFile выполняет команду, подавая файл path на stdin.
|
||||
func FromFile(ctx context.Context, path string, name string, args ...string) error {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return Stream(ctx, f, name, args...)
|
||||
}
|
||||
|
||||
// Stream выполняет команду с заданным stdin, вывод — в консоль.
|
||||
func Stream(ctx context.Context, stdin io.Reader, name string, args ...string) error {
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
cmd.Stdin = stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("%s %s: %w", name, strings.Join(args, " "), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run выполняет команду с выводом в консоль.
|
||||
func Run(ctx context.Context, name string, args ...string) error {
|
||||
return Stream(ctx, nil, name, args...)
|
||||
}
|
||||
|
||||
// Shell выполняет строку через bash -c с выводом в консоль.
|
||||
func Shell(ctx context.Context, script string, env ...string) error {
|
||||
cmd := exec.CommandContext(ctx, "bash", "-c", script)
|
||||
cmd.Env = append(os.Environ(), env...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("bash -c %q: %w", script, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Exists(p string) bool {
|
||||
_, err := os.Lstat(p)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func HasCommand(name string) bool {
|
||||
_, err := exec.LookPath(name)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// OSRelease читает /etc/os-release.
|
||||
func OSRelease() map[string]string {
|
||||
out := map[string]string{}
|
||||
f, err := os.Open("/etc/os-release")
|
||||
if err != nil {
|
||||
return out
|
||||
}
|
||||
defer f.Close()
|
||||
sc := bufio.NewScanner(f)
|
||||
for sc.Scan() {
|
||||
k, v, ok := strings.Cut(sc.Text(), "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out[k] = strings.Trim(v, `"'`)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// DpkgVersion возвращает установленную версию пакета или "".
|
||||
func DpkgVersion(ctx context.Context, pkg string) string {
|
||||
out, err := Output(ctx, "dpkg-query", "-W", "-f=${db:Status-Status} ${Version}", pkg)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
status, ver, _ := strings.Cut(out, " ")
|
||||
if status != "installed" {
|
||||
return ""
|
||||
}
|
||||
return ver
|
||||
}
|
||||
|
||||
func DpkgArch(ctx context.Context) string {
|
||||
out, err := Output(ctx, "dpkg", "--print-architecture")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// UnitState возвращает (enabled, active) для systemd-юнита.
|
||||
func UnitState(ctx context.Context, unit string) (bool, bool) {
|
||||
en, _ := Output(ctx, "systemctl", "is-enabled", unit)
|
||||
ac, _ := Output(ctx, "systemctl", "is-active", unit)
|
||||
return en == "enabled", ac == "active"
|
||||
}
|
||||
|
||||
// UnitLoaded сообщает, известен ли systemd такой юнит.
|
||||
func UnitLoaded(ctx context.Context, unit string) bool {
|
||||
out, err := Output(ctx, "systemctl", "show", "-p", "LoadState", "--value", unit)
|
||||
return err == nil && out == "loaded"
|
||||
}
|
||||
|
||||
// GroupMembers возвращает членов группы из /etc/group.
|
||||
func GroupMembers(group string) []string {
|
||||
data, err := os.ReadFile("/etc/group")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
f := strings.Split(line, ":")
|
||||
if len(f) == 4 && f[0] == group && f[3] != "" {
|
||||
return strings.Split(f[3], ",")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user