mesh/delivery-group group feat/774-l3-reland rejected: composed together: the checker's verdict at the members' heads (b48c90fea31f): integration: mesh-self-ch…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: integration: mesh-self-check/cmd/mesh-self-check.TestTheSelfCheckWholeRunsOnTheBus: the run answered {Run:doctor-1793351737-1 At:20…
One commit with exactly the tree of the reviewed head35b4509c0a, on main12c2574c9d, so two compiled programs added and removed on the branch leave no trace in history (issue 1577).
222 lines
8.6 KiB
Go
222 lines
8.6 KiB
Go
// restic-backups (novox/hq ADR 0214, to-be 43): the machine's backups. One binary, three ways to run:
|
|
//
|
|
// - with no argument, launched by the machine's tool runtime and speaking MCP to it over stdio through the
|
|
// Go SDK (ADR 0193, ADR 0198): the node-backup seat's three verbs — what is backed up, take one now,
|
|
// restore beside the live data — and the health check, backup_health;
|
|
// - `night`, a process the node-engine runs: the night that happens without anyone asking, judged by
|
|
// backup_health from what it leaves;
|
|
// - `measure`, a process the node-engine runs: the hourly measuring of the data declared here (ADR 0233),
|
|
// judged by its unit.
|
|
//
|
|
// The night and the measuring were goroutines of the tool runtime's process until novox/hq issue 454: a module
|
|
// raises a condition only through the health of something it runs (ADR 0240, ADR 0256), and a check by the
|
|
// module's own tool only beside a check of another kind, so each runs as a process of its own.
|
|
//
|
|
// stdout is the MCP channel; everything this module says, it says on stderr.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/announce"
|
|
"git.novox.be/novox/mesh-sdk/go/health"
|
|
)
|
|
|
|
// Seat is the role this module holds.
|
|
const Seat = "node-backup"
|
|
|
|
func say(format string, args ...any) {
|
|
fmt.Fprintf(os.Stderr, "[restic] "+format+"\n", args...)
|
|
}
|
|
|
|
func main() {
|
|
where, err := whereFromEnv()
|
|
if err != nil {
|
|
say("%v", err)
|
|
os.Exit(1)
|
|
}
|
|
b := &Backups{Where: where, Run: execRunner, Now: time.Now, Say: say, Node: os.Getenv("MESH_NODE")}
|
|
way := ""
|
|
if len(os.Args) > 1 {
|
|
way = os.Args[1]
|
|
}
|
|
if way == "" {
|
|
b.Emit = func(event string, body any) error { return stdio.Emit(event, body) }
|
|
b.Readings = busReadings(b.Node)
|
|
}
|
|
switch way {
|
|
case "night":
|
|
nights(b)
|
|
case "awake":
|
|
counting(b)
|
|
case "":
|
|
if err := stdio.Keeps(announce.State{Name: health.GoodState}); err != nil {
|
|
say("%v", err)
|
|
os.Exit(1)
|
|
}
|
|
b.PutGood = func(key string, row health.GoodRow) error {
|
|
_, err := stdio.State(health.GoodState).Put(key, row)
|
|
return err
|
|
}
|
|
go watchGood(b)
|
|
if err := stdio.Serve("", tools(b)); err != nil {
|
|
say("%v", err)
|
|
os.Exit(1)
|
|
}
|
|
default:
|
|
say("%q is not a way to run; with no argument it serves its tools, with night it runs the nights, with awake it counts how long the machine is up since a night", way)
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
// nights runs at the hour, every module; and at start, if a night was missed — the machine was off
|
|
// or this module was not running at the hour — one soon rather than a day later. Not at once: a
|
|
// machine just started has its stores still coming up.
|
|
func nights(b *Backups) {
|
|
hour := 3
|
|
if h, err := strconv.Atoi(os.Getenv("MESH_BACKUP_HOUR")); err == nil && h >= 0 && h < 24 {
|
|
hour = h
|
|
}
|
|
if missedANight(b.Nights(), time.Now()) {
|
|
time.Sleep(10 * time.Minute)
|
|
night(b)
|
|
}
|
|
for {
|
|
time.Sleep(time.Until(nextNight(time.Now(), hour)))
|
|
night(b)
|
|
}
|
|
}
|
|
|
|
func night(b *Backups) {
|
|
ctx := context.Background()
|
|
outcome, err := b.BackUp(ctx, "")
|
|
if err != nil {
|
|
say("the night did not run: %v", err)
|
|
return
|
|
}
|
|
var failed []string
|
|
for module, n := range outcome {
|
|
if !n.OK {
|
|
failed = append(failed, module)
|
|
}
|
|
}
|
|
if len(failed) > 0 {
|
|
say("the night left %s without a backup", strings.Join(failed, ", "))
|
|
}
|
|
// Sundays, the repository's own integrity with a sample of the data read back.
|
|
if time.Now().Weekday() == time.Sunday {
|
|
if err := b.Check(ctx); err != nil {
|
|
say("the repository does NOT check out: %v", err)
|
|
} else {
|
|
say("the repository checks out")
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- the seat's verbs --------------------------------------------------------------------------
|
|
|
|
func str(description string) map[string]any {
|
|
return map[string]any{"type": "string", "description": description}
|
|
}
|
|
|
|
func arg(a map[string]any, k string) string {
|
|
v, _ := a[k].(string)
|
|
return strings.TrimSpace(v)
|
|
}
|
|
|
|
// verb is one of the seat's verbs: listed as `<seat>.<verb>`, so the runtime serves it on the seat's
|
|
// subject.
|
|
func verb(name, description string, input map[string]any, run func(a map[string]any) (any, error)) stdio.Tool {
|
|
return stdio.Tool{Name: Seat + "." + name, Description: description, Input: input, Run: run}
|
|
}
|
|
|
|
func tools(b *Backups) []stdio.Tool {
|
|
return []stdio.Tool{
|
|
verb("backed-up", "What this machine backs up: each module, what it declared, its last good night, how many restore points are kept.",
|
|
map[string]any{"module": str("one module (optional)")},
|
|
func(a map[string]any) (any, error) { return b.BackedUp(context.Background(), arg(a, "module")) }),
|
|
verb("now", "Take a backup now, of one module or of every module on this machine — before a migration, a retirement or anything else that could go wrong. Kept as taken by hand until a later good night of the module exists, and for keep-hand-taken-days in any case. Answers when it has started; `backed-up` says how it went.",
|
|
map[string]any{"module": str("one module (optional)")},
|
|
func(a map[string]any) (any, error) {
|
|
only := arg(a, "module")
|
|
// A night of a large store outlasts any call; it is started, and its outcome recorded.
|
|
go func() {
|
|
if _, err := b.BackUpNow(context.Background(), only); err != nil {
|
|
say("a backup asked for now failed: %v", err)
|
|
}
|
|
}()
|
|
started := only
|
|
if started == "" {
|
|
started = "every module on this machine"
|
|
}
|
|
return map[string]any{"started": started, "follow": Seat + ".backed-up"}, nil
|
|
}),
|
|
verb("restore", "Restore one module's data from a restore point BESIDE the live data, never over it: each directory as <path>.restored-<date>, or in this module's state when a directory on the way belongs to another account. Swapping it in is a person's act.",
|
|
map[string]any{
|
|
"module": str("the module"),
|
|
"snapshot": str("the restore point (the newest when omitted)"),
|
|
"path": str("one of the module's directories (all of them when omitted)"),
|
|
},
|
|
func(a map[string]any) (any, error) {
|
|
module := arg(a, "module")
|
|
if module == "" {
|
|
return nil, fmt.Errorf("module is required")
|
|
}
|
|
return b.Restore(context.Background(), module, arg(a, "snapshot"), arg(a, "path"))
|
|
}),
|
|
{Name: "backup_health", Description: "Whether this machine's backups hold, as the module's health check " +
|
|
"(novox/hq ADR 0240, issue 454): unhealthy when a module was NOT backed up at its last night, the last night " +
|
|
"did not run, the last thinning failed, or the retention file is missing or unreadable — each said with what " +
|
|
"to do; waits, naming the setting, when a retention setting is refused. (r)",
|
|
Run: func(map[string]any) (any, error) { return b.Health(context.Background()), nil }},
|
|
// Deleting a retired item, when a person decided (novox/hq ADR 0233): the controller's `cleanup
|
|
// delete` asks these. Not seat verbs — nobody else calls them.
|
|
{Name: "backup_delete_retired", Description: "Delete one item of data the mesh retired on this machine — " +
|
|
"its module is gone from here — after taking a last restore point of it, tagged retired, so it can be " +
|
|
"restored until that restore point is forgotten. Refuses anything declared now. Answers at once; " +
|
|
"backup_deleted says how it went. Use the controller's `cleanup delete`, which records the hand act.",
|
|
Input: map[string]any{"module": str("the module it was"), "item": str("the item"), "path": str("where it is"),
|
|
"confirm": str("the item's name again"), "why": str("why — required"), "by": str("who decided"),
|
|
"via": str("mesh-controller when asked through its verbs")},
|
|
Run: func(a map[string]any) (any, error) {
|
|
return b.DeleteRetired(context.Background(), arg(a, "module"), arg(a, "item"), arg(a, "path"),
|
|
arg(a, "confirm"), arg(a, "by"), arg(a, "why"))
|
|
}},
|
|
{Name: "backup_deleted", Description: "How the deletion of one retired item went: running, or done with " +
|
|
"its last restore point, or refused with why.",
|
|
Input: map[string]any{"path": str("where it is")},
|
|
Run: func(a map[string]any) (any, error) { return b.DeletedOutcome(arg(a, "path")) }},
|
|
}
|
|
}
|
|
|
|
// busReadings reads disk-load's readings of this machine from its state, every key `<node>.<module>.<item>`.
|
|
func busReadings(node string) func() ([]health.Reading, error) {
|
|
return func() ([]health.Reading, error) {
|
|
st := stdio.State(health.ReadingsRead)
|
|
keys, err := st.Keys()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []health.Reading
|
|
for _, k := range keys {
|
|
if !strings.HasPrefix(k, node+".") {
|
|
continue
|
|
}
|
|
e, err := st.Get(k)
|
|
if err != nil || e == nil {
|
|
continue
|
|
}
|
|
if r, err := health.DecodeReading(e.Value); err == nil {
|
|
out = append(out, r)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
}
|