Агенты подхватывают изменения с сервера: каталог и самообновление
CI / test (push) Successful in 1m29s
CI / test (push) Successful in 1m29s
- сервер отдаёт агенту итоговый каталог (встроенный каталог сервера + переопределения клиента); если каталог не подходит версии агента — встроенный каталог агента и предупреждение - самообновление перед бэкапом: версия сервера новее → скачивание с того же сервера (только /dl/...), проверка sha256 и запуска новой версии, подмена с откатом, перезапуск через exec; при ошибке бэкап идёт текущей версией - галочка «Автообновление» у клиента (по умолчанию включена), версия агента в списке и карточке, отставшие подсвечены; версия сервера в шапке - миграция существующей базы: колонки auto_update и agent_version - backupctl backup --no-update Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1947b26913
commit
488abc2d34
21 files changed
+674
-51
No files matched your search
@@ -0,0 +1,106 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.hsrv.site/mrleo1nid/backup-master/internal/api"
|
||||
)
|
||||
|
||||
// updatedEnv выставляется перезапущенному процессу: не обновляться повторно в том же запуске.
|
||||
const updatedEnv = "BM_UPDATED_FROM"
|
||||
|
||||
// selfUpdate скачивает новую версию агента с того же сервера, сверяет sha256,
|
||||
// подменяет бинарник (старый сохраняется для отката) и перезапускает процесс.
|
||||
// При успехе не возвращается.
|
||||
func selfUpdate(ctx context.Context, client *api.Client, u *api.AgentUpdate, current string) error {
|
||||
// только путь на своём сервере: чужой адрес от сервера не принимаем
|
||||
if !strings.HasPrefix(u.Path, "/dl/") || strings.Contains(u.Path, "..") || strings.Contains(u.Path, "://") {
|
||||
return fmt.Errorf("недопустимый путь обновления %q", u.Path)
|
||||
}
|
||||
if len(u.SHA256) != 64 {
|
||||
return errors.New("сервер не прислал sha256")
|
||||
}
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exe, err = filepath.EvalSymlinks(exe); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||
defer cancel()
|
||||
body, err := client.Download(dctx, u.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("скачивание: %w", err)
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
tmp := exe + ".new"
|
||||
if err := writeVerified(tmp, body, u.SHA256); err != nil {
|
||||
os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
// новый бинарник должен запускаться и сообщать ожидаемую версию
|
||||
if err := checkVersion(ctx, tmp, u.Version); err != nil {
|
||||
os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
|
||||
old := exe + ".old"
|
||||
if err := os.Rename(exe, old); err != nil {
|
||||
os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmp, exe); err != nil {
|
||||
_ = os.Rename(old, exe)
|
||||
return err
|
||||
}
|
||||
env := append(os.Environ(), updatedEnv+"="+current)
|
||||
err = reexec(exe, os.Args, env)
|
||||
// сюда попадаем, только если exec не удался — возвращаем старую версию
|
||||
_ = os.Rename(old, exe)
|
||||
return fmt.Errorf("перезапуск: %w", err)
|
||||
}
|
||||
|
||||
func writeVerified(path string, r io.Reader, want string) error {
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(io.MultiWriter(f, h), r); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if got := hex.EncodeToString(h.Sum(nil)); got != want {
|
||||
return fmt.Errorf("sha256 не совпал: %s, ожидался %s", got, want)
|
||||
}
|
||||
return os.Chmod(path, 0o755)
|
||||
}
|
||||
|
||||
func checkVersion(ctx context.Context, bin, want string) error {
|
||||
cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
out, err := exec.CommandContext(cctx, bin, "version").Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("новый бинарник не запускается: %w", err)
|
||||
}
|
||||
if !strings.Contains(string(out), want) {
|
||||
return fmt.Errorf("новый бинарник сообщает версию %q, ожидалась %s", strings.TrimSpace(string(out)), want)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user