From 8927d689003bda13c2327f980e38fe87b38529de Mon Sep 17 00:00:00 2001 From: jochens Date: Fri, 13 Nov 2026 01:05:19 +0100 Subject: [PATCH] Z4 zfs-manifest-bus: the events stream, states and ledger by template, the keeper as the module's one process bus account MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three root processes declare no bus and reach the keeper's socket; the module invokes the service manager's start. The judge refuses the keeper's bus emits and state until the validator accepts what the module asks for in requires (A-38). Builds: design 42 zfs-owns-zfs §25, §26, §27, §28, A-6, A-19, A-36, A-37, A-38 --- modules/zfs/cmd/zfs-tools/manifest_test.go | 85 ++++++++++- modules/zfs/module.json | 169 ++++++++++++++++++++- 2 files changed, 247 insertions(+), 7 deletions(-) diff --git a/modules/zfs/cmd/zfs-tools/manifest_test.go b/modules/zfs/cmd/zfs-tools/manifest_test.go index 3c028f2a0..ea7021441 100644 --- a/modules/zfs/cmd/zfs-tools/manifest_test.go +++ b/modules/zfs/cmd/zfs-tools/manifest_test.go @@ -76,8 +76,8 @@ func TestTheManifestNamesTheToolsTheBundleServesAndWhatEachReplaces(t *testing.T if len(m.Claims) != 0 { t.Errorf("the module claims no seat: %v", m.Claims) } - if strings.Join(m.Emits, ",") != "snapshot.taken,"+ScrubEvent { - t.Errorf("emits %v", m.Emits) + if len(m.Emits) != 0 { + t.Errorf("the events are asked for by template in requires: emits %v", m.Emits) } if hit := regexp.MustCompile(`\b(g14|novox|ace|shanks|jochen)\b|/home/`).FindString(strings.ReplaceAll(raw, "novox/hq", "")); hit != "" { t.Errorf("the manifest says %q", hit) @@ -459,3 +459,84 @@ func TestTheBundleRunsOnlyTheFormsOfADR0679(t *testing.T) { t.Errorf("found %d call sites; the test no longer sees them", sites) } } + +// §25, §26 (A-19, A-36), A-37, A-38, §28: one events stream by template, 30 days, every kind of §25; the states applied, +// schedule and findings; the ledger snapshots of A-36 with a fixed cap and no index; the keeper alone holds the process bus, as a user of +// the module's own, naming the module's events and states and invoking the three; the three root processes declare no bus and +// reach the keeper's socket; the module invokes the service manager's start; the tools list holds §28's. +func TestTheManifestsBusStateKeeperAndTools(t *testing.T) { + tree, byID, _ := manifestOf(t) + reqs := map[string]map[string]any{} + for _, r := range tree["requires"].([]any) { + e := r.(map[string]any) + reqs[e["name"].(string)] = e + } + ev := reqs["events"] + opts, _ := ev["options"].(map[string]any) + var kinds []string + for _, k := range opts["events"].([]any) { + kinds = append(kinds, k.(string)) + } + if ev["storage"] != "stream" || ev["template"] != "events" || opts["max-age-days"] != float64(30) || strings.Join(kinds, ",") != strings.Join(EventKinds, ",") { + t.Errorf("the events stream: %v", ev) + } + for _, n := range []string{"applied", "schedule", "findings"} { + if e := reqs[n]; e == nil || e["storage"] != "key-value" || e["template"] != "state" { + t.Errorf("the state %s: %v", n, e) + } + } + if reqs["findings"]["options"] != nil { + t.Errorf("findings declares no stream of its own (A-32): %v", reqs["findings"]) + } + l := reqs[LedgerName] + lo, _ := l["options"].(map[string]any) + types, _ := l["types"].([]any) + if l["storage"] != "ledger" || l["template"] != "ledger" || lo["max-mib"] == nil || len(types) != 1 { + t.Fatalf("the ledger: %v", l) + } + ty := types[0].(map[string]any) + ops, _ := json.Marshal(ty["operations"]) + if ty["name"] != "snapshot" || ty["indexes"] != nil || + string(ops) != `[{"effect":"set-entry","fields":["dataset","name","kind","at"],"into":"taken","key":"name","name":"taken"},{"effect":"remove","into":"taken","key":"name","name":"removed"}]` { + t.Errorf("the ledger's type: %v", ty) + } + k := byID["keeper"] + bus, _ := k["bus"].(map[string]any) + acct := byID["keeper-account"] + if k["user"] != "zfs-keeper" || acct["type"] != "user" || acct["name"] != "zfs-keeper" || bus == nil || + fmt.Sprint(bus["state"]) != "[applied schedule findings]" || + fmt.Sprint(bus["invokes"]) != "["+ToolRecord+" "+ToolSchedule+" seat:node-service-manager.start]" { + t.Errorf("the keeper: %v %v", k, acct) + } + if got := fmt.Sprint(bus["emits"]); strings.Contains(got, ScrubEvent) || !strings.Contains(got, EventApplied) || !strings.Contains(got, EventTakenSchedule) { + t.Errorf("the keeper's events: %v", got) + } + for _, id := range []string{"apply", "events", "run"} { + r := byID[id] + if r["bus"] != nil || fmt.Sprint(r["env"]) != "map["+KeeperSocketEnv+":${dir:keeper-state}/keeper.sock]" { + t.Errorf("%s declares a bus or does not reach the keeper's socket: %v", id, r) + } + } + for _, id := range []string{"watch", "keeper-state"} { + if byID[id]["bus"] != nil { + t.Errorf("%s", id) + } + } + if byID["keeper-state"]["owner"] != "zfs-keeper" { + t.Errorf("the keeper's folder: %v", byID["keeper-state"]) + } + if fmt.Sprint(tree["invokes"]) != "[seat:node-service-manager.start]" { + t.Errorf("invokes: %v", tree["invokes"]) + } + m, _ := readManifest(t) + for _, tool := range []string{"zfs_config", "zfs_config_apply", "zfs_snapshots_schedule", "zfs_snapshots_run_now", "zfs_snapshots_prune_now", "zfs_snapshots_pause", + "zfs_snapshots_resume", "zfs_module_events", "zfs_snapshots_check", "zfs_findings", "zfs_snapshots_record"} { + found := false + for _, x := range m.Tools { + found = found || x == tool + } + if !found { + t.Errorf("%s is not in tools", tool) + } + } +} diff --git a/modules/zfs/module.json b/modules/zfs/module.json index 2f119f5ab..fd2b98256 100644 --- a/modules/zfs/module.json +++ b/modules/zfs/module.json @@ -5,9 +5,103 @@ "package-manager", "service-manager" ], - "emits": [ - "snapshot.taken", - "scrub.requested" + "invokes": [ + "seat:node-service-manager.start" + ], + "own-secrets": { + "broker-keeper": { + "path": "${dir:keeper-state}/broker-keeper.secret", + "owner": "zfs-keeper" + } + }, + "requires": [ + { + "name": "events", + "storage": "stream", + "template": "events", + "options": { + "events": [ + "config.applied", + "config.refused", + "config.waiting", + "snapshot.taken.person", + "snapshot.taken.schedule", + "snapshot.removed.person", + "snapshot.removed.schedule", + "snapshot.skipped.person", + "snapshot.skipped.schedule", + "run.skipped", + "scrub.requested" + ], + "max-age-days": 30, + "max-mib": 16 + } + }, + { + "name": "applied", + "storage": "key-value", + "template": "state" + }, + { + "name": "schedule", + "storage": "key-value", + "template": "state" + }, + { + "name": "findings", + "storage": "key-value", + "template": "state" + }, + { + "name": "snapshots", + "storage": "ledger", + "template": "ledger", + "options": { + "max-mib": 64 + }, + "types": [ + { + "name": "snapshot", + "schema": { + "dataset": { + "type": "string" + }, + "name": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "at": { + "type": "string" + }, + "taken": { + "type": "object" + } + }, + "operations": [ + { + "name": "taken", + "effect": "set-entry", + "into": "taken", + "key": "name", + "fields": [ + "dataset", + "name", + "kind", + "at" + ] + }, + { + "name": "removed", + "effect": "remove", + "into": "taken", + "key": "name" + } + ] + } + ] + } ], "config": { "pool": { @@ -260,6 +354,20 @@ "mode": "0755", "unknown": "program" }, + { + "id": "keeper-account", + "type": "user", + "name": "zfs-keeper", + "shell": "/usr/bin/nologin" + }, + { + "id": "keeper-state", + "type": "directory", + "mode": "0711", + "owner": "zfs-keeper", + "place": "mesh", + "unknown": "program" + }, { "id": "config-dir", "type": "directory", @@ -432,6 +540,48 @@ "zed=off" ] }, + { + "id": "keeper", + "type": "process", + "name": "zfs-keeper", + "artifact": "tools", + "run": [ + "./zfs-tools", + "keeper" + ], + "user": "zfs-keeper", + "env": { + "MESH_BROKER_FILE": "${dir:keeper-state}/broker-keeper.secret", + "ZFS_KEEPER_SOCKET": "${dir:keeper-state}/keeper.sock" + }, + "bus": { + "state": [ + "applied", + "schedule", + "findings" + ], + "emits": [ + "config.applied", + "config.refused", + "config.waiting", + "snapshot.taken.person", + "snapshot.taken.schedule", + "snapshot.removed.person", + "snapshot.removed.schedule", + "snapshot.skipped.person", + "snapshot.skipped.schedule", + "run.skipped" + ], + "invokes": [ + "zfs.zfs_snapshots_record", + "zfs.zfs_snapshots_schedule", + "seat:node-service-manager.start" + ] + }, + "health": { + "kind": "unit" + } + }, { "id": "apply", "type": "process", @@ -444,7 +594,10 @@ "run-once": true, "restart-on": [ "config" - ] + ], + "env": { + "ZFS_KEEPER_SOCKET": "${dir:keeper-state}/keeper.sock" + } }, { "id": "watch", @@ -476,6 +629,9 @@ ], "health": { "kind": "unit" + }, + "env": { + "ZFS_KEEPER_SOCKET": "${dir:keeper-state}/keeper.sock" } }, { @@ -487,7 +643,10 @@ "./zfs-tools", "run" ], - "schedule": "0,15,30,45 * * * *" + "schedule": "0,15,30,45 * * * *", + "env": { + "ZFS_KEEPER_SOCKET": "${dir:keeper-state}/keeper.sock" + } }, { "id": "distribution-weekly",