Skip to content

Commit c0e9620

Browse files
kvapsclaude
andcommitted
fix(drbd): peer hosts render upstream's placeholder, not local disk
The .res-file renderer copied the local node's `Volume.Disk` into every `on <node> { … }` block. That worked when both sides happened to use the same backing path (LVM with the same VG name, etc.) but broke the moment paths differed — e.g. FILE_THIN, where each satellite's `losetup --find` lands on a different /dev/loopN. Mirror upstream LINSTOR's ConfFileBuilder: - local diskful host → real backing-disk path - local OR peer diskless host → `disk none` - peer diskful host → `disk /dev/drbd/this/is/not/used` drbd never reads the peer-side `disk` field, but the parser requires a stable non-empty / non-`none` token; using the peer's real path would clash when the peer is mid-conversion from diskless. The constant placeholder is what upstream emits and what drbd users have indexed off for years. To carry the per-peer diskless signal end-to-end: - dispatcher.addPeerEntries now stamps `peer.<name>.diskless=true` on the drbd_options bag when the peer Resource carries the DISKLESS flag. - satellite reconciler's host-list builder reads that key and sets the new drbd.Host{IsLocal, Diskless} fields. - drbd.writeOnBlock consumes the flags to pick the right `disk` via a small `diskField` helper. New TestBuildPeerDisks pins the precedence so a regression here is a unit-test failure rather than a multi-node DRBD setup that silently refuses to come up. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
1 parent 1bc2b4d commit c0e9620

4 files changed

Lines changed: 117 additions & 13 deletions

File tree

pkg/dispatcher/dispatcher.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ import (
4141
// it renders the file (drbd doesn't accept literal 0.0.0.0).
4242
const drbdAddrAny = "0.0.0.0"
4343

44+
// boolPropTrue is the canonical string-form `true` value blockstor
45+
// stamps on drbd_options bag keys that are flag-like (auto-primary,
46+
// peer.<n>.diskless, …). Pinning the literal avoids consumer-side
47+
// drift between `"true"`/`"True"`/`"1"`.
48+
const boolPropTrue = "true"
49+
4450
// BuildDesired translates a Resource + its same-RD peers into the
4551
// satellite-facing DesiredResource. Port/minor/node-id assignment is
4652
// deterministic from the RD name + sorted peer list — good enough for
@@ -106,7 +112,7 @@ func BuildDesired(target *blockstoriov1alpha1.Resource, peers []blockstoriov1alp
106112
// primary --force only on firstActivation.
107113
if !slices.Contains(target.Spec.Flags, "DISKLESS") &&
108114
idOf[target.Spec.NodeName] == lowestDiskfulID(target, peers) {
109-
drbdOpts["auto-primary"] = "true"
115+
drbdOpts["auto-primary"] = boolPropTrue
110116
}
111117

112118
addPeerEntries(drbdOpts, dropped, peers, nodes, port, idOf)
@@ -247,6 +253,13 @@ func addPeerEntries(drbdOpts map[string]string, dropped []string, peers []blocks
247253
drbdOpts["peer."+peer+".port"] = strconv.Itoa(peerPort)
248254
drbdOpts["peer."+peer+".node-id"] = strconv.Itoa(int(idOf[peer]))
249255
drbdOpts["peer."+peer+".address"] = peerAddress(peer, nodes)
256+
257+
// Surface the peer's DISKLESS flag so the satellite's .res
258+
// renderer can emit `disk none;` instead of the diskful
259+
// placeholder for diskless peers.
260+
if p, ok := peerByName[peer]; ok && slices.Contains(p.Spec.Flags, "DISKLESS") {
261+
drbdOpts["peer."+peer+".diskless"] = boolPropTrue
262+
}
250263
}
251264
}
252265

pkg/drbd/conffile.go

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,21 @@ type Host struct {
8686
// NodeID is the drbd-9 node identifier (0..31). Must be unique
8787
// within the resource's host list.
8888
NodeID int
89+
90+
// IsLocal marks this host as the satellite rendering the .res
91+
// file (only one host per resource has this true). Peer hosts
92+
// get a placeholder `disk` value upstream LINSTOR uses
93+
// (`/dev/drbd/this/is/not/used`) — drbd never reads the peer
94+
// host's `disk` field, but the parser refuses an empty one and
95+
// treats `none` as DISKLESS, so a constant placeholder is what
96+
// upstream emits.
97+
IsLocal bool
98+
99+
// Diskless marks the host as a DRBD diskless replica — `disk`
100+
// must be rendered as the literal `none`. Same value on local
101+
// vs. peer hosts: a peer that's diskless still gets `none`,
102+
// not the placeholder.
103+
Diskless bool
89104
}
90105

91106
// Volume is one data volume on the resource.
@@ -178,23 +193,48 @@ func writeOptions(b *strings.Builder, opts map[string]string) {
178193
}
179194

180195
// writeOnBlock emits one `on <node> { … }` block including every volume
181-
// definition for this peer.
182-
func writeOnBlock(b *strings.Builder, h *Host, vols []Volume) {
183-
fmt.Fprintf(b, " on %s {\n", h.NodeName)
184-
fmt.Fprintf(b, " address %s:%d;\n", h.Address, h.Port)
185-
fmt.Fprintf(b, " node-id %d;\n", h.NodeID)
186-
187-
for _, v := range vols {
188-
fmt.Fprintf(b, " volume %d {\n", v.Number)
189-
fmt.Fprintf(b, " device %s minor %d;\n", v.Device, v.Minor)
190-
fmt.Fprintf(b, " disk %s;\n", v.Disk)
196+
// definition for this peer. The `disk` value follows upstream LINSTOR's
197+
// ConfFileBuilder:
198+
// - DISKLESS host (local or peer) → `none`
199+
// - local diskful host → the real backing path (Volume.Disk)
200+
// - peer diskful host → the literal placeholder `/dev/drbd/this/is/not/used`
201+
//
202+
// The peer placeholder exists because drbd never reads the peer's
203+
// `disk` field but the parser rejects empty / requires a stable
204+
// non-`none` token; using each peer's actual backing path would
205+
// also clash when a peer is mid-conversion from diskless and its
206+
// path is reported as `none`.
207+
const peerDiskPlaceholder = "/dev/drbd/this/is/not/used"
208+
209+
func writeOnBlock(b *strings.Builder, host *Host, vols []Volume) {
210+
fmt.Fprintf(b, " on %s {\n", host.NodeName)
211+
fmt.Fprintf(b, " address %s:%d;\n", host.Address, host.Port)
212+
fmt.Fprintf(b, " node-id %d;\n", host.NodeID)
213+
214+
for _, vol := range vols {
215+
fmt.Fprintf(b, " volume %d {\n", vol.Number)
216+
fmt.Fprintf(b, " device %s minor %d;\n", vol.Device, vol.Minor)
217+
fmt.Fprintf(b, " disk %s;\n", diskField(host, vol))
191218
b.WriteString(" meta-disk internal;\n")
192219
b.WriteString(" }\n")
193220
}
194221

195222
b.WriteString(" }\n")
196223
}
197224

225+
// diskField picks the `disk` value to render for one (host, volume)
226+
// pair. See writeOnBlock for the precedence.
227+
func diskField(host *Host, vol Volume) string {
228+
switch {
229+
case host.Diskless:
230+
return "none"
231+
case host.IsLocal:
232+
return vol.Disk
233+
default:
234+
return peerDiskPlaceholder
235+
}
236+
}
237+
198238
// writeConnectionMesh emits one `connection { … }` block per (i, j)
199239
// host pair with i < j. drbd-9 requires the mesh to be explicit; with
200240
// N>2 peers an implicit "everyone talks to everyone" doesn't exist.

pkg/drbd/conffile_test.go

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func TestBuildSinglePeerSingleVolume(t *testing.T) {
4747
Name: "pvc-1",
4848
Net: drbd.Net{ProtocolC: true},
4949
Hosts: []drbd.Host{
50-
{NodeName: "n1", Address: "10.0.0.1", Port: 7000, NodeID: 0},
50+
{NodeName: "n1", Address: "10.0.0.1", Port: 7000, NodeID: 0, IsLocal: true},
5151
{NodeName: "n2", Address: "10.0.0.2", Port: 7000, NodeID: 1},
5252
},
5353
Volumes: []drbd.Volume{
@@ -66,9 +66,14 @@ func TestBuildSinglePeerSingleVolume(t *testing.T) {
6666
" node-id 0;",
6767
" volume 0 {",
6868
" device /dev/drbd1000 minor 1000;",
69+
// Local node uses the real backing disk path.
6970
" disk /dev/vg/pvc-1_00000;",
7071
" meta-disk internal;",
7172
" on n2 {",
73+
// Peer node uses upstream's placeholder — drbd never reads
74+
// the peer-side `disk`, but the parser requires a stable
75+
// non-empty / non-`none` token.
76+
" disk /dev/drbd/this/is/not/used;",
7277
} {
7378
if !strings.Contains(got, want) {
7479
t.Errorf("missing %q in output:\n%s", want, got)
@@ -166,6 +171,43 @@ func TestBuildEmitsResourceOptions(t *testing.T) {
166171
}
167172
}
168173

174+
// TestBuildPeerDisks pins the `disk` precedence upstream LINSTOR
175+
// uses: local diskful → real path; local diskless → `none`;
176+
// peer diskful → `/dev/drbd/this/is/not/used`; peer diskless →
177+
// `none` (not the placeholder).
178+
func TestBuildPeerDisks(t *testing.T) {
179+
got, err := drbd.Build(drbd.Resource{
180+
Name: "pvc-1",
181+
Net: drbd.Net{ProtocolC: true},
182+
Hosts: []drbd.Host{
183+
{NodeName: "n1", Address: "10.0.0.1", Port: 7000, NodeID: 0, IsLocal: true},
184+
{NodeName: "n2", Address: "10.0.0.2", Port: 7000, NodeID: 1},
185+
{NodeName: "n3", Address: "10.0.0.3", Port: 7000, NodeID: 2, Diskless: true},
186+
},
187+
Volumes: []drbd.Volume{
188+
{Number: 0, Device: "/dev/drbd1000", Disk: "/dev/loop42", Minor: 1000},
189+
},
190+
})
191+
if err != nil {
192+
t.Fatalf("Build: %v", err)
193+
}
194+
195+
cases := []struct {
196+
needle string
197+
why string
198+
}{
199+
{" on n1 {\n address 10.0.0.1:7000;\n node-id 0;\n volume 0 {\n device /dev/drbd1000 minor 1000;\n disk /dev/loop42;\n", "local diskful → real path"},
200+
{" on n2 {\n address 10.0.0.2:7000;\n node-id 1;\n volume 0 {\n device /dev/drbd1000 minor 1000;\n disk /dev/drbd/this/is/not/used;\n", "peer diskful → placeholder"},
201+
{" on n3 {\n address 10.0.0.3:7000;\n node-id 2;\n volume 0 {\n device /dev/drbd1000 minor 1000;\n disk none;\n", "peer diskless → none"},
202+
}
203+
204+
for _, c := range cases {
205+
if !strings.Contains(got, c.needle) {
206+
t.Errorf("%s: missing block\n want substring %q\n in:\n%s", c.why, c.needle, got)
207+
}
208+
}
209+
}
210+
169211
// TestBuildDeterministic: same input → same output, twice in a row. Map
170212
// iteration order would otherwise leak into the .res file and trigger
171213
// spurious drbdadm reloads on every reconcile.

pkg/satellite/reconciler.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,7 @@ func (r *Reconciler) applyDRBD(ctx context.Context, dr *satellitepb.DesiredResou
587587
// "Inconsistent" into "UpToDate" without a human running
588588
// drbdadm. Subsequent reconciles see firstActivation=false and
589589
// skip the seed.
590-
if firstActivation && !diskless && dr.GetDrbdOptions()["auto-primary"] == "true" {
590+
if firstActivation && !diskless && dr.GetDrbdOptions()["auto-primary"] == drbdBoolPropTrue {
591591
err = r.cfg.Adm.PrimaryForce(ctx, dr.GetName())
592592
if err != nil {
593593
return errors.Wrapf(err, "auto-primary %s", dr.GetName())
@@ -688,6 +688,8 @@ func buildResFile(dr *satellitepb.DesiredResource, localNode, localAddr string,
688688
Address: resolveAddr(opts["address"], localAddr),
689689
Port: port,
690690
NodeID: nodeID,
691+
IsLocal: true,
692+
Diskless: isDiskless(dr.GetFlags()),
691693
})
692694

693695
for _, peer := range dr.GetPeers() {
@@ -699,6 +701,7 @@ func buildResFile(dr *satellitepb.DesiredResource, localNode, localAddr string,
699701
Address: resolveAddr(opts["peer."+peer+".address"], ""),
700702
Port: peerPort,
701703
NodeID: peerNodeID,
704+
Diskless: opts["peer."+peer+".diskless"] == drbdBoolPropTrue,
702705
})
703706
}
704707

@@ -789,6 +792,12 @@ func splitDRBDOptions(opts map[string]string) (map[string]string, map[string]str
789792
// substitutes the satellite's own IP whenever it sees this value.
790793
const drbdAddrPlaceholder = "0.0.0.0"
791794

795+
// drbdBoolPropTrue mirrors dispatcher.boolPropTrue — the literal
796+
// `true` the dispatcher stamps on flag-like drbd_options keys. We
797+
// inline rather than re-export to keep `pkg/satellite` from
798+
// importing `pkg/dispatcher` just for one constant.
799+
const drbdBoolPropTrue = "true"
800+
792801
// resolveAddr substitutes the satellite's own IP whenever the
793802
// controller-supplied address is the placeholder (which it is until
794803
// the controller starts learning each satellite's pod IP and passing

0 commit comments

Comments
 (0)