carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "crypto/ed25519"
6 "crypto/rand"
7 "crypto/sha256"
8 "encoding/hex"
9 "encoding/json"
10 "errors"
11 "github.com/carlosframework/platform/internal/genselect"
12 "log"
13 "os"
14 "path/filepath"
15 "strings"
16 "sync"
17 "time"
18
19 "github.com/carlosframework/platform/internal/activator"
20 "github.com/carlosframework/platform/internal/cgroup"
21 "github.com/carlosframework/platform/internal/controlsock"
22 "github.com/carlosframework/platform/internal/hostbox"
23 "github.com/carlosframework/platform/internal/hostclaim"
24 "github.com/carlosframework/platform/internal/hostfence"
25 "github.com/carlosframework/platform/internal/hostguard"
26 "github.com/carlosframework/platform/internal/hostops"
27 "github.com/carlosframework/platform/internal/hostwrite"
28 "github.com/carlosframework/platform/internal/lease"
29 "github.com/carlosframework/platform/internal/park"
30 "github.com/carlosframework/platform/internal/registry"
31 "github.com/carlosframework/platform/internal/release"
32 "github.com/carlosframework/platform/internal/rootstate"
33)
34
35var openClaimRootAuthority = func() (rootstate.Store, error) {
36 return rootstate.Open("/var/lib/carlos-claims/authority/fences", true, false)
37}
38
39type ClaimRuntimeResources struct {
40 Mode string
41 Registry *registry.Registry
42 Manager *cgroup.Manager
43 DataDir, OptDir, UnitsDir string
44 // MigrationWindow is journal-migrate's --window: the attended offline
45 // window's directory, whose held unit files let the migration census
46 // accept that window's own masks. Empty everywhere else.
47 MigrationWindow string
48}
49
50// ClaimOwnerBindings are the provider's typed owners. Serving feeds the
51// claim's own serving input, admission and publication owners. Moves is the
52// moves integration's owner set for moving, landing, reverting and restoring,
53// bound only with CARLOS_MOVES_PHASE1 on (claim Plan 9 D5).
54type ClaimOwnerBindings struct {
55 moveRead *claimMoveReadAccess
56 wakeUnavailable error
57 cleanupEnrollment *claimCleanupEnrollment
58 RetirementAuthority *hostops.ProductionAuthority
59 EvidenceKey ed25519.PrivateKey
60 Identity hostbox.IdentityProvider
61 Local hostbox.LocalAuthority
62 Controllers StopControllerReader
63 Data lease.VersionedStore
64 // Claims-off callers keep the legacy owners. Claimed serving has its own
65 // bindings; the moves flag extends that dispatch with operation owners.
66 Inputs ClaimWakeInputs
67 AdmissionEvidence func(context.Context, hostclaim.Observed, hostclaim.Admission, *genselect.Plan) error
68 PublicationEvidence func(context.Context, hostclaim.Observed) error
69 Serving ClaimServingSources
70 Moves ClaimMovesOwners
71 InputPolicy func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error
72 MovesPolicy func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error
73 Evidence hostbox.RegionalEvidence
74 LeaseSet hostbox.LeaseSet
75 View func(context.Context, hostclaim.Observed) (park.View, error)
76 Authorize func(context.Context, hostclaim.Observed, hostclaim.Obligation) error
77 Close func(context.Context) error
78
79 // Relocation is region HA's typed owner set for relocating, bound only
80 // with CARLOS_REGION_HA and the moves C1 integration on (claim Plan 11).
81 Relocation ClaimRelocationOwners
82 // PolicyRefusals are append-only policy checks (claim Plan 11, R8 as
83 // accepted): each runs after the whole base policy passed and can only
84 // refuse. They are fixed here, before the box identity is verified, and
85 // nothing later can remove or replace one.
86 PolicyRefusals []hostbox.PolicyRefusal
87}
88
89type ClaimOwnerProvider interface {
90 OpenClaimOwners(context.Context, release.Store, ClaimRuntimeResources) (ClaimOwnerBindings, error)
91}
92
93var claimOwnerRegistration struct {
94 sync.Mutex
95 Provider ClaimOwnerProvider
96}
97
98// The Plan 8/8b and moves integration registers its typed adapters once at
99// process startup. It cannot replace local construction or add a permissive fallback.
100func RegisterProductionClaimOwners(p ClaimOwnerProvider) error {
101 claimOwnerRegistration.Lock()
102 defer claimOwnerRegistration.Unlock()
103 if p == nil || claimOwnerRegistration.Provider != nil {
104 return hostwrite.ErrUnavailable
105 }
106 claimOwnerRegistration.Provider = p
107 return nil
108}
109func productionClaimOwners() ClaimOwnerProvider {
110 claimOwnerRegistration.Lock()
111 defer claimOwnerRegistration.Unlock()
112 return claimOwnerRegistration.Provider
113}
114func withClaimRuntimeResources(ctx context.Context, r ClaimRuntimeResources) context.Context {
115 entry, _ := ctx.Value(claimEntryKey{}).(claimEntry)
116 entry.Resources = r
117 return context.WithValue(ctx, claimEntryKey{}, entry)
118}
119func ensureProductionClaimAssembly(ctx context.Context) (context.Context, error) {
120 entry, _ := ctx.Value(claimEntryKey{}).(claimEntry)
121 if !entry.Enabled {
122 return ctx, nil
123 }
124 if entry.Assembly != nil {
125 return ctx, nil
126 }
127 if entry.Resources.Registry == nil || entry.Resources.Mode == "" {
128 return ctx, &ClaimDependencyError{Contracts: []string{"box registry and mode-specific runtime resources"}}
129 }
130 provider := productionClaimOwners()
131 if provider == nil {
132 if err := RegisterProductionClaimOwners(ProductionClaimOwnerProvider{}); err != nil {
133 return ctx, err
134 }
135 provider = productionClaimOwners()
136 }
137 assembly, err := OpenProductionClaimAssembly(ctx, entry.Store, entry.Resources, provider)
138 if err != nil {
139 return ctx, err
140 }
141 entry.Assembly = assembly
142 return context.WithValue(ctx, claimEntryKey{}, entry), nil
143}
144
145func OpenProductionClaimAssembly(ctx context.Context, store release.Store, r ClaimRuntimeResources, provider ClaimOwnerProvider) (result *ClaimAssembly, resultErr error) {
146 if ctx == nil || store == nil || provider == nil || r.Registry == nil || (r.Mode != "agent" && r.Mode != "adopt" && r.Mode != "instance-control") || !filepath.IsAbs(r.DataDir) || !filepath.IsAbs(r.OptDir) || !filepath.IsAbs(r.UnitsDir) {
147 return nil, hostwrite.ErrUnavailable
148 }
149 if err := claimActivationPreflight(r.Registry); err != nil {
150 return nil, err
151 }
152 if err := claimActivationVerified(ctx, store, r); err != nil {
153 return nil, err
154 }
155 owners, err := provider.OpenClaimOwners(ctx, store, r)
156 if err != nil {
157 return nil, err
158 }
159 if owners.Close == nil {
160 return nil, hostwrite.ErrUnavailable
161 }
162 defer func() {
163 if resultErr != nil {
164 resultErr = errors.Join(resultErr, owners.Close(context.WithoutCancel(ctx)))
165 }
166 }()
167 movesC1, err := claimMovesC1(claimEntryEnv(ctx), owners.Moves)
168 if err != nil {
169 owners.wakeUnavailable = errors.Join(owners.wakeUnavailable, &ClaimDependencyError{Contracts: []string{"moves wake owners and operation attempts"}}, err)
170 movesC1 = false
171 }
172 if owners.Local == nil || owners.Controllers == nil || owners.Data == nil || owners.InputPolicy == nil || owners.MovesPolicy == nil || owners.Evidence == nil || owners.LeaseSet == nil || owners.View == nil || owners.Authorize == nil {
173 return nil, hostwrite.ErrUnavailable
174 }
175 servingEnabled := hostwrite.Enabled(claimEntryEnv(ctx))
176 if servingEnabled {
177 if err := checkClaimServingSources(owners.Serving); err != nil {
178 owners.wakeUnavailable = errors.Join(owners.wakeUnavailable, err)
179 }
180 } else if owners.Inputs == nil || owners.AdmissionEvidence == nil || owners.PublicationEvidence == nil {
181 return nil, hostwrite.ErrUnavailable
182 }
183 callerCtx := ctx
184 ctx, finishStartup := context.WithTimeout(ctx, 2*time.Minute)
185 defer finishStartup()
186 actor, err := owners.actorForConstruction(ctx, r.Mode)
187 if err != nil {
188 return nil, err
189 }
190 stop, err := owners.claimEvidenceCoordinator(ctx, store, r.Mode, actor)
191 if err != nil {
192 return nil, err
193 }
194 if r.Mode == "agent" && r.Manager == nil {
195 return nil, hostwrite.ErrUnavailable
196 }
197 local := filepath.Join(r.DataDir, ".hostclaims")
198 if r.Mode == "agent" {
199 if err = prepareClaimStateDirectory(local); err != nil {
200 return nil, err
201 }
202 }
203 var launches *hostbox.LaunchJournal
204 var inventory *hostbox.StopInventoryDB
205 var journals *hostbox.JournalService
206 if r.Mode != "agent" {
207 // The legacy journal check may wait on the migration lock while an
208 // operator runs the offline import (carlos system journal-migrate), so
209 // it keeps caller cancellation rather than construction's two minutes.
210 finishStartup()
211 ctx = callerCtx
212 launches, err = openClaimRootJournal()
213 if err != nil {
214 return nil, err
215 }
216 journals, err = hostbox.NewJournalService(launches)
217 if err != nil {
218 launches.Close()
219 return nil, err
220 }
221 inventory = journals.Inventory
222 if err = checkClaimJournalMigration(ctx, journals, local); err != nil {
223 launches.Close()
224 return nil, err
225 }
226 ctx, finishStartup = context.WithTimeout(callerCtx, 2*time.Minute)
227 defer finishStartup()
228 fresh, refreshErr := owners.actorForConstruction(ctx, r.Mode)
229 if refreshErr != nil || fresh.Box != actor.Box || fresh.Enrollment != actor.Enrollment || fresh.Bucket != actor.Bucket {
230 launches.Close()
231 return nil, errors.Join(refreshErr, hostwrite.ErrUnavailable)
232 }
233 actor = fresh
234 if err := recoverRootInventoryStartup(ctx, store, r, owners, actor, journals, launches); err != nil {
235 launches.Close()
236 return nil, err
237 }
238 if err := verifyRootMigrationStartup(ctx, rootFloorIdentity{Deployment: actor.ControlBucket, Box: actor.Box, Enrollment: actor.Enrollment}, journals.Inventory); err != nil {
239 launches.Close()
240 return nil, err
241 }
242 stop, err = owners.claimEvidenceCoordinator(ctx, store, r.Mode, actor)
243 if err != nil {
244 launches.Close()
245 return nil, err
246 }
247 } else {
248 launches, inventory = hostbox.RemoteJournals(rootJournalClient(controlsock.Path))
249 }
250 defer func() {
251 if resultErr != nil {
252 resultErr = errors.Join(resultErr, inventory.Close(), launches.Close())
253 }
254 }()
255 boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
256 if err != nil {
257 return nil, err
258 }
259 processStart, err := claimConstructionProcessStart()
260 if err != nil {
261 return nil, err
262 }
263 controller := ""
264 if r.Mode == "agent" {
265 controller, err = owners.Controllers.VerifiedController(ctx, os.Getpid())
266 if err != nil || controller == "" {
267 return nil, errors.Join(err, hostwrite.ErrUnavailable)
268 }
269 }
270 newID := func() string {
271 var b [16]byte
272 if _, err := rand.Read(b[:]); err != nil {
273 return ""
274 }
275 return hex.EncodeToString(b[:])
276 }
277 roots := map[string]string{"data": r.DataDir, "opt": r.OptDir, "units": r.UnitsDir}
278 guard := hostguard.Guard{DataDir: r.DataDir, Claims: hostguard.StoreReader{Store: store}}
279 var authority rootstate.Store
280 if r.Mode == "agent" {
281 guard.FenceView = rootFenceViewClient(rootJournalClient(controlsock.Path))
282 } else {
283 authority, err = openClaimRootAuthority()
284 if err != nil {
285 return nil, err
286 }
287 defer func() {
288 if resultErr != nil {
289 _ = authority.Close()
290 }
291 }()
292 guard.Root = hostguard.NewRootAuthorityWithInventory(authority, func(ctx context.Context, host string, c hostguard.Captured) (uint64, error) {
293 _, revision, err := inventory.Snapshot(ctx, host, c)
294 return revision, err
295 })
296 }
297 producers := &ClaimProducerSet{CloseWriters: launches.CloseWriters}
298 inputs, admissionEvidence, publicationEvidence := owners.Inputs, owners.AdmissionEvidence, owners.PublicationEvidence
299 var serving *claimServingOwners
300 var publisher ClaimFencedPublication
301 var groupD claimGroupD
302 if servingEnabled {
303 serving = &claimServingOwners{HomeRegion: actor.HomeRegion, Data: hostbox.RequestStore{Store: owners.Data, Check: func(ctx context.Context) error { _, err := owners.Identity.Verify(ctx); return err }}, Bucket: actor.Bucket, Region: park.PlacementRegion(actor.Region), Box: actor.Box, NewID: newID, Now: time.Now, Sources: owners.Serving}
304 serving.emptyRecovery = rootEmptyRecovery(guard)
305 if files, ok := serving.Sources.Files.(*movesFiles); ok {
306 files.emptyRecovery = serving.emptyRecovery
307 }
308 dispatch := ClaimOwnerDispatch{Serving: serving.owners()}
309 if movesC1 {
310 groupD, err = claimGroupDWiring(claimEntryEnv(ctx), serving.owners(), owners.Moves, owners.Relocation)
311 if err != nil {
312 return nil, err
313 }
314 dispatch = groupD.Dispatch
315 }
316 inputs, admissionEvidence, publicationEvidence = dispatch, dispatch.AdmissionEvidence, dispatch.PublicationEvidence
317 publisher = dispatch
318 }
319 wake := &ClaimWakeAssembly{Ready: claimSocketReady, Manager: r.Manager, Inputs: inputs, LeaseRegion: actor.Cloud.Region, Publication: publisher}
320 wake.RequireServingEnrollment = servingEnabled
321 wake.Producers = func(ctx context.Context, o hostclaim.Observed) (*ClaimProducers, error) {
322 if o.Claim == nil || o.Claim.Holder == nil || controller == "" {
323 return nil, hostwrite.ErrUnavailable
324 }
325 h := o.Claim.Holder
326 target := hostguard.FenceTarget{Host: o.Host, Incarnation: h.Incarnation, Stamp: h.Stamp, Session: h.Session, Op: o.Claim.Op.ID, Phase: o.Claim.Op.Phase, Box: actor.Box, Region: actor.Region, Enrollment: actor.Enrollment, BootID: strings.TrimSpace(string(boot)), ControllerID: controller, ClaimETag: o.ETag, DirectoryRevision: actor.Binding, RosterRevision: actor.Revision}
327 return producers.Ensure(target, processStart, launches.RecordProducer, launches.CompleteProducer)
328 }
329 wake.Roles = &ClaimRoleAssembly{Guard: guard, Claims: store, Manager: r.Manager, Journal: launches, Inventory: inventory, ManagedRoots: roots, Identity: owners.Identity, BootID: strings.TrimSpace(string(boot)), ControllerID: controller}
330 if r.Mode == "agent" {
331 wake.Roles.Root = NewClaimRoleRootClient(controlsock.Path)
332 }
333 wake.AfterAdmission = coldAdmissionHook(guard, owners.Identity, controlsock.Path)
334 wake.RetireArtifacts = func(context.Context, hostclaim.Observed, ClaimLaunchInputs, string) error { return hostbox.ErrChanged }
335 // Region HA N6 is wired whenever claim mode is on, whatever
336 // CARLOS_REGION_HA says (ruling 82's reasoning): it serves a relocation
337 // that already completed, and without a completion record it does nothing.
338 completed := &relocationCompletedHandover{Guard: guard, Identity: owners.Identity, Socket: controlsock.Path, Box: actor.Box}
339 handover := claimCrossBoxHandover{RecoveryRebind: recoveryRebindClient(controlsock.Path), Guard: guard, Evidence: store, Box: actor.Box, Inventory: inventory, Identity: owners.Identity, Registry: r.Registry, Launches: launches}
340 if owners.RetirementAuthority != nil {
341 handover.PublicKey = owners.RetirementAuthority.Directory.PublicKey
342 handover.Directory = owners.RetirementAuthority.Directory
343 handover.FenceAuthority = hostfence.ClaimFenceAuthority{Directory: owners.RetirementAuthority.Directory, Now: time.Now}
344 }
345 var relocation *ClaimRelocationOwners
346 if movesC1 && groupD.Dispatch.Relocation != nil {
347 relocation = &owners.Relocation
348 }
349 wireProductionAdmission(wake, actor.Box, movesC1, handover, relocation, completed)
350 if movesC1 {
351 retirement := park.Local{Authorize: func(ctx context.Context, c *hostclaim.Claim, o hostclaim.Obligation) error {
352 return owners.Authorize(ctx, hostclaim.Observed{Host: c.Host, Claim: c}, o)
353 }, Guard: guard, Claims: store, DataDir: r.DataDir, Box: actor.Box}
354 wake.RetireArtifacts = claimArtifactRetirement{Local: retirement, Continue: hostclaim.SessionContinues}.Retire
355 wake.RecoverArtifacts = claimArtifactRetirement{Local: retirement, Continue: hostclaim.SessionContinues}.Recover
356 wake.Continue = claimWakeContinue
357 wake.Operations = groupD.Operations
358 wake.ProbeReady = groupD.ProbeReady
359 }
360 if servingEnabled {
361 retirement := &claimServingArtifactRetirement{Claims: store, Guard: guard, DataDir: r.DataDir, Box: actor.Box, Region: actor.Region, Root: hostbox.ServingParkClient{Call: rootJournalClient(controlsock.Path)}}
362 if err := bindServingArtifactRetirement(wake, retirement); err != nil {
363 return nil, err
364 }
365 }
366
367 wake.Retain = wake.retainCaptured
368 wake.ObserveLoss = func(o hostclaim.Observed, err error) { log.Printf("host claim lease loss for %s: %v", o.Host, err) }
369 wake.Retries = activator.ServingRetries{Claims: store, Box: actor.Box, Enrollment: actor.Enrollment, BootID: strings.TrimSpace(string(boot)), ControllerID: controller, Now: time.Now, NewID: newID}
370 wake.Retries.Inputs = func(ctx context.Context, rt registry.Route) (string, error) {
371 o, err := hostclaim.Read(ctx, store, rt.Host)
372 if err != nil {
373 return "", err
374 }
375 in, err := owners.Inputs.Select(ctx, rt, o, activator.StartPolicy{MaxStarts: 5, Window: 30 * time.Second})
376 if err != nil {
377 return "", err
378 }
379 raw, err := json.Marshal(struct {
380 Identity hostwrite.Identity
381 Executable, Digest string
382 Args, Env []string
383 }{in.Identity, in.Executable, in.ExecutableSHA256, in.Args, in.Env})
384 if err != nil {
385 return "", err
386 }
387 digest := sha256.Sum256(raw)
388 return hex.EncodeToString(digest[:]), nil
389 }
390 if servingEnabled {
391 wake.ServingPrerequisites = servingFilesPrerequisites(store, owners.Serving.Files)
392 wake.Retries.BeforeReserve = func(ctx context.Context, rt registry.Route, o hostclaim.Observed) error {
393 if err := ctx.Err(); err != nil {
394 return err
395 }
396 if err := wake.ServingPrerequisites(ctx, rt, o); err != nil {
397 return err
398 }
399 return claimServingRoom(r.DataDir)
400 }
401 wake.Retries.Inputs = serving.launchDigest
402 }
403 // ClaimAssembly.Build binds Retries.StopAttempt to the attempt's persisted
404 // boot and controller tuple; a wake runtime cannot be wrapped without it.
405 reconcile := &hostbox.ReconcileAdapter{Registry: r.Registry, Inventory: inventory, Retirement: hostbox.TypedRetirement{JournalDir: claimRetireJournalDir(r.Mode, r.DataDir)}}
406 // Region HA R5-3 (Task 10b): the source's row held at its fence's
407 // close, with CARLOS_REGION_HA on only. Build copies the adapter and
408 // sets the reconciler's other fields; it keeps these two.
409 relocationSourceRows(claimEntryEnv(ctx), &reconcile.Reconciler, guard, r.Registry)
410 stop.Guard = guard
411 stop.Claims = hostguard.StoreReader{Store: store}
412 stop.Inventory = inventory
413 stop.NewID = newID
414 assembly := &ClaimAssembly{moveRead: owners.moveRead, wakeUnavailable: owners.wakeUnavailable, Journals: journals, cleanupEnrollment: owners.cleanupEnrollment, RetirementAuthority: owners.RetirementAuthority, SharedPaths: claimGlobalSharedPaths(r), Mode: r.Mode, Producers: producers, Launches: launches, ManagedRoots: roots, Reconcile: reconcile, RootJournal: launches, Controllers: owners.Controllers, Claims: store, Guard: guard, Identity: owners.Identity, Local: owners.Local, Policy: hostbox.Policy{Inputs: owners.InputPolicy, Moves: owners.MovesPolicy, Relocation: groupD.Relocation, Held: hostbox.RegistryHoldPolicy(r.Registry)}.WithRefusals(owners.PolicyRefusals...), Data: owners.Data, Wake: wake, Stop: stop, Evidence: owners.Evidence, LeaseSet: owners.LeaseSet, View: owners.View, Authorize: owners.Authorize, AdmissionEvidence: admissionEvidence, PublicationEvidence: publicationEvidence, NewID: newID}
415 if movesC1 && owners.RetirementAuthority != nil {
416 assembly.LossAdmission = &hostbox.LossAdmission{PublicKey: owners.RetirementAuthority.Directory.PublicKey, Owner: owners.Moves.LossRecovery, Sources: owners.Moves.LossSources}
417 }
418 var closeMu sync.Mutex
419 closed := false
420 assembly.Close = func(ctx context.Context) error {
421 closeMu.Lock()
422 defer closeMu.Unlock()
423 if closed {
424 return nil
425 }
426 if r.Mode == "agent" {
427 if err := wake.Close(ctx); err != nil {
428 return err
429 }
430 if err := wake.closeRetained(ctx); err != nil {
431 return err
432 }
433 }
434 if err := owners.Close(ctx); err != nil {
435 return err
436 }
437 closed = true
438 var rootErr error
439 if authority != nil {
440 rootErr = authority.Close()
441 }
442 return errors.Join(rootErr, inventory.Close(), launches.Close())
443 }
444 return assembly, nil
445}
446
447// claimGroupD is the moves C1 (claim Plan 9 Group D) owner wiring, with
448// region HA's relocation entry when it is bound (claim Plan 11).
449type claimGroupD struct {
450 Dispatch ClaimOwnerDispatch
451 Operations OperationAttempts
452 ProbeReady ClaimProbeReadiness
453 Relocation func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error
454}
455
456// claimGroupDWiring builds the Group D owners once moves C1 is on. With the
457// relocation owners unbound it is exactly Plan 9's wiring: the moves owner
458// set, the moves attempt journal and probe check, and no relocation policy.
459// Bound, the dispatcher gains the typed Relocation entry, a router sends
460// landing attempts to moves and relocating attempts to region HA, and the
461// relocation policy owner is set.
462func claimGroupDWiring(getenv func(string) string, serving ClaimKindOwners, moves ClaimMovesOwners, relocation ClaimRelocationOwners) (claimGroupD, error) {
463 movesOwners := moves.ClaimKindOwners
464 movesReady := moves.ProbeReady
465 if movesReady == nil {
466 movesReady = claimProbeSocketReady
467 }
468 out := claimGroupD{Dispatch: ClaimOwnerDispatch{Serving: serving, Moves: &movesOwners}, Operations: moves.Operations, ProbeReady: movesReady}
469 bound, err := claimRelocationC1(getenv, true, relocation)
470 if err != nil || !bound {
471 return out, err
472 }
473 relocationOwners := relocation.ClaimKindOwners
474 relocationReady := relocation.ProbeReady
475 if relocationReady == nil {
476 relocationReady = claimProbeSocketReady
477 }
478 out.Dispatch.Relocation = &relocationOwners
479 out.Operations = claimOperationRouter{
480 hostclaim.KindLanding: {Attempts: moves.Operations, ProbeReady: movesReady},
481 hostclaim.KindReverting: {Attempts: moves.Operations, ProbeReady: movesReady},
482 hostclaim.KindRestoring: {Attempts: moves.Operations, ProbeReady: movesReady},
483 hostclaim.KindRelocating: {Attempts: relocation.Operations, ProbeReady: relocationReady},
484 }
485 out.Relocation = relocation.Policy
486 return out, nil
487}
488
489// claimRetireJournalDir is root's retirement and cold-handover journal
490// namespace. It never lives under the agent-owned data directory: root parks
491// only what its own journals and stop inventory name. The agent retires
492// through root's socket and never reads it.
493func claimRetireJournalDir(string, string) string {
494 return "/var/lib/carlos-claims/retire"
495}
496func claimConstructionProcessStart() (string, error) {
497 raw, err := os.ReadFile("/proc/self/stat")
498 if err != nil {
499 return "", err
500 }
501 end := strings.LastIndexByte(string(raw), ')')
502 if end < 0 {
503 return "", hostwrite.ErrUnavailable
504 }
505 fields := strings.Fields(string(raw)[end+1:])
506 if len(fields) < 20 {
507 return "", hostwrite.ErrUnavailable
508 }
509 return fields[19], nil
510}
511
512type retainedClaimWork struct {
513 Observed hostclaim.Observed
514 Process activator.Proc
515 Replica activator.Rep
516 Cause error
517}
518
519func (w *ClaimWakeAssembly) retainCaptured(o hostclaim.Observed, p activator.Proc, r activator.Rep, err error) {
520 w.mu.Lock()
521 defer w.mu.Unlock()
522 w.retained = append(w.retained, &retainedClaimWork{o, p, r, err})
523}
524func (w *ClaimWakeAssembly) closeRetained(ctx context.Context) error {
525 w.mu.Lock()
526 saved := append([]*retainedClaimWork{}, w.retained...)
527 w.mu.Unlock()
528 for _, work := range saved {
529 if work.Process != nil {
530 if err := work.Process.Stop(ctx); err != nil {
531 return err
532 }
533 }
534 if work.Replica != nil {
535 if err := work.Replica.Close(ctx); err != nil {
536 return err
537 }
538 }
539 w.mu.Lock()
540 for i, item := range w.retained {
541 if item == work {
542 w.retained = append(w.retained[:i], w.retained[i+1:]...)
543 break
544 }
545 }
546 w.mu.Unlock()
547 }
548 return nil
549}
550
551func refusingRootWake(box string, store release.Store) *activator.ClaimRuntime {
552 return &activator.ClaimRuntime{Box: box, Read: func(ctx context.Context, h string) (hostclaim.Observed, error) { return hostclaim.Read(ctx, store, h) }, StopForeign: func(context.Context, string) error { return hostwrite.ErrUnavailable }, Prerequisites: func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) error {
553 return hostwrite.ErrUnavailable
554 }, Admit: func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) (hostclaim.Observed, error) {
555 return hostclaim.Observed{}, hostwrite.ErrUnavailable
556 }, Prepare: func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) (activator.ClaimStart, error) {
557 return activator.ClaimStart{}, hostwrite.ErrUnavailable
558 }, FailedAdmission: func(context.Context, hostclaim.Observed, error) error { return hostwrite.ErrUnavailable }}
559}
560