Skip to content

Commit ea069e7

Browse files
kvapsclaude
andcommitted
feat(reconcile): auto-primary seed + observed-state writeback
Two halves of the same UpToDate-without-human-intervention story: 1. Dispatcher tags one diskful replica per RD (lex-lowest node name) with drbd_options[auto-primary]=true. On firstActivation the satellite Reconciler.applyDRBD runs `drbdadm primary --force` then `drbdadm secondary`, which seeds the resource into UpToDate without an operator running drbdadm by hand. Subsequent reconciles see firstActivation=false and skip. 2. Server.applyObserved (events2 → ReportObserved) now actually writes the observed DRBD state back to the Resource: DrbdState prop + Resource.State.InUse. Lets REST clients (linstor-csi, kubectl-linstor) see live runtime status without schema changes. drbdadm wrapper gains PrimaryForce so the seed step uses --force explicitly rather than the bare Primary which refuses to promote when local disk is Inconsistent and no peer is UpToDate. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
1 parent 28ea7a5 commit ea069e7

4 files changed

Lines changed: 71 additions & 7 deletions

File tree

pkg/dispatcher/dispatcher.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,17 @@ func buildDesired(target *blockstoriov1alpha1.Resource, peers []blockstoriov1alp
177177
"minor": strconv.Itoa(minor),
178178
}
179179

180+
// Pick a single replica (lexically lowest node name) to seed
181+
// initial Primary. Without this every diskful brand-new RD comes
182+
// up Inconsistent and stays there until something opens for
183+
// write. The seed flag is harmless on subsequent reconciles —
184+
// satellite Reconciler runs primary --force only on
185+
// firstActivation.
186+
if !slices.Contains(target.Spec.Flags, "DISKLESS") &&
187+
target.Spec.NodeName == sortedAll[0] {
188+
drbdOpts["auto-primary"] = "true"
189+
}
190+
180191
// Per-peer entries — used by ConfFileBuilder on the satellite to
181192
// compose the connection mesh. We resolve each peer's
182193
// SatelliteEndpoint prop into a real IP so drbd-9 has somewhere

pkg/drbd/drbdadm.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ func (a *Adm) Primary(ctx context.Context, resource string) error {
6969
return a.run(ctx, "primary", resource)
7070
}
7171

72+
// PrimaryForce promotes a resource to Primary even when local disk is
73+
// Inconsistent and no peer is UpToDate. Used as the initial-sync seed
74+
// on a brand-new diskful replica — without --force, drbd refuses to
75+
// promote, leaving the resource permanently "Inconsistent".
76+
func (a *Adm) PrimaryForce(ctx context.Context, resource string) error {
77+
return a.run(ctx, "primary", "--force", resource)
78+
}
79+
7280
// Secondary flips the resource back to Secondary role. Used after the
7381
// consumer unmounts and before another peer takes Primary.
7482
func (a *Adm) Secondary(ctx context.Context, resource string) error {

pkg/satellite/reconciler.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,24 @@ func (r *Reconciler) applyDRBD(ctx context.Context, dr *satellitepb.DesiredResou
344344
return errors.Wrapf(err, "adjust %s", dr.GetName())
345345
}
346346

347+
// On first activation of a diskful replica the controller may
348+
// flag it as the auto-primary seed. We promote once (force-
349+
// primary then back to secondary) so the metadata moves out of
350+
// "Inconsistent" into "UpToDate" without a human running
351+
// drbdadm. Subsequent reconciles see firstActivation=false and
352+
// skip the seed.
353+
if firstActivation && !diskless && dr.GetDrbdOptions()["auto-primary"] == "true" {
354+
err = r.cfg.Adm.PrimaryForce(ctx, dr.GetName())
355+
if err != nil {
356+
return errors.Wrapf(err, "auto-primary %s", dr.GetName())
357+
}
358+
359+
err = r.cfg.Adm.Secondary(ctx, dr.GetName())
360+
if err != nil {
361+
return errors.Wrapf(err, "auto-secondary %s", dr.GetName())
362+
}
363+
}
364+
347365
return nil
348366
}
349367

pkg/satellitecontroller/server.go

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,39 @@ func (s *Server) ReportObserved(stream satellitepb.Controller_ReportObservedServ
132132
}
133133
}
134134

135-
// applyObserved is the per-event Status update — currently a no-op
136-
// placeholder while the Resource CRD's Status fields settle (we'll
137-
// land conditions + per-volume disk state in the next slice). Keeping
138-
// the call site means the wire path is exercised end-to-end on the
139-
// stand and any future implementation slots in without re-touching
140-
// the gRPC frame loop.
141-
func (s *Server) applyObserved(_ context.Context, _ *satellitepb.ResourceObservedEvent) error {
135+
// applyObserved lands one parsed events2 frame on the matching
136+
// Resource. We store the DRBD state as a `DrbdState` prop and the
137+
// "in use" hint via Resource.State.InUse so existing REST callers
138+
// (linstor-csi, kubectl-linstor) see the live runtime info without
139+
// the schema needing to change. Granular per-volume disk state lands
140+
// once the CRD's volume-level status fields settle.
141+
func (s *Server) applyObserved(ctx context.Context, ev *satellitepb.ResourceObservedEvent) error {
142+
if ev.GetResourceName() == "" || ev.GetNodeName() == "" {
143+
return nil
144+
}
145+
146+
res, err := s.st.Resources().Get(ctx, ev.GetResourceName(), ev.GetNodeName())
147+
if err != nil {
148+
// NotFound is normal during convergence — the satellite may
149+
// observe state for a resource the controller hasn't yet
150+
// created. Bubble nothing.
151+
return nil //nolint:nilerr // best-effort: missing resource isn't a stream-fatal event
152+
}
153+
154+
if res.Props == nil {
155+
res.Props = map[string]string{}
156+
}
157+
158+
if disk := ev.GetDrbdState(); disk != "" {
159+
res.Props["DrbdState"] = disk
160+
}
161+
162+
res.State.InUse = ev.GetInUse()
163+
164+
err = s.st.Resources().Update(ctx, &res)
165+
if err != nil {
166+
return errors.Wrap(err, "update resource state")
167+
}
168+
142169
return nil
143170
}

0 commit comments

Comments
 (0)