//go:build linux package host import ( "context" "errors" "os" "reflect" "strconv" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" ) // Preparation preserves the old authority. No filesystem effect or transport // accepts this record; publishing a new namespace needs a separate commit. type servingParkRebootPreparation struct { Version int Observation servingParkRebootRecord Slot servingParkSlot Legacy *servingLegacyConsumption `json:",omitempty"` Active *servingParkRebootAuthority `json:",omitempty"` } type servingParkRebootAuthority struct { ClaimETag string // Actor is the bound actor (hostbox.Actor.Bound): the roster write counter // is cleared, so a preparation persisted before a hold or any other // unrelated roster write still equals one observed after it. Its Binding // still moves on a quarantine, readmission or re-enrolment. Actor hostbox.Actor Directory servingArtifactDirectory Tags uint64 } // prepareServingReboot is root-local. An incomplete retirement still needs its // current installation, claim, census, stopped history and unlaunched successor. // A completed park remains inspectable after the host's installation is gone. func (p *servingParkRoot) prepareServingReboot(ctx context.Context, id string) error { return p.prepareOrCommitServingReboot(ctx, id, false) } func (p *servingParkRoot) prepareOrCommitServingReboot(ctx context.Context, id string, commit bool) error { if ctx == nil || p == nil || p.ledger == nil || p.ledger.state == nil { return hostbox.ErrUnsupported } if err := ctx.Err(); err != nil { return err } copyLedger := *p.ledger copyLedger.historical = true saved, err := p.ledger.readHistorical(id) if err != nil { return err } prepare := func(active *servingParkRebootAuthority) error { return copyLedger.prepareRebootLocked(ctx, saved, active) } if saved.Complete { return p.ledger.withNamespace(ctx, func() error { if err := prepare(nil); err != nil { return err } if commit { return copyLedger.commitRebootLocked(ctx, saved, nil) } return nil }) } current := saved.activeAuthorization().Successor return p.withRootInstallation(ctx, current, ctx.Err, func(o hostclaim.Observed, view hostguard.FenceView, actor hostbox.Actor, dir servingArtifactDirectory, tags uint64) error { r, err := p.ledger.readHistorical(id) if err != nil || !reflect.DeepEqual(r, saved) || r.Complete { return hostbox.ErrChanged } active := r.activeAuthorization() if tags != active.TagsFrom && tags != current.Stamp { return hostbox.ErrChanged } if err := p.validateServingRebootAuthority(ctx, o, view, actor, r); err != nil { return err } if err := prepare(&servingParkRebootAuthority{ClaimETag: o.ETag, Actor: actor.Bound(), Directory: dir, Tags: tags}); err != nil { return err } fresh, err := p.root.Stop.Coordinator.Guard.Claims.Read(ctx, current.Host) if err != nil { return err } if fresh.Absent || fresh.ETag != o.ETag { return hostbox.ErrChanged } nextActor, err := p.root.InventoryActor(ctx) if err != nil { return err } if !hostbox.SameBinding(nextActor, actor) { return hostbox.ErrChanged } if err := p.root.RequireNewWork(ctx); err != nil { return err } if err := p.validateServingRebootAuthority(ctx, fresh, view, actor, r); err != nil { return err } if err := ctx.Err(); err != nil { return err } if commit { return copyLedger.commitRebootLocked(ctx, r, &servingParkRebootAuthority{ClaimETag: o.ETag, Actor: actor.Bound(), Directory: dir, Tags: tags}) } return nil }) } // The namespace and (for incomplete work) host guard are already held. Keep the // ordinary slot/record lock order while pinning the remaining private evidence. func (l *servingParkLedger) prepareRebootLocked(ctx context.Context, saved servingParkRecord, active *servingParkRebootAuthority) error { a := saved.Authorization unlock, err := l.state.Lock(ctx, "slot-"+servingParkKey(a.Successor.Box, a.Successor.Host)+".lock") if err != nil { return err } defer unlock() unlockRecord, err := l.state.Lock(ctx, "retirement-"+a.ID+".lock") if err != nil { return err } defer unlockRecord() first, err := l.observeRebootPreparation(ctx, saved, active) if err != nil { return err } name := servingNamespacePreparationName(a.ID, first.Observation.Topology) body, err := l.state.Read(name) if err == nil { var prior servingParkRebootPreparation if servingParkDecode(body, &prior) != nil || !reflect.DeepEqual(prior, first) { return hostbox.ErrChanged } } else if !errors.Is(err, os.ErrNotExist) { return err } missing := errors.Is(err, os.ErrNotExist) again, err := l.observeRebootPreparation(ctx, saved, active) if err != nil { return err } if !reflect.DeepEqual(first, again) { return hostbox.ErrChanged } if missing { if err := ctx.Err(); err != nil { return err } if err := l.write(name, first); err != nil { return err } } // A lost write reply is retried by comparing the entire saved preparation. // Even a successful write cannot turn a later pathname change into evidence. last, err := l.observeRebootPreparation(ctx, saved, active) if err != nil { return err } if !reflect.DeepEqual(first, last) { return hostbox.ErrChanged } return ctx.Err() } func (l *servingParkLedger) observeRebootPreparation(ctx context.Context, saved servingParkRecord, active *servingParkRebootAuthority) (servingParkRebootPreparation, error) { var zero servingParkRebootPreparation fs, ok := l.fs.(*servingParkFS) if !ok { return zero, hostbox.ErrUnsupported } observation, err := fs.inspectRebootRecord(ctx, saved.Authorization.ID) if err != nil { return zero, err } r := observation.Record if !reflect.DeepEqual(r, saved) || (active == nil) != r.Complete { return zero, hostbox.ErrChanged } if active != nil { a := r.activeAuthorization() actor := active.Actor if active.ClaimETag == "" || !actor.Eligible || actor.Quarantined || actor.Enrollment == "" || actor.Binding == "" || actor.Box != a.Successor.Box || actor.Region != a.Successor.Region || active.Directory != observation.Source || (active.Tags != a.TagsFrom && active.Tags != a.Successor.Stamp) { return zero, hostbox.ErrChanged } } slot, err := l.slot(r.Authorization) if err != nil { return zero, err } id := r.Authorization.ID if !r.Complete && (slot.Pending != "" && slot.Pending != id || r.Prepared && (slot.Pending != id || slot.Last != id)) || r.Prepared && r.Disposition == servingParkRetain && !r.Closed && slot.ID != id { return zero, hostbox.ErrChanged } var legacy *servingLegacyConsumption if r.Authorization.Legacy { legacy, err = l.legacyConsumption(r.Authorization.Successor) if err != nil { return zero, err } if legacy == nil || !sameServingLegacyConsumption(*legacy, r) || legacy.Collected && (!r.Closed || !r.Complete || !r.EventRemoved) { return zero, hostbox.ErrChanged } } if err := ctx.Err(); err != nil { return zero, err } return servingParkRebootPreparation{Version: 1, Observation: observation, Slot: slot, Legacy: legacy, Active: active}, nil } func (p *servingParkRoot) validateServingRebootAuthority(ctx context.Context, o hostclaim.Observed, view hostguard.FenceView, actor hostbox.Actor, r servingParkRecord) error { current, ledger := *p, *p.ledger ledger.historical = true current.ledger = &ledger return current.validateServingRecoveryAuthority(ctx, o, view, actor, r) } func servingRebootPreparationName(id, boot string) string { return "reboot-" + id + "-" + boot + ".json" } func servingNamespacePreparationName(id string, t servingParkRebootObservation) string { boot := t.BootID if t.CurrentParent.Mount != t.Previous.Parent.Mount { boot += "-" + strconv.FormatUint(t.CurrentParent.Mount, 10) } return servingRebootPreparationName(id, boot) }