173 lines
6.3 KiB
Go
173 lines
6.3 KiB
Go
package main
|
|
|
|
// Where this machine's backup seat keeps its data, from the bus (follow-up to the review of
|
|
// mesh-catalog #136, point 4).
|
|
//
|
|
// The look below a share for a backup repository walks the folders and is believed for LookKept, so a
|
|
// repository that the backup seat starts keeping under a share inside that hour would be exported until the
|
|
// look is taken again. So the module also learns, over the bus, which module holds `node-backup` on this
|
|
// machine and every path that module keeps data in (its repository among them), and every pass judges
|
|
// every share against those paths with nothing kept: a share at, under or above one of them is refused.
|
|
//
|
|
// The tools ask — the root process does not reach the bus — every BackupEvery, and leave what they learnt
|
|
// in BackupFile for the root process. A failed ask keeps the last paths and says why. Until a list has
|
|
// been read, what is exported stays exported and the status says the list is not known: the bus being
|
|
// slow is no reason to take the library away, and the look below still guards against a repository.
|
|
//
|
|
// The parsing of mesh-controller.data's answer is a small copy of the mounts module's (DataFrom); shared
|
|
// code belongs in mesh-sdk, and moves there when a third module needs it.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"sort"
|
|
"time"
|
|
)
|
|
|
|
// BackupFile is what the tools learnt about the backup seat on this machine.
|
|
const BackupFile = WantsDir + "/backup.json"
|
|
|
|
// BackupEvery is how often the tools ask.
|
|
const BackupEvery = 5 * time.Minute
|
|
|
|
// BackupSeat is the seat whose holder keeps this machine's backups.
|
|
const BackupSeat = "node-backup"
|
|
|
|
// BackupFromBus is the backup seat's holder on this machine and the paths it keeps data in.
|
|
type BackupFromBus struct {
|
|
Holder string `json:"holder"`
|
|
Paths []string `json:"paths"`
|
|
Looked string `json:"looked,omitempty"`
|
|
// Error is why the last ask got no answer; the holder and paths are then the last answer's.
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// BackupFrom reads the holder of node-backup on one machine from mesh-controller.seats' answer, and the
|
|
// paths that module keeps data in on that machine from mesh-controller.data's. A machine whose backup seat
|
|
// nobody holds, and an answer with no data list, are errors: never an empty list read as "nothing to fear".
|
|
func BackupFrom(machine string, seats, data json.RawMessage) (string, []string, error) {
|
|
var s struct {
|
|
Answer *struct {
|
|
Seats []struct {
|
|
Seat string `json:"seat"`
|
|
Holders []struct {
|
|
Module string `json:"module"`
|
|
Node string `json:"node"`
|
|
} `json:"holders"`
|
|
} `json:"seats"`
|
|
} `json:"answer"`
|
|
}
|
|
if err := json.Unmarshal(seats, &s); err != nil || s.Answer == nil {
|
|
return "", nil, fmt.Errorf("the controller's seats answer could not be read: %v", err)
|
|
}
|
|
holder := ""
|
|
for _, seat := range s.Answer.Seats {
|
|
if seat.Seat != BackupSeat {
|
|
continue
|
|
}
|
|
for _, h := range seat.Holders {
|
|
if h.Node == machine {
|
|
holder = h.Module
|
|
}
|
|
}
|
|
}
|
|
if holder == "" {
|
|
return "", nil, fmt.Errorf("no module holds %s on %s", BackupSeat, machine)
|
|
}
|
|
var d struct {
|
|
Answer *struct {
|
|
Data *[]struct {
|
|
Machine string `json:"machine"`
|
|
Module string `json:"module"`
|
|
Path string `json:"path"`
|
|
} `json:"data"`
|
|
} `json:"answer"`
|
|
}
|
|
if err := json.Unmarshal(data, &d); err != nil || d.Answer == nil || d.Answer.Data == nil {
|
|
return "", nil, fmt.Errorf("the controller's data answer holds no data list (%v)", err)
|
|
}
|
|
var paths []string
|
|
for _, item := range *d.Answer.Data {
|
|
if item.Module == holder && (item.Machine == "" || item.Machine == machine) && path.IsAbs(item.Path) {
|
|
paths = append(paths, path.Clean(item.Path))
|
|
}
|
|
}
|
|
sort.Strings(paths)
|
|
return holder, paths, nil
|
|
}
|
|
|
|
// TakeBackup asks the controller over the bus, as the module, through the runtime.
|
|
func (m *Machine) TakeBackup(machine string) {
|
|
m.TakeBackupWith(machine, askBus)
|
|
}
|
|
|
|
// TakeBackupWith is TakeBackup with the ask given, for tests. A failed ask keeps the last holder and
|
|
// paths and adds why.
|
|
func (m *Machine) TakeBackupWith(machine string, ask func(key string, body any) (json.RawMessage, error)) {
|
|
var last BackupFromBus
|
|
if text, err := m.Read(BackupFile); err == nil {
|
|
_ = json.Unmarshal([]byte(text), &last)
|
|
}
|
|
next := last
|
|
next.Looked = m.Now().UTC().Format(time.RFC3339)
|
|
next.Error = ""
|
|
seats, err := ask("seat:mesh-controller.seats", map[string]any{})
|
|
var data json.RawMessage
|
|
if err == nil {
|
|
data, err = ask("seat:mesh-controller.data", map[string]any{"machine": machine})
|
|
}
|
|
if err == nil {
|
|
var holder string
|
|
var paths []string
|
|
holder, paths, err = BackupFrom(machine, seats, data)
|
|
if err == nil {
|
|
next.Holder, next.Paths = holder, paths
|
|
}
|
|
}
|
|
if err != nil {
|
|
next.Error = err.Error()
|
|
fmt.Fprintln(os.Stderr, "nfs-server: where the backup seat keeps its data could not be learnt:", err)
|
|
}
|
|
raw, _ := json.MarshalIndent(next, "", " ")
|
|
if werr := m.Write(BackupFile, string(raw)+"\n"); werr != nil {
|
|
fmt.Fprintln(os.Stderr, "nfs-server: the backup seat's paths could not be written:", werr)
|
|
}
|
|
}
|
|
|
|
// Backup is what the tools last learnt, and a sentence when nothing usable has been learnt yet.
|
|
func (m *Machine) Backup() (BackupFromBus, string) {
|
|
text, err := m.Read(BackupFile)
|
|
if err != nil {
|
|
return BackupFromBus{}, "where this machine's backup seat keeps its repository has not been learnt from the bus yet; the shares are judged by the look below them alone until it is"
|
|
}
|
|
var b BackupFromBus
|
|
if err := json.Unmarshal([]byte(text), &b); err != nil {
|
|
return BackupFromBus{}, fmt.Sprintf("what was learnt about the backup seat (%s) could not be read: %v", BackupFile, err)
|
|
}
|
|
if len(b.Paths) == 0 {
|
|
why := "no path has been learnt"
|
|
if b.Error != "" {
|
|
why = b.Error
|
|
}
|
|
return b, "where this machine's backup seat keeps its repository is not known (" + why + "); the shares are judged by the look below them alone until it is"
|
|
}
|
|
return b, ""
|
|
}
|
|
|
|
// nearBackup is the backup seat's path a real path is at, under or above, or "".
|
|
func nearBackup(real string, b BackupFromBus) (string, string) {
|
|
for _, p := range b.Paths {
|
|
switch {
|
|
case real == p:
|
|
return p, "is"
|
|
case len(real) > len(p) && real[:len(p)] == p && (p == "/" || real[len(p)] == '/'):
|
|
return p, "is inside"
|
|
case len(p) > len(real) && p[:len(real)] == real && (real == "/" || p[len(real)] == '/'):
|
|
return p, "holds"
|
|
}
|
|
}
|
|
return "", ""
|
|
}
|