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
+560
View File
@@ -0,0 +1,560 @@
package main
import (
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// The secret every test that has one uses; it must never appear in anything the module says.
const testPassword = "s3cr3t-Pa55"
// fakeSystem is a machine under a temporary root with a recorded service manager.
type fakeSystem struct {
t *testing.T
root string
calls []string
// units are the service manager's answers: ActiveState and UnitFileState by unit.
units map[string]map[string]string
// reachable are the host:port pairs that answer.
reachable map[string]bool
}
func newSystem(t *testing.T) *fakeSystem {
f := &fakeSystem{t: t, root: t.TempDir(), units: map[string]map[string]string{}, reachable: map[string]bool{}}
for _, d := range []string{UnitDir, RunDir, BusDir, "/etc/mesh-mounts", "/proc/self", "/var/lib/mesh/mounts"} {
if err := os.MkdirAll(filepath.Join(f.root, d), 0o755); err != nil {
t.Fatal(err)
}
}
f.write("/proc/self/mountinfo", "")
return f
}
func (f *fakeSystem) write(p, content string) {
f.t.Helper()
full := filepath.Join(f.root, p)
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
f.t.Fatal(err)
}
if err := os.WriteFile(full, []byte(content), 0o644); err != nil {
f.t.Fatal(err)
}
}
func (f *fakeSystem) read(p string) string {
b, _ := os.ReadFile(filepath.Join(f.root, p))
return string(b)
}
func (f *fakeSystem) run(_ context.Context, name string, args ...string) (string, error) {
f.calls = append(f.calls, name+" "+strings.Join(args, " "))
if name != "systemctl" || len(args) == 0 {
return "", nil
}
switch args[0] {
case "show":
if len(args) >= 5 && args[3] == "--value" {
return f.units[args[4]]["ActiveState"] + "\n", nil
}
var blocks []string
for _, u := range args {
if !strings.HasSuffix(u, ".mount") && !strings.HasSuffix(u, ".automount") {
continue
}
s := f.units[u]
blocks = append(blocks, "Id="+u+"\nActiveState="+orDefault(s["ActiveState"], "inactive")+"\nSubState=dead\nResult="+
orDefault(s["Result"], "success")+"\nUnitFileState="+orDefault(s["UnitFileState"], "disabled"))
}
return strings.Join(blocks, "\n\n") + "\n", nil
case "enable":
u := args[len(args)-1]
f.units[u] = map[string]string{"ActiveState": "active", "UnitFileState": "enabled"}
}
return "", nil
}
func (f *fakeSystem) machine() *Machine {
return &Machine{Root: f.root, Run: f.run, Escalate: f.run, LookPath: func(string) bool { return true }}
}
func (f *fakeSystem) applier(now time.Time) *Applier {
return &Applier{M: f.machine(), Act: true, Judge: true,
Dial: func(addr string, _ time.Duration) error {
if f.reachable[addr] {
return nil
}
return errors.New("dial tcp " + addr + ": connect: no route to host")
},
Now: func() time.Time { return now },
Lookup: func(string) (int, int, error) { return 1000, 1000, nil },
}
}
func (f *fakeSystem) called(prefix string) bool {
for _, c := range f.calls {
if strings.HasPrefix(c, prefix) {
return true
}
}
return false
}
// everything is every file under the root, for a search that a secret appears nowhere it should not.
func (f *fakeSystem) everything(except ...string) string {
var b strings.Builder
filepath.Walk(f.root, func(p string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return nil
}
for _, e := range except {
if strings.HasSuffix(p, e) {
return nil
}
}
raw, _ := os.ReadFile(p)
b.Write(raw)
return nil
})
return b.String()
}
const config = "shares=library=/srv/library downloads=/srv/downloads:ro\n" +
"sources=games=smb://192.0.2.76/Share@/mnt/games usb=disk:1234-ABCD@/mnt/usb\n" +
"server=homeserver\nat=192.0.2.10\naccount=operator\n" +
"secret=/var/lib/mesh/mounts/smb-credentials.secret\nstate=/var/lib/mesh/mounts\n"
func exportsAnswer(t *testing.T, shares ...ServerShare) string {
raw, err := json.Marshal(ExportsAnswer{Server: "homeserver", At: "192.0.2.10", Shares: shares})
if err != nil {
t.Fatal(err)
}
return string(raw)
}
var (
library = ServerShare{Name: "library", Path: "/tank/media", Access: "rw", UID: 1001, GID: 2000, Live: true}
downloads = ServerShare{Name: "downloads", Path: "/tank/downloads", Access: "rw", UID: 1001, GID: 2000, Live: true}
)
func TestTheSettingsAreReadAndWhatCannotBeIsSaidPlainly(t *testing.T) {
shares, problems := ParseShares("library=/srv/library downloads=/srv/downloads:ro,bad Upper=/x other=relative library=/srv/again root=/")
if len(shares) != 2 || shares[0] != (Wanted{"library", "/srv/library", false}) || shares[1] != (Wanted{"downloads", "/srv/downloads", true}) {
t.Fatalf("shares %+v", shares)
}
want := []string{`"bad" is not name=mountpoint`, `"Upper" is not lower-case`, `"relative" is not an absolute path`, "named twice", `"/" is the system's own directory`}
for i, w := range want {
if i >= len(problems) || !strings.Contains(problems[i], w) {
t.Errorf("problem %d: want %q in %q", i, w, problems)
}
}
if s, p := ParseShares("none"); len(s) != 0 || len(p) != 0 {
t.Errorf("none is nothing: %v %v", s, p)
}
sources, problems := ParseSources("games=smb://192.0.2.76/Share@/mnt/games:ro usb=disk:1234-ABCD@/mnt/usb ftp=ftp://x/y@/mnt/x nomount=smb://h/s")
if len(sources) != 2 || sources[0].Kind != "smb" || sources[0].Host != "192.0.2.76" || sources[0].Share != "Share" || !sources[0].ReadOnly ||
sources[1].Kind != "disk" || sources[1].UUID != "1234-ABCD" {
t.Fatalf("sources %+v", sources)
}
if len(problems) != 2 || !strings.Contains(problems[0], "neither smb://") || !strings.Contains(problems[1], "names no mount point") {
t.Errorf("problems %q", problems)
}
if err := CheckMountpoint("/etc/x"); err == nil {
t.Error("a mount point under /etc is the system's")
}
}
func TestAMountPointIsNamedAsSystemdEscapeNamesIt(t *testing.T) {
for in, want := range map[string]string{
"/srv/library": "srv-library",
"/storage/media": "storage-media",
"/mnt/my-disk": `mnt-my\x2ddisk`,
"/mnt/.hidden": `mnt-.hidden`,
"/.x": `\x2ex`,
"/srv/a b": `srv-a\x20b`,
"/dev/disk/by-uuid/1234-ABCD": `dev-disk-by\x2duuid-1234\x2dABCD`,
} {
if got := EscapePath(in); got != want {
t.Errorf("EscapePath(%q) = %q, want %q", in, got, want)
}
}
}
func TestANodesShareIsMountedHardWhenWritableAndSoftWhenNot(t *testing.T) {
rw := NFSPair(Wanted{Name: "library", Mountpoint: "/srv/library"}, "192.0.2.10", library)
for _, want := range []string{"What=192.0.2.10:/tank/media", "Where=/srv/library", "Type=nfs4",
"Options=vers=4.2,hard,timeo=150,retrans=3,_netdev", "TimeoutSec=30", "After=network-online.target"} {
if !strings.Contains(rw.Mount, want) {
t.Errorf("the read-write mount lacks %q:\n%s", want, rw.Mount)
}
}
if !strings.HasPrefix(rw.Mount, Marker) || !strings.HasPrefix(rw.Automount, Marker) {
t.Error("both units open with the module's marker")
}
if strings.Contains(rw.Mount, "[Install]") {
t.Error("the mount is never enabled: only the automount is")
}
for _, want := range []string{"[Automount]\nWhere=/srv/library\nTimeoutIdleSec=600", "WantedBy=remote-fs.target"} {
if !strings.Contains(rw.Automount, want) {
t.Errorf("the automount lacks %q:\n%s", want, rw.Automount)
}
}
if rw.MountUnit != "srv-library.mount" || rw.AutoUnit != "srv-library.automount" || rw.Probe != "192.0.2.10:2049" {
t.Errorf("names %+v", rw)
}
ro := NFSPair(Wanted{Name: "downloads", Mountpoint: "/srv/downloads", ReadOnly: true}, "192.0.2.10", downloads)
if !strings.Contains(ro.Mount, "Options=ro,vers=4.2,soft,") {
t.Errorf("a read-only share is mounted ro and soft:\n%s", ro.Mount)
}
served := library
served.Access = "ro"
if p := NFSPair(Wanted{Name: "library", Mountpoint: "/srv/library"}, "192.0.2.10", served); !strings.Contains(p.Mount, "Options=ro,") {
t.Error("a share the server exports read-only is mounted read-only")
}
}
func TestAnOccasionalSourceWaitsTenSecondsAndNeverFailsABoot(t *testing.T) {
smb := SMBPair(Source{Name: "games", Kind: "smb", Host: "192.0.2.76", Share: "Share", Mountpoint: "/mnt/games"}, "/var/lib/mesh/mounts/smb-games.credentials", 1000, 1000)
for _, want := range []string{"What=//192.0.2.76/Share", "Type=cifs", "credentials=/var/lib/mesh/mounts/smb-games.credentials", "nofail", "uid=1000,gid=1000", "TimeoutSec=10"} {
if !strings.Contains(smb.Mount, want) {
t.Errorf("the SMB mount lacks %q:\n%s", want, smb.Mount)
}
}
if !smb.Occasional || smb.Probe != "192.0.2.76:445" || strings.Contains(smb.Mount, "password") {
t.Errorf("smb %+v", smb)
}
disk := DiskPair(Source{Name: "usb", Kind: "disk", UUID: "1234-ABCD", Mountpoint: "/mnt/usb"})
dev := `dev-disk-by\x2duuid-1234\x2dABCD.device`
if !strings.Contains(disk.Mount, "BindsTo="+dev) || !strings.Contains(disk.Mount, "What=/dev/disk/by-uuid/1234-ABCD") {
t.Errorf("the disk's mount goes with the disk:\n%s", disk.Mount)
}
if strings.Contains(disk.Automount, "BindsTo") || !strings.Contains(disk.Automount, "WantedBy=local-fs.target") {
t.Errorf("the disk's automount stays armed when the disk is away:\n%s", disk.Automount)
}
}
func TestALookWritesArmsAndRemovesOnlyTheModulesOwnUnits(t *testing.T) {
f := newSystem(t)
f.write(ConfigFile, config)
f.write(ExportsFile, exportsAnswer(t, library, downloads))
f.write("/var/lib/mesh/mounts/smb-credentials.secret", "[games]\nusername=root\npassword="+testPassword+"\n")
// The operator's own unit for the downloads' mount point: never written over.
f.write(filepath.Join(UnitDir, "srv-downloads.mount"), "[Mount]\nWhat=elsewhere\n")
// A pair of the module's whose mount point nothing names any more.
f.write(filepath.Join(UnitDir, "srv-old.mount"), Marker+"\n[Mount]\n")
f.write(filepath.Join(UnitDir, "srv-old.automount"), Marker+"\n[Automount]\n")
f.reachable["192.0.2.10:2049"] = true
now := time.Date(2026, 10, 8, 19, 21, 0, 0, time.UTC)
st := f.applier(now).Look(context.Background())
if got := f.read(filepath.Join(UnitDir, "srv-library.mount")); !strings.Contains(got, "What=192.0.2.10:/tank/media") {
t.Errorf("the library's mount: %q", got)
}
if got := f.read(filepath.Join(UnitDir, "srv-downloads.mount")); got != "[Mount]\nWhat=elsewhere\n" {
t.Errorf("a unit file that is not the module's is left byte for byte: %q", got)
}
if _, err := os.Stat(filepath.Join(f.root, UnitDir, "srv-old.mount")); !os.IsNotExist(err) {
t.Error("the module's pair nothing names any more is removed")
}
for _, c := range []string{"systemctl disable --now srv-old.automount", "systemctl stop srv-old.mount", "systemctl daemon-reload",
"systemctl enable --now srv-library.automount", "systemctl enable --now mnt-games.automount", "systemctl enable --now mnt-usb.automount"} {
if !f.called(c) {
t.Errorf("not called: %s (calls %q)", c, f.calls)
}
}
if f.called("systemctl enable --now srv-downloads.automount") {
t.Error("the refused share is not armed")
}
if info, err := os.Stat(filepath.Join(f.root, "/srv/library")); err != nil || !info.IsDir() {
t.Error("a missing mount point is made")
}
creds := filepath.Join(f.root, "/var/lib/mesh/mounts/smb-games.credentials")
if info, err := os.Stat(creds); err != nil || info.Mode().Perm() != 0o600 {
t.Fatalf("the source's login is a file only root reads: %v", err)
}
if b, _ := os.ReadFile(creds); string(b) != "username=root\npassword="+testPassword+"\n" {
t.Errorf("credentials file %q", b)
}
states := map[string]ShareStatus{}
for _, s := range st.Shares {
states[s.Name] = s
}
if states["library"].State != StateArmed || states["games"].State != StateAbsent || states["usb"].State != StateAbsent {
t.Errorf("states %+v", states)
}
if d := states["downloads"]; d.State != StateFailed || !strings.Contains(d.Why, "not the module's") {
t.Errorf("downloads %+v", d)
}
if !strings.Contains(states["games"].Why, "not present") || states["games"].Since != now.Format(time.RFC3339) {
t.Errorf("games %+v", states["games"])
}
// The password is in the secret and in the credentials file, and nowhere else.
if strings.Contains(f.everything("smb-credentials.secret", "smb-games.credentials"), testPassword) {
t.Error("the password leaked into a file the module wrote")
}
raw, _ := json.Marshal(f.machine().Health(now))
if strings.Contains(string(raw), testPassword) {
t.Error("the password leaked into the health")
}
// A second look a minute later changes nothing and keeps since.
f.calls = nil
later := now.Add(time.Minute)
st = f.applier(later).Look(context.Background())
if f.called("systemctl daemon-reload") || f.called("systemctl enable") {
t.Errorf("a look with nothing to change changes nothing: %q", f.calls)
}
for _, s := range st.Shares {
if s.Since != now.Format(time.RFC3339) {
t.Errorf("%s since %s, want the first look's", s.Name, s.Since)
}
}
}
func TestASilentServerRemovesNoMount(t *testing.T) {
f := newSystem(t)
f.write(ConfigFile, "shares=library=/srv/library\nserver=homeserver\nat=192.0.2.10\n")
f.write(filepath.Join(UnitDir, "srv-library.mount"), Marker+"\nkept\n")
f.write(filepath.Join(UnitDir, "srv-library.automount"), Marker+"\nkept\n")
st := f.applier(time.Now()).Look(context.Background())
if f.read(filepath.Join(UnitDir, "srv-library.mount")) != Marker+"\nkept\n" {
t.Error("with no answer from the server the pair stays as it is")
}
if len(st.Shares) != 1 || st.Shares[0].State != StateUnreachable || !strings.Contains(st.Shares[0].Why, "has not been read from the bus yet") {
t.Errorf("shares %+v", st.Shares)
}
// And a missing config file writes and removes nothing.
g := newSystem(t)
g.write(filepath.Join(UnitDir, "srv-x.mount"), Marker+"\n")
st = g.applier(time.Now()).Look(context.Background())
if _, err := os.Stat(filepath.Join(g.root, UnitDir, "srv-x.mount")); err != nil || len(st.Problems) == 0 {
t.Errorf("without its config the process removes nothing and says why: %v %v", err, st.Problems)
}
}
func TestAMissingLoginIsSaidWithItsRemedyAndTheSourceIsNotMounted(t *testing.T) {
f := newSystem(t)
f.write(ConfigFile, "sources=games=smb://192.0.2.76/Share@/mnt/games\nsecret=/var/lib/mesh/mounts/smb-credentials.secret\nstate=/var/lib/mesh/mounts\n")
f.write("/var/lib/mesh/mounts/smb-credentials.secret", "[other]\nusername=x\npassword="+testPassword+"\n")
now := time.Now()
st := f.applier(now).Look(context.Background())
if len(st.Shares) != 1 || st.Shares[0].State != StateFailed ||
!strings.Contains(st.Shares[0].Why, "secret accept <node> mounts smb-credentials --from <file>") {
t.Fatalf("shares %+v", st.Shares)
}
if _, err := os.Stat(filepath.Join(f.root, UnitDir, "mnt-games.mount")); !os.IsNotExist(err) {
t.Error("a source without its login gets no units")
}
h := f.machine().Health(now)
if h.Healthy || !strings.Contains(h.Why, "login is not given") || strings.Contains(h.Why, testPassword) {
t.Errorf("health %+v", h)
}
creds, problems := ParseSMBCredentials("[a]\nusername=u\npassword=" + testPassword + "\nbogus=1\n[b]\npassword=p\n")
if creds["a"].Password != testPassword || len(problems) != 2 || strings.Contains(strings.Join(problems, " "), testPassword) {
t.Errorf("creds %v problems %q", len(creds), problems)
}
}
func TestTheHealthGivesAShareFiveMinutesAndAnAbsentSourceNone(t *testing.T) {
now := time.Date(2026, 10, 8, 19, 30, 0, 0, time.Local)
at := func(d time.Duration) string { return now.Add(-d).Format(time.RFC3339) }
st := Status{Looked: at(10 * time.Second), Server: "homeserver", Shares: []ShareStatus{
{Name: "library", Kind: "nfs", Mountpoint: "/srv/library", State: StateUnreachable, Since: at(4 * time.Minute), Why: "192.0.2.10:2049 does not answer"},
{Name: "games", Kind: "smb", Occasional: true, Mountpoint: "/mnt/games", State: StateAbsent, Since: at(3 * time.Hour)},
}}
if h := Judge(st, nil, now); !h.Healthy || !strings.Contains(strings.Join(h.Said, " "), "games at /mnt/games: not present since 16:30") {
t.Errorf("under five minutes, and an absent source, are healthy: %+v", h)
}
st.Shares[0].Since = at(9 * time.Minute)
h := Judge(st, nil, now)
if h.Healthy || !strings.Contains(h.Why, "The share library of homeserver is not reachable from this machine since 19:21") {
t.Errorf("five minutes away raises: %+v", h)
}
st.Shares = []ShareStatus{{Name: "library", Kind: "nfs", Mountpoint: "/srv/library", State: StateFailed, Since: at(time.Minute),
Why: "its automount srv-library.automount is not armed (inactive)"}}
if h := Judge(st, nil, now); h.Healthy {
t.Error("an automount not armed is raised at once")
}
st.Looked = at(3 * time.Minute)
st.Shares = nil
if h := Judge(st, nil, now); h.Healthy || !strings.Contains(h.Why, "last looked") {
t.Errorf("a stale look is raised: %+v", h)
}
if h := Judge(Status{}, errors.New("not present"), now); h.Healthy {
t.Error("no status is unhealthy")
}
if h := Judge(Status{Looked: at(time.Second)}, nil, now); !h.Healthy || h.Why != "This machine is asked to mount nothing." {
t.Errorf("nothing asked: %+v", h)
}
}
// The workstation's two lines as they were found (research 038), in documentation addresses.
const operatorsFstab = "UUID=ad8b / ext4 rw,relatime 0 1\n" +
"192.0.2.10:/tank/media /srv/library nfs defaults,_netdev 0 0\n" +
"//192.0.2.76/Share /mnt/games cifs username=root,password=" + testPassword + ",iocharset=utf8,sec=ntlmssp 0 0\n"
func TestAnFstabLineIsTakenOverOnlyOnceItsReplacementIsArmed(t *testing.T) {
f := newSystem(t)
f.write("/etc/fstab", operatorsFstab)
m := f.machine()
now := time.Date(2026, 10, 8, 20, 0, 0, 0, time.UTC)
ctx := context.Background()
a, err := m.Adopt(ctx, "/mnt/games", false, "", now)
if err != nil || a.Done || !strings.HasPrefix(a.Said, "Would be refused now: the module has no unit pair") {
t.Fatalf("no pair yet: %+v %v", a, err)
}
if strings.Contains(a.Was, testPassword) || !strings.Contains(a.Was, "password=(redacted)") {
t.Errorf("the dry run says the line without its password: %q", a.Was)
}
if _, err := m.AdoptAsRoot(ctx, "/mnt/games", now); err == nil {
t.Fatal("the root half refuses too")
}
if f.read("/etc/fstab") != operatorsFstab {
t.Fatal("a refusal changes nothing")
}
f.write(filepath.Join(UnitDir, "mnt-games.mount"), Marker+"\n")
f.write(filepath.Join(UnitDir, "mnt-games.automount"), Marker+"\n")
f.units["mnt-games.automount"] = map[string]string{"ActiveState": "inactive"}
if _, err := m.AdoptAsRoot(ctx, "/mnt/games", now); err == nil || !strings.Contains(err.Error(), "not armed") {
t.Fatalf("an automount not armed refuses: %v", err)
}
f.units["mnt-games.automount"] = map[string]string{"ActiveState": "active"}
if a, err := m.Adopt(ctx, "/mnt/games", false, "", now); err != nil || !strings.HasPrefix(a.Said, "A dry run") || a.Line != 3 {
t.Fatalf("dry run: %+v %v", a, err)
}
if _, err := m.Adopt(ctx, "/mnt/games", true, "", now); err == nil {
t.Error("confirm without why is refused")
}
a, err = m.AdoptAsRoot(ctx, "/mnt/games", now)
if err != nil || !a.Done || a.Line != 3 || a.Copy != "/etc/fstab.mesh-adopted-20261008-200000" {
t.Fatalf("adopt: %+v %v", a, err)
}
got := f.read("/etc/fstab")
lines := strings.Split(got, "\n")
if lines[0] != "UUID=ad8b / ext4 rw,relatime 0 1" || lines[1] != "192.0.2.10:/tank/media /srv/library nfs defaults,_netdev 0 0" {
t.Errorf("every other line is kept byte for byte:\n%s", got)
}
if !strings.HasPrefix(lines[2], AdoptedPrefix+"2026-10-08: //192.0.2.76/Share") || strings.Contains(got, testPassword) {
t.Errorf("the line is commented out with its password redacted:\n%s", got)
}
info, err := os.Stat(filepath.Join(f.root, a.Copy))
if err != nil || info.Mode().Perm() != 0o600 || f.read(a.Copy) != operatorsFstab {
t.Errorf("the file as it was is kept, root only: %v", err)
}
if !f.called("systemctl daemon-reload") {
t.Error("the service manager reloads")
}
raw, _ := json.Marshal(a)
if strings.Contains(string(raw), testPassword) {
t.Error("the answer holds the password")
}
if _, err := m.AdoptAsRoot(ctx, "/mnt/games", now); err == nil || !strings.Contains(err.Error(), "no line for /mnt/games") {
t.Errorf("a line taken over is not taken twice: %v", err)
}
if _, _, err := PlanAdoption("a /x nfs d 0 0\nb /x nfs d 0 0\n", "/x", now); err == nil || !strings.Contains(err.Error(), "more than one line") {
t.Errorf("two lines for one mount point are left to a person: %v", err)
}
}
func TestAFailedAskKeepsTheLastAnswer(t *testing.T) {
cfg := ParseConfig("shares=library=/srv/library\nserver=homeserver\nat=192.0.2.10\n")
now := time.Now()
var asked string
ok := func(key string) (json.RawMessage, error) {
asked = key
return json.RawMessage(`{"shares":[{"name":"library","path":"/tank/media","access":"rw","uid":1001,"gid":2000,"live":true}]}`), nil
}
first := AskExports(cfg, ExportsAnswer{}, ok, now)
if asked != "homeserver" || len(first.Shares) != 1 || first.Error != "" {
t.Fatalf("first %+v asked %q", first, asked)
}
fail := func(string) (json.RawMessage, error) { return nil, errors.New("no responders") }
second := AskExports(cfg, first, fail, now.Add(time.Minute))
empty := AskExports(cfg, first, func(string) (json.RawMessage, error) { return nil, nil }, now)
if !strings.Contains(empty.Error, "keeps nothing") || len(empty.Shares) != 1 {
t.Errorf("a machine with nothing kept yet: %+v", empty)
}
if second.Error != "no responders" || len(second.Shares) != 1 || second.Answered != first.Answered {
t.Errorf("second %+v", second)
}
if got := ParseConfig("at=homeserver.mesh.example\n").ServerNode(); got != "homeserver" {
t.Errorf("without from, the first label of at: %q", got)
}
}
func TestTheBundlePutsWhatTheProcessFoundOnTheBus(t *testing.T) {
f := newSystem(t)
f.write(ConfigFile, "shares=library=/srv/library\nserver=homeserver\nat=192.0.2.10\n")
f.write(StatusFile, `{"looked":"2026-10-08T19:21:00Z","shares":[{"name":"library","state":"armed"}]}`)
var put []string
last := ""
ask := func(string) (json.RawMessage, error) { return json.RawMessage(`{"shares":[]}`), nil }
said := f.machine().Share("workstation", ask, func(key string, _ any) error { put = append(put, key); return nil }, &last, time.Now())
if len(said) != 0 || len(put) != 1 || put[0] != "workstation" || !strings.Contains(f.read(ExportsFile), `"server": "homeserver"`) {
t.Errorf("said %q put %q exports %q", said, put, f.read(ExportsFile))
}
f.machine().Share("workstation", ask, func(key string, _ any) error { put = append(put, key); return nil }, &last, time.Now())
if len(put) != 1 {
t.Error("an unchanged status is not put again")
}
}
// In production the acting process and the judging one are two: apply writes and arms and says nothing
// of how the shares are; watch writes and arms nothing and says how they are.
func TestApplyActsAndWatchJudgesAndNeitherDoesTheOthersWork(t *testing.T) {
f := newSystem(t)
f.write(ConfigFile, config)
f.write(ExportsFile, exportsAnswer(t, library, downloads))
f.write("/var/lib/mesh/mounts/smb-credentials.secret", "[other]\nusername=x\npassword="+testPassword+"\n")
f.write(filepath.Join(UnitDir, "srv-downloads.mount"), "[Mount]\nWhat=elsewhere\n")
f.reachable["192.0.2.10:2049"] = true
now := time.Now()
apply := f.applier(now)
apply.Judge = false
apply.Look(context.Background())
if _, err := os.Stat(filepath.Join(f.root, StatusFile)); !os.IsNotExist(err) {
t.Error("apply leaves no status: that is watch's")
}
if !f.called("systemctl enable --now srv-library.automount") || f.read(filepath.Join(UnitDir, "srv-library.mount")) == "" {
t.Fatal("apply writes and arms")
}
applied, err := readJSON[Applied](f.machine(), AppliedFile)
if err != nil || len(applied.NotSet) != 2 {
t.Fatalf("apply says what it could not set up: %+v %v", applied, err)
}
f.calls = nil
watch := f.applier(now)
watch.Act = false
f.write(filepath.Join(UnitDir, "srv-gone.mount"), Marker+"\n")
st := watch.Look(context.Background())
for _, c := range f.calls {
if !strings.HasPrefix(c, "systemctl show") {
t.Errorf("watch only reads the service manager: %s", c)
}
}
if _, err := os.Stat(filepath.Join(f.root, UnitDir, "srv-gone.mount")); err != nil {
t.Error("watch removes nothing")
}
states := map[string]string{}
for _, s := range st.Shares {
states[s.Name] = s.State
}
if states["library"] != StateArmed || states["downloads"] != StateFailed || states["games"] != StateFailed || states["usb"] != StateAbsent {
t.Errorf("watch says how each is: %v", states)
}
if strings.Contains(f.everything("smb-credentials.secret"), testPassword) {
t.Error("the password leaked")
}
}