package host import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "os" "reflect" "strings" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/rootparticipation" "github.com/carlosframework/platform/internal/rootstate" ) type rootMigrationTimerRecord struct { Schema int Target hostguard.FenceTarget Before, Next hostbox.StopInventory Revision uint64 Rows []registry.Timer Registrations []string Artifacts hostguard.InstallationArtifacts } type rootMigrationTimers struct { state rootstate.Store registry *registry.Registry inventory *hostbox.StopInventoryDB guard hostguard.Guard } func (m rootMigrationTimers) key(item rootMigrationItem) string { in := item.Installation return "timers-" + generationInventoryDigest([]any{in.Host, in.Incarnation, in.Stamp, item.Session}) + ".json" } func (m rootMigrationTimers) owner(item rootMigrationItem, registration string) registry.ClaimOwnership { in := item.Installation sum := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%s\x00%d\x00%s", in.Host, in.Incarnation, in.Stamp, in.Box))) return registry.ClaimOwnership{ClaimIncarnation: in.Incarnation, ClaimStamp: in.Stamp, ClaimOwner: hex.EncodeToString(sum[:]), ClaimRowID: registration} } func (m rootMigrationTimers) load(item rootMigrationItem) (rootMigrationTimerRecord, bool, error) { var r rootMigrationTimerRecord raw, err := m.state.Read(m.key(item)) if errors.Is(err, os.ErrNotExist) { return r, false, nil } if err != nil { return r, false, err } in := item.Installation if rootparticipation.Decode(raw, &r) != nil || r.Schema != 1 || r.Revision == 0 || r.Revision == ^uint64(0) || r.Target.Host != in.Host || r.Target.Incarnation != in.Incarnation || r.Target.Stamp != in.Stamp || r.Target.Session != item.Session || r.Target.Box != in.Box || r.Target.Enrollment != in.Enrollment { return r, false, errRootMigrationBusy } if len(r.Rows) != len(r.Registrations) { return r, false, errRootMigrationBusy } for i, id := range r.Registrations { if id == "" { return r, false, errRootMigrationBusy } r.Rows[i].Registration = id } return r, true, nil } func (m rootMigrationTimers) adopt(ctx context.Context, item rootMigrationItem, target hostguard.FenceTarget, canonical registry.Route) error { if m.state == nil || m.registry == nil || m.inventory == nil || m.guard.Root == nil { return errRootMigrationBusy } r, exists, err := m.load(item) if err != nil { return err } if exists { if r.Target != target { return errRootMigrationBusy } } else { in, rev, err := m.inventory.Snapshot(ctx, target.Host, hostguard.Captured{Incarnation: target.Incarnation, Stamp: target.Stamp}) if err != nil { return err } if !in.Complete || rev == 0 || rev == ^uint64(0) || in.Host != item.Installation.Host || in.Incarnation != item.Installation.Incarnation || in.InstallationStamp != item.Installation.Stamp || in.Box != item.Installation.Box || in.Enrollment != item.Installation.Enrollment { return errRootMigrationBusy } rows, err := m.registry.TimersFor(in.Host) if err != nil { return err } r = rootMigrationTimerRecord{Schema: 1, Target: target, Before: in, Next: in, Revision: rev, Rows: rows} for _, row := range rows { r.Registrations = append(r.Registrations, row.Registration) } r.Next.Entries = append([]hostbox.StopInventoryEntry(nil), in.Entries...) matched := map[string]bool{} for i, e := range r.Next.Entries { if e.Kind != "timer" { continue } name := strings.TrimPrefix(e.RelativePath, in.Host+"/") if name == e.RelativePath || name == "" || strings.ContainsAny(name, "/\\\r\n\x00") || matched[name] || e.TimerRetired || e.Materialization != "materialized" { return errRootMigrationBusy } found := false for _, row := range rows { if row.Name != name { continue } found = true matched[name] = true owner := m.owner(item, row.Registration) if row.Registration == "" || row.Host != in.Host || (row.ClaimOwnership != (registry.ClaimOwnership{}) && (row.ClaimOwnership != owner || e.RowIdentity != row.Registration || e.Ownership != owner.ClaimOwner)) { return errRootMigrationBusy } e.ManagedRoot, e.RowIdentity, e.Ownership = "registry", row.Registration, owner.ClaimOwner e.Device, e.Inode, e.Type = 0, 0, 0 r.Next.Entries[i] = e } if !found { return errRootMigrationBusy } } if len(matched) != len(rows) { return errRootMigrationBusy } prior := item.Installation prior.Artifacts.Schema = 1 r.Artifacts, err = rootInventoryArtifacts(prior, canonical, r.Next, rev) if err != nil { return err } } // The original inventory remains bound to root's completed receipt. Registry // observations cannot replace the stop proof or the retained session denial. receipt, err := m.guard.FenceReceiptFor(ctx, r.Target) if err != nil { return err } var proof StopReceipt if json.Unmarshal(receipt.Bytes, &proof) != nil || proof.InventorySHA256 != digestStop(r.Before) { return errRootMigrationBusy } view, err := m.guard.Root.View(ctx, target.Host, hostguard.Captured{Incarnation: target.Incarnation, Stamp: target.Stamp}) if err != nil { return err } denied := false for _, d := range view.MigrationDenies { if d.Incarnation == target.Incarnation && d.Stamp >= target.Stamp && d.Session >= target.Session { denied = true } } if !denied { return errRootMigrationBusy } raw, err := json.Marshal(r) if err != nil { return err } if err := m.state.Write(m.key(item), raw); err != nil { return err } current, rev, err := m.inventory.Snapshot(ctx, target.Host, hostguard.Captured{Incarnation: target.Incarnation, Stamp: target.Stamp}) if err != nil { return err } before := rev == r.Revision && reflect.DeepEqual(current, r.Before) after := rev == r.Revision+1 && reflect.DeepEqual(current, r.Next) if !before && !after { return errRootMigrationBusy } rows, err := m.registry.TimersFor(target.Host) if err != nil { return err } if len(rows) != len(r.Rows) { return errRootMigrationBusy } for _, observed := range r.Rows { owned := observed owned.ClaimOwnership = m.owner(item, observed.Registration) current, err := m.registry.GetTimer(observed.Host, observed.Name) if err != nil { return err } if *current == observed && observed.ClaimOwnership == (registry.ClaimOwnership{}) { if err := m.registry.AdoptClaimTimer(ctx, observed, owned.ClaimOwnership); err != nil { return err } } else if *current != owned { return errRootMigrationBusy } } if before { if err := m.inventory.WriteStopInventory(ctx, r.Next); err != nil { return err } } return m.guard.Root.CompleteMigrationInventory(ctx, target, r.Revision+1, r.Artifacts, r.Revision) }