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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user