Restore: перевод бэкапов на восстановленный сервер
Раньше восстановленная копия получала конфиг и таймер агента со старым токеном (через категории etc и units), и бэкапы шли с двух серверов одновременно. - конфиг и юниты агента больше не бэкапятся - в конце restore спрашивает, сделать ли сервер основным: сервер бэкапов выпускает новый токен (POST /api/v1/restore/takeover), restore ставит агента; токен старого сервера отозван — его агент получает отказ - --takeover / --no-takeover; без них — вопрос (с --yes — да) - уведомление «Сервер восстановлен» в ntfy/Telegram Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
488abc2d34
commit
97c7051021
12 files changed
+195
-4
No files matched your search
@@ -440,3 +440,22 @@ func (s *Server) servePart(w http.ResponseWriter, r *http.Request, b *Backup, na
|
||||
filepath.Base(filepath.Dir(b.Dir)), b.ID, name))
|
||||
http.ServeContent(w, r, "", fi.ModTime(), f)
|
||||
}
|
||||
|
||||
// apiTakeover делает восстановленный сервер основным: выпускает клиенту новый
|
||||
// агентский токен. Старый токен перестаёт работать, поэтому агент старого сервера,
|
||||
// если тот ещё жив, получает отказ и больше не шлёт бэкапы.
|
||||
func (s *Server) apiTakeover(w http.ResponseWriter, r *http.Request) {
|
||||
c := clientFrom(r)
|
||||
tok := newToken()
|
||||
if err := s.st.SetClientToken(c.ID, hashToken(tok)); err != nil {
|
||||
jsonErr(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
log.Printf("%s: бэкапы переведены на восстановленный сервер, старый токен отозван", c.Domain)
|
||||
s.notify(notification{
|
||||
Title: "Сервер восстановлен: " + c.Domain,
|
||||
Msg: "Бэкапы теперь присылает восстановленный сервер, токен старого отозван.",
|
||||
Tags: "white_check_mark", Link: s.clientLink(c.ID),
|
||||
})
|
||||
jsonResp(w, http.StatusOK, map[string]string{"token": tok})
|
||||
}
|
||||
@@ -86,6 +86,7 @@ func (s *Server) Handler() http.Handler {
|
||||
// API восстановления
|
||||
mux.HandleFunc("GET /api/v1/restore/manifest", s.restoreAuth(s.apiRestoreManifest))
|
||||
mux.HandleFunc("GET /api/v1/restore/{id}/parts/{name}", s.restoreAuth(s.apiRestorePart))
|
||||
mux.HandleFunc("POST /api/v1/restore/takeover", s.restoreAuth(s.apiTakeover))
|
||||
|
||||
// публичное
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok\n")) })
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.hsrv.site/mrleo1nid/backup-master/internal/catalog"
|
||||
)
|
||||
@@ -110,3 +111,34 @@ func TestStoreMigration(t *testing.T) {
|
||||
st2.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func TestTakeoverRevokesOldAgent(t *testing.T) {
|
||||
s, ts := newTestServer(t)
|
||||
oldTok := newToken()
|
||||
id, err := s.st.CreateClient("x", "x.example.com", hashToken(oldTok))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r := call(t, "GET", ts.URL+"/api/v1/agent/config", oldTok, nil); r.StatusCode != 200 {
|
||||
t.Fatalf("старый токен до takeover: %d", r.StatusCode)
|
||||
}
|
||||
// агентский токен takeover не делает
|
||||
if r := call(t, "POST", ts.URL+"/api/v1/restore/takeover", oldTok, nil); r.StatusCode != 401 {
|
||||
t.Fatalf("takeover агентским токеном: %d", r.StatusCode)
|
||||
}
|
||||
rt := newToken()
|
||||
if err := s.st.CreateRestoreToken(hashToken(rt), id, time.Hour); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var out struct{ Token string }
|
||||
decode(t, call(t, "POST", ts.URL+"/api/v1/restore/takeover", rt, nil), &out)
|
||||
if out.Token == "" || out.Token == oldTok {
|
||||
t.Fatalf("новый токен: %q", out.Token)
|
||||
}
|
||||
if r := call(t, "GET", ts.URL+"/api/v1/agent/config", oldTok, nil); r.StatusCode != 401 {
|
||||
t.Fatalf("старый сервер после takeover всё ещё принят: %d", r.StatusCode)
|
||||
}
|
||||
if r := call(t, "GET", ts.URL+"/api/v1/agent/config", out.Token, nil); r.StatusCode != 200 {
|
||||
t.Fatalf("новый токен: %d", r.StatusCode)
|
||||
}
|
||||
}
|
||||
@@ -291,7 +291,7 @@ func (s *Server) webClientRestoreToken(w http.ResponseWriter, r *http.Request) {
|
||||
s.render(w, r, "token", map[string]any{
|
||||
"Client": c,
|
||||
"Title": "Восстановление " + c.Domain,
|
||||
"Note": "Токен действует 24 часа и даёт только чтение бэкапов этого клиента. Выполните на новом сервере (Ubuntu 24.04, от root или пользователя с sudo):",
|
||||
"Note": "Токен действует 24 часа и даёт только чтение бэкапов этого клиента. Выполните на новом сервере (Ubuntu 24.04, от root или пользователя с sudo). В конце restore спросит, сделать ли новый сервер основным: тогда токен старого отзывается и бэкапы шлёт только новый. Для пробного восстановления добавьте --no-takeover.",
|
||||
"Cmd": cmd,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user