Files
mesh-catalog/modules/nfs-server/cmd/nfs-server/backup_test.go
T

117 lines
5.0 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
"time"
)
// Follow-up to the review (point 4): the one-hour look below a share must not hide a backup repository
// that appears later. Where this machine's backup seat keeps its repository is read from the bus and every
// share is judged against it on every pass.
func (f *fake) backup(holder string, paths ...string) {
raw, _ := json.Marshal(BackupFromBus{Holder: holder, Paths: paths})
f.file(BackupFile, string(raw))
}
func TestAShareHoldingTheBackupSeatsRepositoryIsRefusedOnTheNextPass(t *testing.T) {
f := newFake(t)
f.mountinfo("/")
f.dir("/srv/pool/media")
f.dir("/srv/pool/restic")
f.file(ConfigFile, grantsConfig("pool=/srv/pool media=/srv/pool/media", "pool=ws:rw media=ws:rw"))
f.roster("ws 10.0.0.3")
f.wants(map[string][]Want{"ws": {{Share: "pool", Access: "rw"}, {Share: "media", Access: "rw"}}})
m := f.machine()
now := time.Date(2026, 10, 8, 20, 0, 0, 0, time.UTC)
m.Now = func() time.Time { return now }
// The repository is not there yet: the look below finds nothing and is kept for an hour.
if s := m.Pass(context.Background(), nil); refusedWhy(s, "pool") != "" {
t.Fatalf("the pool is refused before any repository exists: %+v", s.Refused)
}
// The backup seat starts keeping its repository there; a minute later is well inside LookKept.
f.backup("restic", "/srv/pool/restic", "/var/lib/restic")
now = now.Add(time.Minute)
s := m.Pass(context.Background(), nil)
if why := refusedWhy(s, "pool"); !strings.Contains(why, "/srv/pool/restic") || !strings.Contains(why, "restic") {
t.Errorf("a share holding the backup seat's repository is exported inside the kept look: %q", why)
}
if refusedWhy(s, "media") != "" {
t.Errorf("a share beside the repository is refused: %q", refusedWhy(s, "media"))
}
if strings.Contains(f.read(ExportFile), "/srv/pool ") {
t.Errorf("the export file still names the pool:\n%s", f.read(ExportFile))
}
}
func TestAShareInsideTheBackupSeatsRepositoryIsRefused(t *testing.T) {
f := newFake(t)
f.mountinfo("/")
f.dir("/srv/backups/data")
f.file(ConfigFile, grantsConfig("inside=/srv/backups/data", "inside=ws:rw"))
f.roster("ws 10.0.0.3")
f.wants(map[string][]Want{"ws": {{Share: "inside", Access: "rw"}}})
f.backup("restic", "/srv/backups")
s := f.machine().Pass(context.Background(), nil)
if !strings.Contains(refusedWhy(s, "inside"), "/srv/backups") {
t.Errorf("a share inside the backup seat's repository is not refused: %+v", s.Refused)
}
}
// Until a list has been read, what is exported stays exported — the bus being slow is no reason to take
// the library away — and the status says the list is not known.
func TestWithoutTheBackupListTheExportsStayAndItIsSaid(t *testing.T) {
f := newFake(t)
f.mountinfo("/")
f.dir("/srv/library")
f.file(ConfigFile, grantsConfig("library=/srv/library", "library=ws:rw"))
f.roster("ws 10.0.0.3")
f.wants(map[string][]Want{"ws": {{Share: "library", Access: "rw"}}})
s := f.machine().Pass(context.Background(), nil)
if refusedWhy(s, "library") != "" || len(s.Shares) != 1 {
t.Fatalf("the library is not exported while the backup list is unknown: %+v", s)
}
if !strings.Contains(strings.Join(s.Said, "\n"), "backup seat") {
t.Errorf("the status does not say the backup seat's repository is not known: %v", s.Said)
}
}
func TestBackupFromTheSeatsAndTheData(t *testing.T) {
seats := json.RawMessage(`{"answer":{"seats":[{"seat":"node-backup","scope":"node","holders":[{"module":"restic","node":"ace"},{"module":"borg","node":"shanks"}]}]},"ok":true}`)
data := json.RawMessage(`{"answer":{"data":[{"machine":"ace","module":"restic","item":"repository","path":"/storage/restic"},{"machine":"ace","module":"restic","item":"state","path":"/var/lib/restic"},{"machine":"ace","module":"plex","item":"data","path":"/mnt/plex/data"}]},"ok":true}`)
holder, paths, err := BackupFrom("ace", seats, data)
if err != nil || holder != "restic" || strings.Join(paths, " ") != "/storage/restic /var/lib/restic" {
t.Errorf("got %q %v %v", holder, paths, err)
}
if _, _, err := BackupFrom("g14", seats, data); err == nil {
t.Errorf("a machine whose backup seat nobody holds is not said")
}
if _, _, err := BackupFrom("ace", seats, json.RawMessage(`{"ok":true}`)); err == nil {
t.Errorf("an answer with no data list reads as an empty list")
}
}
// A failed ask keeps the last paths: the file is not emptied, only the error is added.
func TestAFailedAskKeepsTheLastBackupPaths(t *testing.T) {
f := newFake(t)
m := f.machine()
m.TakeBackupWith("ace", func(string, any) (json.RawMessage, error) {
return nil, errors.New("no responders")
})
f.backup("restic", "/storage/restic")
m.TakeBackupWith("ace", func(string, any) (json.RawMessage, error) {
return nil, errors.New("timeout")
})
var b BackupFromBus
if err := json.Unmarshal([]byte(f.read(BackupFile)), &b); err != nil {
t.Fatal(err)
}
if strings.Join(b.Paths, " ") != "/storage/restic" || !strings.Contains(b.Error, "timeout") {
t.Errorf("a failed ask lost the last paths or did not say why: %+v", b)
}
}