bbed5af662
Part 2 of the USB/backup spec. agentapi: StatusResponse.Backup record, DueResponse
age_seconds, RestoreTestStatus(). New "Rendszermentés (teljes mentés)" section
(read-only: last backup/target PBS-vs-local/next-due/restore-test) + "Mentés most"
manual trigger that goes through the quiesce loop (controller owns quiescing):
quiesce.Loop gains mutex + TriggerNow() (single-flight, async). New
/api/guest-backup/{trigger,status} (distinct from apiRouter's /api/backup/*).
App-data rows relabeled under an "Alkalmazás-mentések" divider. Config → slice 10.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
164 lines
6.0 KiB
Go
164 lines
6.0 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/quiesce"
|
|
)
|
|
|
|
// Whole-guest backup visibility + manual trigger (spec Part 2). The agent owns whole-guest
|
|
// vzdump/PBS backup; the controller is a read-only window onto it (GET /backup/{status,due},
|
|
// /restore-test/status) plus a "Mentés most" trigger that goes through the quiesce loop (the
|
|
// CONTROLLER owns quiescing — stop stacks → POST /backup → resume — so the captured state is
|
|
// app-consistent, not the agent's crash-consistent default). Cadence/retention CONFIG is NOT here
|
|
// (hub-served policy, slice 10).
|
|
|
|
// guestBackupView is the template payload for the "Rendszermentés" section. Times are time.Time so
|
|
// the existing fmtTime/timeAgo funcmap helpers format them; size is int64 for fmtBytes.
|
|
type guestBackupView struct {
|
|
Available bool // agent reachable + a status read succeeded
|
|
Note string // shown when not Available (unprovisioned / unreachable)
|
|
|
|
Phase string // idle | running | snapshotted | done | failed
|
|
Running bool // a backup job is in progress now
|
|
|
|
HasBackup bool
|
|
Success bool
|
|
StartedAt time.Time
|
|
SizeBytes int64
|
|
Target string // human label: "Biztonsági szerver (PBS)" / "Helyi tároló (local)"
|
|
Archive string
|
|
Mode string // snapshot | stop
|
|
StopMode bool // mode == stop → full app downtime during the backup (warn)
|
|
|
|
Due bool
|
|
DueReason string
|
|
AgeHours int64 // age of the newest successful backup, hours (for "X órája")
|
|
|
|
HasRestoreTest bool
|
|
RestorePass bool
|
|
RestoreVerified string
|
|
RestoreTestedAt time.Time
|
|
|
|
CanTrigger bool // a backup trigger (quiesce loop) is wired
|
|
}
|
|
|
|
// loadGuestBackup fetches the agent's whole-guest backup view (best-effort). Returns a view with
|
|
// Available=false (+ a note) when the agent isn't configured/reachable — the page still renders.
|
|
func (s *Server) loadGuestBackup(ctx context.Context) *guestBackupView {
|
|
v := &guestBackupView{CanTrigger: s.backupTrigger != nil}
|
|
client, err := s.agentClient()
|
|
if err != nil {
|
|
v.Note = "A host-ügynök nincs konfigurálva ezen a gépen."
|
|
return v
|
|
}
|
|
st, err := client.BackupStatus(ctx)
|
|
if err != nil {
|
|
v.Note = "A host-ügynök jelenleg nem elérhető."
|
|
return v
|
|
}
|
|
v.Available = true
|
|
v.Phase = st.Phase
|
|
v.Running = st.Phase == agentapi.PhaseRunning || st.Phase == "snapshotted"
|
|
if st.Backup != nil {
|
|
v.HasBackup = true
|
|
v.Success = st.Backup.Success
|
|
v.SizeBytes = st.Backup.SizeBytes
|
|
v.Archive = st.Backup.Archive
|
|
v.Mode = st.Backup.Mode
|
|
v.StopMode = st.Backup.Mode == "stop"
|
|
v.Target = backupTargetLabel(st.Backup)
|
|
if t, perr := time.Parse(time.RFC3339, st.Backup.StartedAt); perr == nil {
|
|
v.StartedAt = t
|
|
}
|
|
}
|
|
// Due window (best-effort; a failure just leaves the due fields zero).
|
|
if due, derr := client.BackupDue(ctx); derr == nil {
|
|
v.Due = due.Due
|
|
v.DueReason = due.Reason
|
|
if due.AgeSecs != nil {
|
|
v.AgeHours = *due.AgeSecs / 3600
|
|
}
|
|
}
|
|
// Restore-test (the "verified restorable" trust signal; nil until one runs).
|
|
if rt, rerr := client.RestoreTestStatus(ctx); rerr == nil && rt != nil {
|
|
v.HasRestoreTest = true
|
|
v.RestorePass = rt.Pass
|
|
v.RestoreVerified = rt.Verified
|
|
if t, perr := time.Parse(time.RFC3339, rt.TestedAt); perr == nil {
|
|
v.RestoreTestedAt = t
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
|
|
// backupTargetLabel maps the agent's backup target to a customer-facing Hungarian label, surfacing
|
|
// whether the backup landed on the PBS offsite tier or local host storage (from the archive volid /
|
|
// target id — "felhom-pbs"/"pbs:" ⇒ PBS, else local host storage).
|
|
func backupTargetLabel(b *agentapi.BackupRecord) string {
|
|
id := strings.ToLower(b.TargetID)
|
|
if strings.Contains(id, "pbs") || strings.HasPrefix(strings.ToLower(b.Archive), "felhom-pbs") || strings.Contains(strings.ToLower(b.Archive), "pbs:") {
|
|
return "Biztonsági szerver (PBS)"
|
|
}
|
|
if b.TargetID != "" {
|
|
return "Helyi tároló (" + b.TargetID + ")"
|
|
}
|
|
return "Helyi tároló"
|
|
}
|
|
|
|
// ServeBackupAPI dispatches /api/guest-backup/* (whole-guest manual trigger + status poll). A
|
|
// distinct prefix from apiRouter's app-data /api/backup/{run,status}. Wired behind RequireAuth +
|
|
// CsrfProtect in main.go.
|
|
func (s *Server) ServeBackupAPI(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.URL.Path == "/api/guest-backup/trigger" && r.Method == http.MethodPost:
|
|
s.handleBackupTriggerAPI(w, r)
|
|
case r.URL.Path == "/api/guest-backup/status" && r.Method == http.MethodGet:
|
|
s.handleBackupStatusAPI(w, r)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}
|
|
|
|
// handleBackupTriggerAPI starts an app-consistent whole-guest backup NOW via the quiesce loop. It
|
|
// returns immediately (the backup runs async, minutes); the page polls /api/backup/status.
|
|
func (s *Server) handleBackupTriggerAPI(w http.ResponseWriter, r *http.Request) {
|
|
if s.backupTrigger == nil {
|
|
writeDiskJSON(w, http.StatusServiceUnavailable, false, "a rendszermentés nem érhető el ezen a gépen", nil)
|
|
return
|
|
}
|
|
if err := s.backupTrigger.TriggerNow(); err != nil {
|
|
if errors.Is(err, quiesce.ErrBackupInProgress) {
|
|
writeDiskJSON(w, http.StatusConflict, false, "mentés már folyamatban van", nil)
|
|
return
|
|
}
|
|
s.logger.Printf("[ERROR] [web] backup trigger failed: %v", err)
|
|
writeDiskJSON(w, http.StatusBadGateway, false, err.Error(), nil)
|
|
return
|
|
}
|
|
s.logger.Printf("[INFO] [web] manual whole-guest backup triggered (quiesce loop)")
|
|
writeDiskJSON(w, http.StatusOK, true, "", map[string]any{"started": true})
|
|
}
|
|
|
|
// handleBackupStatusAPI proxies the agent's GET /backup/status for the page's progress poll.
|
|
func (s *Server) handleBackupStatusAPI(w http.ResponseWriter, r *http.Request) {
|
|
client, err := s.agentClient()
|
|
if err != nil {
|
|
writeDiskJSON(w, http.StatusServiceUnavailable, false, err.Error(), nil)
|
|
return
|
|
}
|
|
st, err := client.BackupStatus(r.Context())
|
|
if err != nil {
|
|
writeDiskJSON(w, http.StatusBadGateway, false, err.Error(), nil)
|
|
return
|
|
}
|
|
writeDiskJSON(w, http.StatusOK, true, "", map[string]any{
|
|
"phase": st.Phase, "job_id": st.JobID, "error": st.Error, "backup": st.Backup,
|
|
})
|
|
}
|