carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux && rootrunner
2
3package host
4
5import (
6 "context"
7 "encoding/json"
8 "errors"
9 "os"
10 "path/filepath"
11 "strings"
12 "testing"
13 "time"
14
15 "github.com/carlosframework/platform/internal/hostguard"
16 "github.com/carlosframework/platform/internal/hostops"
17 "github.com/carlosframework/platform/internal/rootparticipation"
18 "github.com/carlosframework/platform/internal/rootstate"
19)
20
21const migrationRebootDir = "/var/lib/plan14-p3-migration-reboot"
22
23func rebootMigration(t *testing.T) (rootMigration, *hostguard.RootAuthority) {
24 t.Helper()
25 open := func(name string) rootstate.Store {
26 s, e := rootstate.Open(filepath.Join(migrationRebootDir, name), true, false)
27 if e != nil {
28 t.Fatal(e)
29 }
30 t.Cleanup(func() { s.Close() })
31 return s
32 }
33 state, execution, fences := open("migration"), open("execution"), open("fences")
34 root := hostguard.NewRootAuthority(fences)
35 m := rootMigration{prepareFloors: func(context.Context) error { return nil }, guardReady: func(context.Context) error { return nil }, state: state, execution: execution, identity: rootMigrationIdentity{"deploy", "box", "en", 1}, quiesce: func(context.Context) (func(), error) { return func() {}, nil }}
36 m.observe = func(context.Context) (rootMigrationSnapshot, error) {
37 raw, e := state.Read("snapshot.json")
38 var s rootMigrationSnapshot
39 if e == nil {
40 e = json.Unmarshal(raw, &s)
41 }
42 return s, e
43 }
44 m.deny = func(ctx context.Context, item rootMigrationItem) error {
45 return root.RetainMigrationDeny(ctx, item.Installation, item.Session)
46 }
47 m.recover = func(ctx context.Context, item rootMigrationItem) error {
48 return verifyRootMigrationRetained(ctx, root, rootMigrationSnapshot{Items: []rootMigrationItem{item}})
49 }
50 m.ready = func(ctx context.Context, s rootMigrationSnapshot) error {
51 return verifyRootMigrationRetained(ctx, root, s)
52 }
53 m.publish = func(context.Context, hostops.CensusReport) error { return nil }
54 return m, root
55}
56func TestRootRebootPlan14MigrationPhase1(t *testing.T) {
57 if err := os.Mkdir(migrationRebootDir, 0700); err != nil {
58 t.Fatal(err)
59 }
60 m, root := rebootMigration(t)
61 _, s, _ := migrationFixture(t)
62 item := rootMigrationItem{Installation: hostguard.RootInstallation{Host: "a.example", Incarnation: "inc", Stamp: 1, Account: "bab", App: "app", Record: "record", Box: "box", Enrollment: "en", Migration: 1, InventoryRevision: 1, Artifacts: hostguard.InstallationArtifacts{Schema: 1}}, Session: 9, ClaimETag: "claim"}
63 s.Items = []rootMigrationItem{item}
64 if err := m.write("snapshot.json", s); err != nil {
65 t.Fatal(err)
66 }
67 // This phase-engine witness starts with a root-verified completed stop. The
68 // separate historic runner exercises its signed import and physical census.
69 if err := root.RetainMigrationDeny(t.Context(), item.Installation, item.Session); err != nil {
70 t.Fatal(err)
71 }
72 target := hostguard.FenceTarget{Host: "a.example", Incarnation: "inc", Stamp: 1, Session: 9, Box: "box", Enrollment: "en", BootID: "old", ControllerID: "controller", ClaimETag: "claim", DirectoryRevision: "7", FenceID: "fence"}
73 fences, err := rootstate.Open(filepath.Join(migrationRebootDir, "fences"), false, false)
74 if err != nil {
75 t.Fatal(err)
76 }
77 defer fences.Close()
78 raw, err := fences.Read("a.example.json")
79 if err != nil {
80 t.Fatal(err)
81 }
82 var record map[string]any
83 if json.Unmarshal(raw, &record) != nil {
84 t.Fatal("record")
85 }
86 // Encode the typed completed receipt as pre-existing authority for this crash fixture.
87 var state map[string]any
88 encoded, _ := json.Marshal(record["State"])
89 if json.Unmarshal(encoded, &state) != nil {
90 t.Fatal("state")
91 }
92 state["fence_journal"] = []hostguard.FenceReceipt{{Target: target, Bytes: []byte("root verified receipt"), EvidenceAt: time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)}}
93 record["State"] = state
94 raw, _ = json.Marshal(record)
95 if err := fences.Write("a.example.json", raw); err != nil {
96 t.Fatal(err)
97 }
98 boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
99 if err != nil {
100 t.Fatal(err)
101 }
102 if err := m.state.Write("boot", boot); err != nil {
103 t.Fatal(err)
104 }
105 cuts := 0
106 interrupted := errors.New("completion parent sync interrupted")
107 m.state = rootstate.WithWriteFault(m.state, func(name, stage string, op func() error) error {
108 if filepath.Base(name) == "migration.json" && stage == "parent-sync" {
109 cuts++
110 if cuts == 4 {
111 return interrupted
112 }
113 }
114 return op()
115 })
116 if err := m.run(t.Context()); !errors.Is(err, interrupted) {
117 t.Fatal("crash boundary", cuts, err)
118 }
119 if _, err := (rootparticipation.Ledger{State: m.execution}).Census(); err == nil {
120 t.Fatal("unsynced completion allowed grants")
121 }
122}
123func TestRootRebootPlan14MigrationPhase2(t *testing.T) {
124 if _, err := os.Stat(migrationRebootDir); err != nil {
125 t.Fatal(err)
126 }
127 defer os.RemoveAll(migrationRebootDir)
128 m, root := rebootMigration(t)
129 before, err := m.state.Read("boot")
130 if err != nil {
131 t.Fatal(err)
132 }
133 now, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
134 if err != nil || strings.TrimSpace(string(before)) == strings.TrimSpace(string(now)) {
135 t.Fatal("no real reboot", err)
136 }
137 if err := m.run(t.Context()); err != nil {
138 t.Fatal("reboot resume", err)
139 }
140 if err := verifyRootMigrationState(t.Context(), m.state, m.execution, m.identity, root); err != nil {
141 t.Fatal("startup verification", err)
142 }
143 v, err := root.View(t.Context(), "a.example", hostguard.Captured{Incarnation: "inc", Stamp: 1})
144 if err != nil || len(v.MigrationDenies) != 1 || v.MigrationDenies[0].Session != 9 || len(v.Receipts) != 1 || string(v.Receipts[0].Bytes) != "root verified receipt" {
145 t.Fatal("reboot lost evidence", v, err)
146 }
147 if err := m.run(t.Context()); err != nil {
148 t.Fatal("completed reboot retry", err)
149 }
150}
151