Let mounts write each share's mount and automount, so no node depends on a hand-written fstab line (hq ADR 0263)

A machine mounts what its assignment names: a node's share from the
provision nfs-share, whose path it reads from nfs-server's state on the
bus, and occasional sources (an SMB device, a disk) whose absence is a
state, not a fault. apply writes and arms the unit pairs as root; watch
tries each source and judges the health; the operator's fstab lines are
taken over only through adopt, on their word, once the replacement is
armed. SMB logins are an outside party's secret, given with secret accept.
This commit is contained in:
jochen
2026-10-08 18:36:01 +02:00
parent 3251758340
commit 2b1b9b0235
16 changed files with 2583 additions and 18 deletions
+99
View File
@@ -0,0 +1,99 @@
package main
// The bundle's half on the bus (novox/hq ADR 0263 rule 9, ADR 0201). The bundle runs as the operator
// account and reaches the bus through the runtime; the process runs as root and does not. So every 30
// seconds the bundle:
//
// - reads what the sharing machine exports from nfs-server's state `exports`, keyed by that machine
// (ADR 0201: a module reads another's state, never its files), and leaves it in ExportsFile for the
// process. A failed read keeps the last answer's shares, so a bus that is away for a minute removes no
// mount;
// - puts what the process last found in the state `shares`, keyed by this machine, so a view or the
// controller watches it without reading this machine's files.
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
)
// SharesState is the state the module keeps on the bus, one key per machine.
const SharesState = "shares"
// ExportsState is nfs-server's state this module reads: what each sharing machine exports, by machine.
const ExportsState = "nfs-server.exports"
// Getter reads one key of a state, nil when it has none; Putter puts one key of a state.
type (
Getter func(key string) (json.RawMessage, error)
Putter func(key string, value any) error
)
// AskExports reads what the sharing machine exports, once, and answers what to leave for the process.
func AskExports(cfg Config, previous ExportsAnswer, get Getter, now time.Time) ExportsAnswer {
out := ExportsAnswer{Server: cfg.ServerNode(), At: cfg.At, Asked: now.Format(time.RFC3339)}
if out.Server == "" {
out.Error = "no machine shares nfs-share for this one"
return out
}
raw, err := get(out.Server)
var answer struct {
Shares []ServerShare `json:"shares"`
}
if err == nil && len(raw) == 0 {
err = fmt.Errorf("%s keeps nothing in nfs-server's state exports yet", out.Server)
}
if err == nil {
err = json.Unmarshal(raw, &answer)
if err == nil && answer.Shares == nil {
err = fmt.Errorf("the answer names no shares")
}
}
if err != nil {
out.Error = err.Error()
if previous.Server == out.Server {
out.Shares, out.Answered = previous.Shares, previous.Answered
}
return out
}
out.Shares, out.Answered = answer.Shares, out.Asked
return out
}
// Share is one pass of the bundle's work over a machine.
func (m *Machine) Share(node string, get Getter, put Putter, last *string, now time.Time) []string {
var said []string
if text, err := m.read(ConfigFile); err == nil {
cfg := ParseConfig(text)
if len(cfg.Shares) > 0 {
previous, _ := readJSON[ExportsAnswer](m, ExportsFile)
answer := AskExports(cfg, previous, get, now)
if answer.Error != "" {
said = append(said, "what the sharing machine exports was not read: "+answer.Error)
}
raw, _ := json.MarshalIndent(answer, "", " ")
full := filepath.Join(m.Root, ExportsFile)
tmp := full + ".new"
if err := os.WriteFile(tmp, append(raw, '\n'), 0o644); err != nil {
said = append(said, "the answer was not left for the process: "+err.Error())
} else if err := os.Rename(tmp, full); err != nil {
said = append(said, "the answer was not left for the process: "+err.Error())
}
}
}
if node != "" && put != nil {
if text, err := m.read(StatusFile); err == nil && text != *last {
var st Status
if json.Unmarshal([]byte(text), &st) == nil {
if err := put(node, st); err != nil {
said = append(said, "the state was not put: "+err.Error())
} else {
*last = text
}
}
}
}
return said
}