carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostbox
2
3import (
4 "context"
5 "database/sql"
6 "encoding/json"
7
8 "net/url"
9 "os"
10 "path/filepath"
11
12 "github.com/carlosframework/platform/internal/hostguard"
13 _ "modernc.org/sqlite"
14)
15
16type LaunchFile struct{ Device, Inode uint64 }
17type LaunchGroup struct {
18 RelativePath string
19 Device, Inode uint64
20 InvocationID string
21}
22type LaunchReservation struct {
23 NeverIntended bool `json:",omitempty"`
24 ID, Role, InvocationID, Inputs, State, ProcessStart string
25 Target hostguard.FenceTarget
26 Group LaunchGroup
27 Parent LaunchGroup
28 Executable LaunchFile
29 PID int
30}
31type LaunchInventory struct {
32 WritersClosed bool `json:"writers_closed,omitempty"`
33 Completed ProducerCompletion `json:",omitzero"`
34 ControllerPID int
35 ProcessStart string
36 Target hostguard.FenceTarget
37 Groups []LaunchGroup
38 Files []LaunchFile
39 ProducerIDs []string
40 NoState bool
41 Reservations []LaunchReservation
42}
43type LaunchJournal struct {
44 db *sql.DB
45 remote JournalCall
46 path string
47 identity os.FileInfo
48}
49
50func OpenLaunchJournal(path string) (*LaunchJournal, error) {
51 if !filepath.IsAbs(path) {
52 return nil, ErrUnsupported
53 }
54 if err := checkJournalPath(path); err != nil {
55 return nil, err
56 }
57 db, err := sql.Open("sqlite", "file:"+url.PathEscape(path)+"?_pragma=busy_timeout(5000)&_pragma=synchronous(FULL)")
58 if err != nil {
59 return nil, err
60 }
61 db.SetMaxOpenConns(1)
62 for _, q := range []string{"PRAGMA busy_timeout=5000", "PRAGMA journal_mode=WAL", "PRAGMA synchronous=FULL", "CREATE TABLE IF NOT EXISTS launches (identity TEXT PRIMARY KEY, body BLOB NOT NULL)"} {
63 if _, err = db.Exec(q); err != nil {
64 db.Close()
65 return nil, err
66 }
67 }
68 identity, err := os.Lstat(path)
69 if err != nil {
70 db.Close()
71 return nil, err
72 }
73 return &LaunchJournal{db: db, path: path, identity: identity}, nil
74}
75func (j *LaunchJournal) Close() error {
76 if j == nil {
77 return nil
78 }
79 if j.remote != nil {
80 return nil
81 }
82 return j.db.Close()
83}
84func launchKey(t hostguard.FenceTarget) string {
85 raw, _ := json.Marshal([]any{t.Host, t.Incarnation, t.Stamp, t.Session, t.Box, t.Enrollment, t.BootID, t.ControllerID})
86 return string(raw)
87}
88func SameLaunchOwner(a, b hostguard.FenceTarget) bool {
89 return a.Host == b.Host && a.Incarnation == b.Incarnation && a.Stamp == b.Stamp && a.Session == b.Session && a.Box == b.Box && a.Enrollment == b.Enrollment && a.BootID == b.BootID && a.ControllerID == b.ControllerID
90}
91func validLaunchOwner(t hostguard.FenceTarget) bool {
92 return t.Host != "" && t.Incarnation != "" && t.Stamp > 0 && t.Session > 0 && t.Box != "" && t.Enrollment != "" && t.BootID != "" && t.ControllerID != ""
93}
94func (j *LaunchJournal) Create(ctx context.Context, in LaunchInventory) error {
95 if j == nil {
96 return ErrUnsupported
97 }
98 if j.remote != nil {
99 out, err := journalCall(ctx, j.remote, JournalRequest{Op: "create", Launch: in})
100 _ = out
101 return err
102 }
103
104 if err := journalIdentity(j.path, j.identity); err != nil {
105 return err
106 }
107 if in.WritersClosed || in.Completed != (ProducerCompletion{}) || !validLaunchOwner(in.Target) || in.ControllerPID <= 0 || in.ProcessStart == "" || len(in.Groups) == 0 || (len(in.Files) == 0 && !in.NoState) || len(in.Reservations) != 0 {
108 return ErrUnsupported
109 }
110 body, err := json.Marshal(in)
111 if err != nil {
112 return err
113 }
114 _, err = journalExecutorFor(ctx, j.db).ExecContext(ctx, "INSERT INTO launches(identity,body) VALUES(?,?)", launchKey(in.Target), body)
115 return err
116}
117
118// Prelaunch persists an admitted session's recoverable record before restore
119// or materialization. It names this controller and the session's empty parent
120// group only. State directories are inventoried before restore; file identities
121// and producer reservations are added later. A failure or crash from here on
122// leaves the exact tuple root stops and settles.
123func (j *LaunchJournal) Prelaunch(ctx context.Context, in LaunchInventory) error {
124 if j == nil {
125 return ErrUnsupported
126 }
127 if j.remote != nil {
128 out, err := journalCall(ctx, j.remote, JournalRequest{Op: "prelaunch", Launch: in})
129 _ = out
130 return err
131 }
132
133 if len(in.Groups) != 1 || in.Groups[0].RelativePath == "" || in.Groups[0].Device == 0 || in.Groups[0].Inode == 0 || in.Groups[0].InvocationID != "" || len(in.Files) != 0 || !in.NoState || len(in.ProducerIDs) != 0 {
134 return ErrUnsupported
135 }
136 in.ProducerIDs = []string{}
137 return j.Create(ctx, in)
138}
139
140// SessionLaunches returns every record journaled for one writer session of
141// this box, whichever boot or controller wrote it. A restarted controller uses
142// it to name the original tuple. An empty result never proves emptiness.
143func (j *LaunchJournal) SessionLaunches(ctx context.Context, host, incarnation string, stamp, session uint64, box, enrollment string) ([]LaunchInventory, error) {
144 if j == nil {
145 return nil, ErrUnsupported
146 }
147 if j.remote != nil {
148 out, err := journalCall(ctx, j.remote, JournalRequest{Op: "session", Target: hostguard.FenceTarget{Host: host, Incarnation: incarnation, Stamp: stamp, Session: session, Box: box, Enrollment: enrollment}})
149 return out.Launches, err
150 }
151
152 if j == nil || j.db == nil || !component(host) || incarnation == "" || stamp == 0 || session == 0 || box == "" || enrollment == "" {
153 return nil, ErrUnsupported
154 }
155 rows, err := journalExecutorFor(ctx, j.db).QueryContext(ctx, "SELECT identity, body FROM launches")
156 if err != nil {
157 return nil, err
158 }
159 defer rows.Close()
160 var out []LaunchInventory
161 for rows.Next() {
162 var key string
163 var body []byte
164 if err = rows.Scan(&key, &body); err != nil {
165 return nil, err
166 }
167 var in LaunchInventory
168 if err = json.Unmarshal(body, &in); err != nil {
169 return nil, err
170 }
171 if !validLaunchOwner(in.Target) || launchKey(in.Target) != key {
172 return nil, ErrTypedInventory
173 }
174 t := in.Target
175 if t.Host == host && t.Incarnation == incarnation && t.Stamp == stamp && t.Session == session && t.Box == box && t.Enrollment == enrollment {
176 out = append(out, in)
177 }
178 }
179 return out, rows.Err()
180}
181func (j *LaunchJournal) Read(ctx context.Context, t hostguard.FenceTarget) (LaunchInventory, error) {
182 if j == nil {
183 return LaunchInventory{}, ErrUnsupported
184 }
185 if j.remote != nil {
186 out, err := journalCall(ctx, j.remote, JournalRequest{Op: "read", Target: t})
187 return out.Launch, err
188 }
189
190 var in LaunchInventory
191 var raw []byte
192 if j == nil || j.db == nil || !validLaunchOwner(t) {
193 return in, ErrUnsupported
194 }
195 if err := journalExecutorFor(ctx, j.db).QueryRowContext(ctx, "SELECT body FROM launches WHERE identity=?", launchKey(t)).Scan(&raw); err != nil {
196 return in, err
197 }
198 if err := json.Unmarshal(raw, &in); err != nil {
199 return in, err
200 }
201 if !SameLaunchOwner(in.Target, t) {
202 return in, ErrChanged
203 }
204 return in, nil
205}
206func (j *LaunchJournal) mutate(ctx context.Context, t hostguard.FenceTarget, fn func(*LaunchInventory) error) error {
207 if j == nil || j.db == nil || !validLaunchOwner(t) {
208 return ErrUnsupported
209 }
210 tx, commit, rollback, err := journalTransaction(ctx, j.db)
211 if err != nil {
212 return err
213 }
214 defer rollback()
215 var raw []byte
216 if err = tx.QueryRowContext(ctx, "SELECT body FROM launches WHERE identity=?", launchKey(t)).Scan(&raw); err != nil {
217 return err
218 }
219 var in LaunchInventory
220 if err = json.Unmarshal(raw, &in); err != nil {
221 return err
222 }
223 if !SameLaunchOwner(in.Target, t) {
224 return ErrChanged
225 }
226 if err = fn(&in); err != nil {
227 return err
228 }
229 next, err := json.Marshal(in)
230 if err != nil {
231 return err
232 }
233 res, err := tx.ExecContext(ctx, "UPDATE launches SET body=? WHERE identity=? AND body=?", next, launchKey(t), raw)
234 if err != nil {
235 return err
236 }
237 n, err := res.RowsAffected()
238 if err != nil {
239 return err
240 }
241 if n != 1 {
242 return ErrChanged
243 }
244 return commit()
245}
246func (j *LaunchJournal) Reserve(ctx context.Context, r LaunchReservation) error {
247 if j == nil {
248 return ErrUnsupported
249 }
250 if j.remote != nil {
251 out, err := journalCall(ctx, j.remote, JournalRequest{Op: "reserve", Reservation: r})
252 _ = out
253 return err
254 }
255
256 if r.NeverIntended || r.ID == "" || r.Role == "" || r.InvocationID == "" || r.Inputs == "" || r.Group.RelativePath == "" || r.Group.Device == 0 || r.Group.Inode == 0 || r.Executable.Device == 0 || r.Executable.Inode == 0 || r.State != "" || r.PID != 0 || r.Group.InvocationID != r.InvocationID {
257 return ErrUnsupported
258 }
259 return j.mutate(ctx, r.Target, func(in *LaunchInventory) error {
260 found := -1
261 for i, old := range in.Reservations {
262 if old.ID == r.ID || old.Group.RelativePath == r.Group.RelativePath {
263 expected := r
264 expected.State = "intended"
265 expected.Group.Device, expected.Group.Inode = 0, 0
266 if old != expected {
267 return ErrChanged
268 }
269 found = i
270 }
271 }
272 if r.Parent.RelativePath != "" && found < 0 {
273 return ErrChanged
274 }
275 r.State = "reserved"
276 if found >= 0 {
277 in.Reservations[found] = r
278 } else {
279 in.Reservations = append(in.Reservations, r)
280 }
281 in.Groups = append(in.Groups, r.Group)
282 return nil
283 })
284}
285func (j *LaunchJournal) Started(ctx context.Context, r LaunchReservation, pid int, processStart string) error {
286 if j == nil {
287 return ErrUnsupported
288 }
289 if j.remote != nil {
290 out, err := journalCall(ctx, j.remote, JournalRequest{Op: "started", Reservation: r, PID: pid, ProcessStart: processStart})
291 _ = out
292 return err
293 }
294
295 if pid <= 0 || processStart == "" {
296 return ErrUnsupported
297 }
298 return j.mutate(ctx, r.Target, func(in *LaunchInventory) error {
299 for i := range in.Reservations {
300 old := &in.Reservations[i]
301 expected := r
302 expected.State = "reserved"
303 if *old == expected {
304 old.State = "started"
305 old.PID = pid
306 old.ProcessStart = processStart
307 return nil
308 }
309 }
310 return ErrChanged
311 })
312}
313func (j *LaunchJournal) Settled(ctx context.Context, r LaunchReservation) error {
314 if j == nil {
315 return ErrUnsupported
316 }
317 if j.remote != nil {
318 out, err := journalCall(ctx, j.remote, JournalRequest{Op: "settled", Reservation: r})
319 _ = out
320 return err
321 }
322
323 return j.mutate(ctx, r.Target, func(in *LaunchInventory) error {
324 for i := range in.Reservations {
325 old := &in.Reservations[i]
326 expected := r
327 expected.State = old.State
328 expected.PID = old.PID
329 expected.ProcessStart = old.ProcessStart
330 if *old == expected && (old.State == "reserved" || old.State == "started" || old.State == "settled") {
331 old.State = "settled"
332 return nil
333 }
334 }
335 return ErrChanged
336 })
337}
338func (j *LaunchJournal) RecordFile(ctx context.Context, t hostguard.FenceTarget, f LaunchFile) error {
339 if j == nil {
340 return ErrUnsupported
341 }
342 if j.remote != nil {
343 out, err := journalCall(ctx, j.remote, JournalRequest{Op: "file", Target: t, File: f})
344 _ = out
345 return err
346 }
347
348 if f.Device == 0 || f.Inode == 0 {
349 return ErrUnsupported
350 }
351 return j.mutate(ctx, t, func(in *LaunchInventory) error {
352 for _, old := range in.Files {
353 if old == f {
354 return nil
355 }
356 }
357 in.NoState = false
358 in.Files = append(in.Files, f)
359 return nil
360 })
361}
362
363func (j *LaunchJournal) Remote() bool { return j != nil && j.remote != nil }
364