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
2
3package host
4
5import (
6 "context"
7 "errors"
8 "os"
9 "reflect"
10 "strconv"
11
12 "github.com/carlosframework/platform/internal/hostbox"
13 "github.com/carlosframework/platform/internal/hostclaim"
14 "github.com/carlosframework/platform/internal/hostguard"
15)
16
17// Preparation preserves the old authority. No filesystem effect or transport
18// accepts this record; publishing a new namespace needs a separate commit.
19type servingParkRebootPreparation struct {
20 Version int
21 Observation servingParkRebootRecord
22 Slot servingParkSlot
23 Legacy *servingLegacyConsumption `json:",omitempty"`
24 Active *servingParkRebootAuthority `json:",omitempty"`
25}
26
27type servingParkRebootAuthority struct {
28 ClaimETag string
29 // Actor is the bound actor (hostbox.Actor.Bound): the roster write counter
30 // is cleared, so a preparation persisted before a hold or any other
31 // unrelated roster write still equals one observed after it. Its Binding
32 // still moves on a quarantine, readmission or re-enrolment.
33 Actor hostbox.Actor
34 Directory servingArtifactDirectory
35 Tags uint64
36}
37
38// prepareServingReboot is root-local. An incomplete retirement still needs its
39// current installation, claim, census, stopped history and unlaunched successor.
40// A completed park remains inspectable after the host's installation is gone.
41func (p *servingParkRoot) prepareServingReboot(ctx context.Context, id string) error {
42 return p.prepareOrCommitServingReboot(ctx, id, false)
43}
44
45func (p *servingParkRoot) prepareOrCommitServingReboot(ctx context.Context, id string, commit bool) error {
46 if ctx == nil || p == nil || p.ledger == nil || p.ledger.state == nil {
47 return hostbox.ErrUnsupported
48 }
49 if err := ctx.Err(); err != nil {
50 return err
51 }
52 copyLedger := *p.ledger
53 copyLedger.historical = true
54 saved, err := p.ledger.readHistorical(id)
55 if err != nil {
56 return err
57 }
58 prepare := func(active *servingParkRebootAuthority) error {
59 return copyLedger.prepareRebootLocked(ctx, saved, active)
60 }
61 if saved.Complete {
62 return p.ledger.withNamespace(ctx, func() error {
63 if err := prepare(nil); err != nil {
64 return err
65 }
66 if commit {
67 return copyLedger.commitRebootLocked(ctx, saved, nil)
68 }
69 return nil
70 })
71 }
72 current := saved.activeAuthorization().Successor
73 return p.withRootInstallation(ctx, current, ctx.Err, func(o hostclaim.Observed, view hostguard.FenceView, actor hostbox.Actor, dir servingArtifactDirectory, tags uint64) error {
74 r, err := p.ledger.readHistorical(id)
75 if err != nil || !reflect.DeepEqual(r, saved) || r.Complete {
76 return hostbox.ErrChanged
77 }
78 active := r.activeAuthorization()
79 if tags != active.TagsFrom && tags != current.Stamp {
80 return hostbox.ErrChanged
81 }
82 if err := p.validateServingRebootAuthority(ctx, o, view, actor, r); err != nil {
83 return err
84 }
85 if err := prepare(&servingParkRebootAuthority{ClaimETag: o.ETag, Actor: actor.Bound(), Directory: dir, Tags: tags}); err != nil {
86 return err
87 }
88 fresh, err := p.root.Stop.Coordinator.Guard.Claims.Read(ctx, current.Host)
89 if err != nil {
90 return err
91 }
92 if fresh.Absent || fresh.ETag != o.ETag {
93 return hostbox.ErrChanged
94 }
95 nextActor, err := p.root.InventoryActor(ctx)
96 if err != nil {
97 return err
98 }
99 if !hostbox.SameBinding(nextActor, actor) {
100 return hostbox.ErrChanged
101 }
102 if err := p.root.RequireNewWork(ctx); err != nil {
103 return err
104 }
105 if err := p.validateServingRebootAuthority(ctx, fresh, view, actor, r); err != nil {
106 return err
107 }
108 if err := ctx.Err(); err != nil {
109 return err
110 }
111 if commit {
112 return copyLedger.commitRebootLocked(ctx, r, &servingParkRebootAuthority{ClaimETag: o.ETag, Actor: actor.Bound(), Directory: dir, Tags: tags})
113 }
114 return nil
115 })
116}
117
118// The namespace and (for incomplete work) host guard are already held. Keep the
119// ordinary slot/record lock order while pinning the remaining private evidence.
120func (l *servingParkLedger) prepareRebootLocked(ctx context.Context, saved servingParkRecord, active *servingParkRebootAuthority) error {
121 a := saved.Authorization
122 unlock, err := l.state.Lock(ctx, "slot-"+servingParkKey(a.Successor.Box, a.Successor.Host)+".lock")
123 if err != nil {
124 return err
125 }
126 defer unlock()
127 unlockRecord, err := l.state.Lock(ctx, "retirement-"+a.ID+".lock")
128 if err != nil {
129 return err
130 }
131 defer unlockRecord()
132 first, err := l.observeRebootPreparation(ctx, saved, active)
133 if err != nil {
134 return err
135 }
136 name := servingNamespacePreparationName(a.ID, first.Observation.Topology)
137 body, err := l.state.Read(name)
138 if err == nil {
139 var prior servingParkRebootPreparation
140 if servingParkDecode(body, &prior) != nil || !reflect.DeepEqual(prior, first) {
141 return hostbox.ErrChanged
142 }
143 } else if !errors.Is(err, os.ErrNotExist) {
144 return err
145 }
146 missing := errors.Is(err, os.ErrNotExist)
147 again, err := l.observeRebootPreparation(ctx, saved, active)
148 if err != nil {
149 return err
150 }
151 if !reflect.DeepEqual(first, again) {
152 return hostbox.ErrChanged
153 }
154 if missing {
155 if err := ctx.Err(); err != nil {
156 return err
157 }
158 if err := l.write(name, first); err != nil {
159 return err
160 }
161 }
162 // A lost write reply is retried by comparing the entire saved preparation.
163 // Even a successful write cannot turn a later pathname change into evidence.
164 last, err := l.observeRebootPreparation(ctx, saved, active)
165 if err != nil {
166 return err
167 }
168 if !reflect.DeepEqual(first, last) {
169 return hostbox.ErrChanged
170 }
171 return ctx.Err()
172}
173
174func (l *servingParkLedger) observeRebootPreparation(ctx context.Context, saved servingParkRecord, active *servingParkRebootAuthority) (servingParkRebootPreparation, error) {
175 var zero servingParkRebootPreparation
176 fs, ok := l.fs.(*servingParkFS)
177 if !ok {
178 return zero, hostbox.ErrUnsupported
179 }
180 observation, err := fs.inspectRebootRecord(ctx, saved.Authorization.ID)
181 if err != nil {
182 return zero, err
183 }
184 r := observation.Record
185 if !reflect.DeepEqual(r, saved) || (active == nil) != r.Complete {
186 return zero, hostbox.ErrChanged
187 }
188 if active != nil {
189 a := r.activeAuthorization()
190 actor := active.Actor
191 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) {
192 return zero, hostbox.ErrChanged
193 }
194 }
195 slot, err := l.slot(r.Authorization)
196 if err != nil {
197 return zero, err
198 }
199 id := r.Authorization.ID
200 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 {
201 return zero, hostbox.ErrChanged
202 }
203 var legacy *servingLegacyConsumption
204 if r.Authorization.Legacy {
205 legacy, err = l.legacyConsumption(r.Authorization.Successor)
206 if err != nil {
207 return zero, err
208 }
209 if legacy == nil || !sameServingLegacyConsumption(*legacy, r) || legacy.Collected && (!r.Closed || !r.Complete || !r.EventRemoved) {
210 return zero, hostbox.ErrChanged
211 }
212 }
213 if err := ctx.Err(); err != nil {
214 return zero, err
215 }
216 return servingParkRebootPreparation{Version: 1, Observation: observation, Slot: slot, Legacy: legacy, Active: active}, nil
217}
218
219func (p *servingParkRoot) validateServingRebootAuthority(ctx context.Context, o hostclaim.Observed, view hostguard.FenceView, actor hostbox.Actor, r servingParkRecord) error {
220 current, ledger := *p, *p.ledger
221 ledger.historical = true
222 current.ledger = &ledger
223 return current.validateServingRecoveryAuthority(ctx, o, view, actor, r)
224}
225
226func servingRebootPreparationName(id, boot string) string {
227 return "reboot-" + id + "-" + boot + ".json"
228}
229
230func servingNamespacePreparationName(id string, t servingParkRebootObservation) string {
231 boot := t.BootID
232 if t.CurrentParent.Mount != t.Previous.Parent.Mount {
233 boot += "-" + strconv.FormatUint(t.CurrentParent.Mount, 10)
234 }
235 return servingRebootPreparationName(id, boot)
236}
237