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 "net"
10 "os"
11 "os/exec"
12 "path/filepath"
13 "strings"
14 "testing"
15 "time"
16
17 "github.com/carlosframework/platform/internal/rootstate"
18 "golang.org/x/sys/unix"
19)
20
21const unitMigrationRebootDir = "/var/lib/carlos-e52-migration"
22const migrationRebootUnit = "plan14-e52-migration.service"
23const migrationRebootSocket = "/run/carlos/plan14-e52.sock"
24
25func migrationRebootMaterial(t *testing.T) claimUnitMaterializer {
26 t.Helper()
27 store, err := rootstate.Open(filepath.Join(unitMigrationRebootDir, "state"), true, false)
28 if err != nil {
29 t.Fatal(err)
30 }
31 t.Cleanup(func() { store.Close() })
32 files := claimUnitFiles{dir: "/etc/systemd/system"}
33 k := claimPredecessorKernel{ctl: rebootPredecessorCtl, group: claimPredecessorGroup, boot: claimUnitBoot}
34 return claimUnitMaterializer{state: store, read: files.read, create: files.create, replace: files.replace, predecessor: k.capture, stopPredecessor: k.stop,
35 socketCapture: captureProductionClaimUnitSocket, socketPark: parkProductionClaimUnitSocket, idle: func(string) error { return nil }}
36}
37
38func TestRootRebootClaimUnitMigrationPhase1(t *testing.T) {
39 for _, dir := range []string{unitMigrationRebootDir, "/run/carlos", "/run/carlos-control"} {
40 if err := os.Mkdir(dir, 0755); err != nil {
41 t.Fatal(err)
42 }
43 }
44 binary, err := os.Executable()
45 if err != nil {
46 t.Fatal(err)
47 }
48 body, err := os.ReadFile(binary)
49 if err != nil {
50 t.Fatal(err)
51 }
52 if err := os.WriteFile(filepath.Join(unitMigrationRebootDir, "payload"), body, 0755); err != nil {
53 t.Fatal(err)
54 }
55 legacy := []byte("[Service]\nType=simple\nEnvironment=PLAN14_TENANT_SOCKET=" + migrationRebootSocket + "\nExecStart=" + unitMigrationRebootDir + "/payload plan14-tenant\n")
56 path := "/etc/systemd/system/" + migrationRebootUnit
57 f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
58 if err != nil {
59 t.Fatal(err)
60 }
61 _, err = f.Write(legacy)
62 if err = errors.Join(err, f.Close()); err != nil {
63 t.Fatal(err)
64 }
65 if out, err := rebootPredecessorCtl(t.Context(), "daemon-reload"); err != nil {
66 t.Fatal(out, err)
67 }
68 if out, err := rebootPredecessorCtl(t.Context(), "start", migrationRebootUnit); err != nil {
69 t.Fatal(out, err)
70 }
71 for until := time.Now().Add(5 * time.Second); ; {
72 conn, err := net.DialTimeout("unix", migrationRebootSocket, time.Second)
73 if err == nil {
74 conn.Close()
75 break
76 }
77 if time.Now().After(until) {
78 t.Fatal(err)
79 }
80 time.Sleep(20 * time.Millisecond)
81 }
82 _, in, s := plan14UnitBindingFixture(t, migrationRebootUnit, "", migrationRebootSocket)
83 s.Program, s.PredecessorDigest = strings.Repeat("a", 64), claimUnitDigest(legacy)
84 b := claimUnitBinding{Schema: 1, Owner: unitBindingOwner(in), Shape: s}
85 m := migrationRebootMaterial(t)
86 m.stopPredecessor = func(context.Context, claimUnitPredecessor) error { return os.ErrClosed }
87 if err := m.install(t.Context(), b); !errors.Is(err, os.ErrClosed) {
88 t.Fatal("migration intent boundary", err)
89 }
90 r, err := m.load(b.Owner.Host)
91 if err != nil || r.Migration == nil || r.Migration.Socket.Boot == "" || r.Identity != (rootstate.FileIdentity{}) {
92 t.Fatal("lost durable migration", r, err)
93 }
94 raw, _ := json.Marshal(b)
95 if err := os.WriteFile(filepath.Join(unitMigrationRebootDir, "binding.json"), raw, 0600); err != nil {
96 t.Fatal(err)
97 }
98 old := claimEdgeMaskOwner
99 defer func() { claimEdgeMaskOwner = old }()
100 claimEdgeMaskOwner = filepath.Join(unitMigrationRebootDir, "edge-mask")
101 if _, err := createClaimEdgeMask(t.Context(), r.Migration.Socket.Boot); err != nil {
102 t.Fatal(err)
103 }
104 t.Logf("migration saved boot=%s device=%d mount=%d socket=%d:%d", r.Migration.Definition.Boot, r.Migration.Definition.Device, r.Migration.Definition.Mount, r.Migration.Socket.Device, r.Migration.Socket.Inode)
105}
106
107func TestRootRebootClaimUnitMigrationPhase2(t *testing.T) {
108 t.Cleanup(func() {
109 rebootPredecessorCtl(context.Background(), "stop", migrationRebootUnit)
110 os.Remove("/etc/systemd/system/" + migrationRebootUnit)
111 os.Remove("/run/systemd/system/carlos-edge.service")
112 rebootPredecessorCtl(context.Background(), "daemon-reload")
113 for _, dir := range []string{unitMigrationRebootDir, "/run/carlos", "/run/carlos-control"} {
114 os.RemoveAll(dir)
115 }
116 })
117 for _, dir := range []string{"/run/carlos", "/run/carlos-control"} {
118 if err := os.Mkdir(dir, 0755); err != nil {
119 t.Fatal(err)
120 }
121 }
122 var b claimUnitBinding
123 raw, err := os.ReadFile(filepath.Join(unitMigrationRebootDir, "binding.json"))
124 if err != nil || json.Unmarshal(raw, &b) != nil {
125 t.Fatal(err)
126 }
127 m := migrationRebootMaterial(t)
128 r, err := m.load(b.Owner.Host)
129 if err != nil {
130 t.Fatal(err)
131 }
132 boot, err := claimUnitBoot()
133 if err != nil || boot == r.Migration.Definition.Boot {
134 t.Fatal("actual reboot required", err)
135 }
136 oldMaskOwner := claimEdgeMaskOwner
137 defer func() { claimEdgeMaskOwner = oldMaskOwner }()
138 claimEdgeMaskOwner = filepath.Join(unitMigrationRebootDir, "edge-mask")
139 if err := os.Symlink("/dev/null", "/run/systemd/system/carlos-edge.service"); err != nil {
140 t.Fatal(err)
141 }
142 if out, err := rebootPredecessorCtl(t.Context(), "daemon-reload"); err != nil {
143 t.Fatal(out, err)
144 }
145 if err := recoverLegacyClaimEdgeMask(t.Context()); err != nil {
146 t.Fatal(err)
147 }
148 if target, err := os.Readlink("/run/systemd/system/carlos-edge.service"); err != nil || target != "/dev/null" {
149 t.Fatal("old boot removed replacement mask", err)
150 }
151 if _, err := os.Lstat(claimEdgeMaskOwner); !os.IsNotExist(err) {
152 t.Fatal("old mask record retained", err)
153 }
154 path := "/etc/systemd/system/" + migrationRebootUnit
155 if err := unix.Mount(path, path, "", unix.MS_BIND, ""); err != nil {
156 t.Fatal(err)
157 }
158 refused := m.install(t.Context(), b)
159 if err := unix.Unmount(path, 0); err != nil {
160 t.Fatal(err)
161 }
162 if refused == nil {
163 t.Fatal("substituted definition mount accepted")
164 }
165 listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: migrationRebootSocket, Net: "unix"})
166 if err != nil {
167 t.Fatal(err)
168 }
169 if err = m.install(t.Context(), b); err == nil {
170 t.Fatal("post-boot replacement socket removed")
171 }
172 if _, err := os.Lstat(migrationRebootSocket); err != nil {
173 t.Fatal("replacement socket lost", err)
174 }
175 listener.Close()
176 if err := m.install(t.Context(), b); err != nil {
177 t.Fatal("resume migration after reboot", err)
178 }
179 if err := m.verify(b); err != nil {
180 t.Fatal(err)
181 }
182 if err := projectClaimBoundSocket(b.Shape); err != nil {
183 t.Fatal(err)
184 }
185 if target, err := os.Readlink(migrationRebootSocket); err != nil || target != claimSocketTarget(b.Shape) {
186 t.Fatal("socket projection", target, err)
187 }
188 if out, err := exec.Command("systemctl", "daemon-reload").CombinedOutput(); err != nil {
189 t.Fatal(string(out), err)
190 }
191 if _, err := rebootPredecessorCtl(t.Context(), "start", migrationRebootUnit); err == nil {
192 time.Sleep(100 * time.Millisecond)
193 pid, _ := rebootPredecessorCtl(t.Context(), "show", "-p", "MainPID", "--value", migrationRebootUnit)
194 if pid != "0" {
195 t.Fatal("migration started without reservation", pid)
196 }
197 }
198}
199