Резервное копирование bj на SMB: настройки (хранилище, расписание, хранение), копия сейчас, список и проверка копий; восстановление из копии на шаге 1 мастера; таймер bj-backup, smbclient в установщике

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-10-05 14:55:38 +03:00
parent e31e35e663
commit 6aecac1cc8
15 changed files with 558 additions and 4 deletions
+179
View File
@@ -0,0 +1,179 @@
package lkgateway
// Резервное копирование сервера bj (05.10.2026): базы PostgreSQL bj и igate, данные bj и шлюза НРД — на
// сетевое хранилище (SMB) по расписанию; восстановление — в мастере настройки (переустановка сервера).
// Делает root: таймер bj-backup.timer → /usr/local/sbin/bj-backup; настройка, проверка, список и
// восстановление — /usr/local/sbin/bj-backup-admin через root-демон bj-keymedia. Пароль хранилища bj
// не хранит: он передаётся root-скрипту и лежит в /etc/bj/backup-smb.cred (только root).
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"time"
)
// BackupSettings — куда и как часто копировать (без пароля).
type BackupSettings struct {
Enabled bool `json:"enabled"`
Share string `json:"share,omitempty"` // //10.10.10.11/Backup
Dir string `json:"dir,omitempty"` // BJ_Backup/bj
User string `json:"user,omitempty"`
PasswordSet bool `json:"password_set,omitempty"`
EveryHours int `json:"every_hours,omitempty"`
KeepDays int `json:"keep_days,omitempty"`
SavedAt time.Time `json:"saved_at,omitempty"`
}
func (b BackupSettings) every() int {
switch b.EveryHours {
case 1, 2, 3, 4, 6, 8, 12, 24:
return b.EveryHours
}
return 6
}
func (b BackupSettings) keep() int {
if b.KeepDays >= 1 {
return b.KeepDays
}
return 14
}
func (b BackupSettings) dir() string { return pickStr(strings.TrimSpace(b.Dir), "BJ_Backup/bj") }
// BackupStatus — итог последней копии (пишет root-скрипт).
type BackupStatus struct {
At string `json:"at"`
OK bool `json:"ok"`
Dir string `json:"dir"`
Msg string `json:"msg"`
}
const (
backupStatusFile = "/var/lib/bj/backup-status.json"
restoreStatusFile = "/var/lib/bj/restore-status.json"
)
func backupStatus() *BackupStatus {
raw, err := os.ReadFile(backupStatusFile)
if err != nil {
return nil
}
var s BackupStatus
if json.Unmarshal(raw, &s) != nil {
return nil
}
return &s
}
// backupOp — команда root-скрипту bj-backup-admin (config|test|run|list|verify|restore) с параметрами.
func backupOp(ctx context.Context, op string, params map[string]string) (map[string]any, error) {
extra, _ := json.Marshal(params)
return runKeymediaOp(ctx, "backup-"+op, map[string]string{"extra": string(extra)})
}
// backupParams — хранилище из формы (share/dir/user/password); пустое — из настроек (root берёт свои).
func backupParams(r *http.Request) map[string]string {
p := map[string]string{}
for _, k := range []string{"share", "dir", "user", "password", "name"} {
if v := strings.TrimSpace(r.FormValue(k)); v != "" {
p[k] = v
}
}
return p
}
func jsonOut(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(v)
}
func opResult(res map[string]any, err error) map[string]any {
if err != nil {
return map[string]any{"ok": false, "error": err.Error()}
}
return res
}
func registerBackup(mux *http.ServeMux, rc *RuntimeConfig) {
// сохранить настройки (и передать root-скрипту: конфигурация, пароль, расписание таймера)
mux.HandleFunc("/admin/setup/backup", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
b := rc.Snapshot().Backup
b.Enabled = r.FormValue("enabled") != ""
b.Share = strings.TrimSpace(r.FormValue("share"))
b.Dir = strings.TrimSpace(r.FormValue("dir"))
b.User = strings.TrimSpace(r.FormValue("user"))
b.EveryHours, _ = strconv.Atoi(r.FormValue("every_hours"))
b.KeepDays, _ = strconv.Atoi(r.FormValue("keep_days"))
pass := r.FormValue("password")
if b.Share == "" {
setupFlash(w, r, "Резервное копирование: укажите хранилище, например //10.10.10.11/Backup")
return
}
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
defer cancel()
res, err := backupOp(ctx, "config", map[string]string{"share": b.Share, "dir": b.dir(), "user": b.User, "password": pass,
"every_hours": strconv.Itoa(b.every()), "keep_days": strconv.Itoa(b.keep()), "enabled": strconv.FormatBool(b.Enabled)})
if err != nil {
setupFlash(w, r, "Резервное копирование: не сохранено — "+err.Error())
return
}
if pass != "" {
b.PasswordSet = true
}
b.SavedAt = time.Now()
_ = rc.UpdateBackup(b)
setupFlash(w, r, "Резервное копирование: "+fmt.Sprint(res["output"]))
})
mux.HandleFunc("/admin/setup/backup/test", func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
defer cancel()
jsonOut(w, opResult(backupOp(ctx, "test", backupParams(r))))
})
mux.HandleFunc("/admin/setup/backup/run", func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
jsonOut(w, opResult(backupOp(ctx, "run", nil)))
})
mux.HandleFunc("/admin/setup/backup/list", func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
defer cancel()
jsonOut(w, opResult(backupOp(ctx, "list", backupParams(r))))
})
// проверка копии: скачать и сверить контрольные суммы (ничего не восстанавливает)
mux.HandleFunc("/admin/setup/backup/verify", func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
defer cancel()
jsonOut(w, opResult(backupOp(ctx, "verify", backupParams(r))))
})
mux.HandleFunc("/admin/setup/backup/status", func(w http.ResponseWriter, r *http.Request) {
out := map[string]any{"backup": backupStatus()}
if raw, err := os.ReadFile(restoreStatusFile); err == nil {
var rs map[string]any
if json.Unmarshal(raw, &rs) == nil {
out["restore"] = rs
}
}
jsonOut(w, out)
})
// восстановление: останавливает bj и шлюз НРД, поэтому выполняется в фоне; страница опрашивает status
mux.HandleFunc("/admin/setup/backup/restore", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
defer cancel()
jsonOut(w, opResult(backupOp(ctx, "restore", backupParams(r))))
})
}