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 "bytes"
7 "context"
8 "encoding/base64"
9 "encoding/json"
10 "fmt"
11 "os"
12 "os/exec"
13 "path/filepath"
14 "strings"
15 "testing"
16
17 "github.com/carlosframework/platform/internal/hostclaim"
18 "github.com/carlosframework/platform/internal/hostguard"
19 "github.com/carlosframework/platform/internal/registry"
20 "github.com/carlosframework/platform/internal/release"
21)
22
23const migrationInstalledDir = "/var/lib/plan14-p3-migration-installed"
24const migrationInstalledUnit = "plan14-p3-migration-installed.service"
25
26type migrationInstalledFixture struct {
27 Timer registry.Timer
28 Registry, TimerRegistration string
29 Data, Opt, Units, Scratch, PublicKey, Boot string
30 Target hostguard.FenceTarget
31 Receipt []byte
32}
33
34func saveMigrationInstalled(t *testing.T, store release.Store, r ClaimRuntimeResources, scratch string, target hostguard.FenceTarget, receipt []byte) {
35 t.Helper()
36 if err := os.Mkdir(migrationInstalledDir, 0700); err != nil {
37 t.Fatal(err)
38 }
39 saved := release.NewDirStore(filepath.Join(migrationInstalledDir, "store"))
40 keys, err := store.List(t.Context(), "")
41 if err != nil {
42 t.Fatal(err)
43 }
44 for _, key := range keys {
45 raw, _, err := store.Get(t.Context(), key)
46 if err != nil {
47 t.Fatal(err)
48 }
49 if _, err = saved.Put(t.Context(), key, raw, ""); err != nil {
50 t.Fatal(err)
51 }
52 }
53 boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
54 if err != nil {
55 t.Fatal(err)
56 }
57 timer, err := r.Registry.GetTimer(target.Host, "refresh")
58 if err != nil || timer == nil || !timer.ClaimOwnership.Valid() {
59 t.Fatal("unadopted reboot timer", timer, err)
60 }
61 f := migrationInstalledFixture{Data: r.DataDir, Opt: r.OptDir, Units: r.UnitsDir, Scratch: scratch, PublicKey: os.Getenv("CARLOS_RELEASE_PUBKEY"), Boot: string(boot), Registry: r.Registry.Path(), TimerRegistration: timer.Registration, Timer: *timer, Target: target, Receipt: receipt}
62 if err := r.Registry.Close(); err != nil {
63 t.Fatal(err)
64 }
65 registryBytes, err := os.ReadFile(f.Registry)
66 if err != nil {
67 t.Fatal(err)
68 }
69 f.Registry = filepath.Join(migrationInstalledDir, "registry.db")
70 if err := writeFileDurable(f.Registry, registryBytes, 0600); err != nil {
71 t.Fatal(err)
72 }
73 raw, err := json.Marshal(f)
74 if err != nil {
75 t.Fatal(err)
76 }
77 if err := writeFileDurable(filepath.Join(migrationInstalledDir, "fixture.json"), raw, 0600); err != nil {
78 t.Fatal(err)
79 }
80 binary, err := os.ReadFile(os.Args[0])
81 if err != nil {
82 t.Fatal(err)
83 }
84 if err := writeFileDurable(filepath.Join(migrationInstalledDir, "runner.test"), binary, 0755); err != nil {
85 t.Fatal(err)
86 }
87 // Deliberately retain this test's root state and scratch across the real reboot.
88 // Phase 2 removes the exact saved paths. No testing cleanup may erase them now.
89 os.Exit(0)
90}
91
92func installedMigrationFixture(t *testing.T) migrationInstalledFixture {
93 t.Helper()
94 raw, err := os.ReadFile(filepath.Join(migrationInstalledDir, "fixture.json"))
95 if err != nil {
96 t.Fatal(err)
97 }
98 var f migrationInstalledFixture
99 if json.Unmarshal(raw, &f) != nil {
100 t.Fatal("fixture")
101 }
102 return f
103}
104func migrationInstalledCtl(t *testing.T, args ...string) {
105 t.Helper()
106 out, err := exec.CommandContext(t.Context(), "systemctl", args...).CombinedOutput()
107 if err != nil {
108 logs, _ := exec.Command("journalctl", "-b", "-u", migrationInstalledUnit, "--no-pager").CombinedOutput()
109 t.Fatal(args, err, string(out), string(logs))
110 }
111}
112func TestRootRebootPlan14MigrationInstalledPhase1(t *testing.T) {
113 if os.Getenv("PLAN14_P3_BOOTSTRAP_CHILD") == "" {
114 t.Run("unknown crashing target recovery", TestPlan14RootFloorUnknownCrashRecoveryRootRunner)
115 }
116 testRootMigrationBootstrap(t, true, false)
117 f := installedMigrationFixture(t)
118 raw, err := unitsFS.ReadFile("units/" + claimControlUnit)
119 if err != nil {
120 t.Fatal(err)
121 }
122 content := claimUnitContent(claimControlUnit, raw, claimUnitSettings{enabled: true, data: f.Data, opt: f.Opt})
123 var lines []string
124 for _, line := range strings.Split(string(content), "\n") {
125 if strings.HasPrefix(line, "Requires=") || strings.HasPrefix(line, "After=") || strings.HasPrefix(line, "ExecStart=") || strings.HasPrefix(line, "EnvironmentFile=") {
126 continue
127 }
128 lines = append(lines, line)
129 }
130 content = []byte(strings.Join(lines, "\n") + fmt.Sprintf("\nType=oneshot\nRuntimeDirectory=carlos carlos-control carlos/hostguard\nReadWritePaths=%s\nTimeoutStartSec=120s\nExecStart=%s/runner.test -test.run=^TestPlan14MigrationInstalledStartupPayload$ -test.v -test.timeout=90s\nIPAddressDeny=any\n[Install]\nWantedBy=multi-user.target\n", migrationInstalledDir, migrationInstalledDir))
131 path := filepath.Join("/etc/systemd/system", migrationInstalledUnit)
132 fd, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
133 if err != nil {
134 t.Fatal(err)
135 }
136 if _, err := fd.Write(content); err != nil {
137 t.Fatal(err)
138 }
139 if err := fd.Close(); err != nil {
140 t.Fatal(err)
141 }
142 migrationInstalledCtl(t, "daemon-reload")
143 migrationInstalledCtl(t, "enable", migrationInstalledUnit)
144 migrationInstalledCtl(t, "start", migrationInstalledUnit)
145}
146func TestRootRebootPlan14MigrationInstalledPhase2(t *testing.T) {
147 f := installedMigrationFixture(t)
148 defer func() {
149 migrationInstalledCtl(t, "disable", "--now", migrationInstalledUnit)
150 for _, path := range []string{filepath.Join("/etc/systemd/system", migrationInstalledUnit), f.Scratch, "/var/lib/carlos-claims", "/var/lib/carlos-release", migrationInstalledDir} {
151 if err := os.RemoveAll(path); err != nil {
152 t.Error(err)
153 }
154 }
155 migrationInstalledCtl(t, "daemon-reload")
156 }()
157 boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
158 if err != nil || string(boot) == f.Boot {
159 t.Fatal("runner did not reboot", err)
160 }
161 // The enabled installed unit must have started during this boot, before phase 2.
162 observed, err := os.ReadFile(filepath.Join(migrationInstalledDir, "started-boot"))
163 if err != nil || !bytes.Equal(observed, boot) {
164 out, _ := exec.Command("journalctl", "-b", "-u", migrationInstalledUnit, "--no-pager").CombinedOutput()
165 t.Fatal("installed startup", err, string(observed), string(out))
166 }
167 migrationInstalledCtl(t, "start", migrationInstalledUnit)
168 path := filepath.Join(rootMigrationDir, "migration.json")
169 original, err := os.ReadFile(path)
170 if err != nil {
171 t.Fatal(err)
172 }
173 var pending rootMigrationRecord
174 if json.Unmarshal(original, &pending) != nil {
175 t.Fatal("saved migration")
176 }
177 pending.Phase = "prepared"
178 raw, _ := json.Marshal(pending)
179 if err := writeFileDurable(path, raw, 0600); err != nil {
180 t.Fatal(err)
181 }
182 if out, err := exec.CommandContext(t.Context(), "systemctl", "start", migrationInstalledUnit).CombinedOutput(); err == nil {
183 t.Fatal("installed startup ignored incomplete migration", string(out))
184 }
185 if err := writeFileDurable(path, original, 0600); err != nil {
186 t.Fatal(err)
187 }
188 migrationInstalledCtl(t, "reset-failed", migrationInstalledUnit)
189 migrationInstalledCtl(t, "start", migrationInstalledUnit)
190}
191func TestPlan14MigrationInstalledStartupPayload(t *testing.T) {
192 f := installedMigrationFixture(t)
193 t.Setenv("CARLOS_OPT_DIR", f.Opt)
194 t.Setenv("CARLOS_DATA_DIR", f.Data)
195 t.Setenv("CARLOS_RELEASE_PUBKEY", f.PublicKey)
196 t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "deployment")
197 // External actor/store services are deterministic fixtures. The constructor,
198 // root openers, migration verification and retained journals are production.
199 authority, journal := openClaimRootAuthority, openClaimRootJournal
200 _, _, owners, _ := fix4JournalFixture(t)
201 openClaimRootAuthority, openClaimRootJournal = authority, journal
202 store := release.NewDirStore(filepath.Join(migrationInstalledDir, "store"))
203 owners.RetirementAuthority.Store = store
204 owners.RetirementAuthority.Directory.Store = store
205 pub, err := base64.StdEncoding.DecodeString(f.PublicKey)
206 if err != nil {
207 t.Fatal(err)
208 }
209 owners.RetirementAuthority.Directory.PublicKey = pub
210 reg, err := registry.Open(f.Registry)
211 if err != nil {
212 t.Fatal(err)
213 }
214 defer reg.Close()
215 r := ClaimRuntimeResources{Mode: "instance-control", Registry: reg, DataDir: f.Data, OptDir: f.Opt, UnitsDir: f.Units}
216 assembly, err := OpenProductionClaimAssembly(t.Context(), store, r, owners)
217 if err != nil {
218 t.Fatal("installed production constructor", err)
219 }
220 defer assembly.Close(context.Background())
221 receipt, err := assembly.Guard.FenceReceiptFor(t.Context(), f.Target)
222 if err != nil || !bytes.Equal(receipt.Bytes, f.Receipt) {
223 t.Fatal("reboot changed receipt", err)
224 }
225 captured := hostguard.Captured{Incarnation: f.Target.Incarnation, Stamp: f.Target.Stamp}
226 timer, err := reg.GetTimer(f.Target.Host, "refresh")
227 expectedTimer := f.Timer
228 expectedTimer.Registration = f.TimerRegistration
229 if err != nil || *timer != expectedTimer || timer.ClaimRowID != f.TimerRegistration || timer.ClaimIncarnation != f.Target.Incarnation || timer.ClaimStamp != f.Target.Stamp {
230 t.Fatal("reboot timer identity", timer, err)
231 }
232 view, err := assembly.Guard.Root.Installation(t.Context(), f.Target.Host, captured)
233 if err != nil || view.Artifacts.Timers != "refresh" || view.InventoryRevision != 2 {
234 t.Fatal("reboot root timer inventory", view, err)
235 }
236 launches := 0
237 launch := func() error { launches++; return exec.Command("/usr/bin/true").Run() }
238 allow := func(*hostclaim.Claim) error { return nil }
239 if err := assembly.Guard.With(t.Context(), f.Target.Host, captured, allow, launch); err != nil || launches != 1 {
240 t.Fatal("installed successor", err, launches)
241 }
242 // Replay of the old claim session is still refused after reboot and startup.
243 current, err := hostclaim.Read(t.Context(), store, f.Target.Host)
244 if err != nil {
245 t.Fatal(err)
246 }
247 old := *current.Claim
248 holder := *old.Holder
249 holder.Session = f.Target.Session
250 old.Holder = &holder
251 denied := assembly.Guard
252 denied.Claims = rootViewClaimReader{obs: hostclaim.Observed{Host: old.Host, Claim: &old, ETag: current.ETag}}
253 if err := denied.With(t.Context(), f.Target.Host, captured, allow, launch); err == nil || launches != 1 {
254 t.Fatal("old session after reboot", err, launches)
255 }
256 boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
257 if err != nil {
258 t.Fatal(err)
259 }
260 if err := writeFileDurable(filepath.Join(migrationInstalledDir, "started-boot"), boot, 0600); err != nil {
261 t.Fatal(err)
262 }
263}
264