Skip to content

Commit b28cbaa

Browse files
kvapsclaude
andcommitted
feat(drbd): well-known options catalogue with section + LinstorKey
Typed Option struct + section constants + initial port of the upstream drbdoptions.json catalogue covering the keys cozystack-style clusters set in practice. Used by the satellite reconciler to route a flat property bag into the right .res block, and (Phase 6) by the property validator on POST /properties. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
1 parent 56045aa commit b28cbaa

3 files changed

Lines changed: 271 additions & 1 deletion

File tree

PLAN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ Full scope list lives in `docs/csi-api-surface.md` (to be created in Phase 1).
258258
- [ ] LUKS encryption layer (volume-level)
259259
- [x] DRBD encryption passphrase (`POST /v1/resource-definitions/{rd}/encryption-passphrase`): writes the per-RD shared secret onto the RD's props under `DrbdOptions/Net/shared-secret`. Flows through to satellites via the existing drbd_options channel. 3 contract tests.
260260
- [x] DRBD proxy enable/disable/configure: 501 Not Implemented stubs (`/v1/resource-definitions/{rd}/drbd-proxy*`). Cozystack-style clusters run flat L2 so DRBD-9's native protocol suffices; proxy isn't needed. Endpoints exist so `linstor drbd-proxy *` returns a deterministic error.
261-
- [ ] DRBD options: full set from `drbdoptions.json`
261+
- [x] DRBD options catalogue (`pkg/drbd/options.go`): typed Option struct + section constants (net / disk / peer-device / options / handlers) + initial subset of well-known keys (protocol, shared-secret, max-buffers, after-sb-*, on-io-error, al-extents, c-max-rate, auto-promote, quorum, on-no-quorum). 4 contract tests pin LinstorKey shape, section validity, uniqueness, and presence of cozystack-relevant keys. Full upstream catalogue ports as new keys come up.
262262
- [x] file storage provider (`pkg/storage/file`): FILE / FILE_THIN behind same Provider seam — fallocate (thick) / truncate (thin) for create, statfs(2) for pool capacity, snapshots intentionally unsupported (caller routes to LVM/ZFS instead). 9 contract tests.
263263
- [x] External-file management stub (`/v1/files` LIST returns []; GET /{path} → 404). Cozystack manages host config via Talos extensions; the endpoints exist so `linstor external-file list` doesn't 404.
264264

pkg/drbd/options.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/*
2+
Copyright 2026 Cozystack contributors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package drbd
18+
19+
// Option describes one DRBD-9 configurable knob — a port of one entry
20+
// from upstream's `drbdoptions.json` catalogue. We use it both for
21+
// property validation (Phase 6) and so the satellite reconciler can
22+
// route a flat string→string property bag into the right `.res` file
23+
// section.
24+
type Option struct {
25+
// Name is the bare option key (e.g. "max-buffers", "auto-promote").
26+
Name string
27+
28+
// Section selects which `.res` block this option belongs in:
29+
// `SectionNet`, `SectionDisk`, `SectionPeerDevice`, `SectionOptions`
30+
// (top-level resource options), or `SectionHandlers`.
31+
Section string
32+
33+
// LinstorKey is the upstream-compatible LINSTOR property name
34+
// (e.g. "DrbdOptions/Net/max-buffers") that callers set on
35+
// resource definitions. Stored verbatim so existing golinstor
36+
// clients keep working unmodified.
37+
LinstorKey string
38+
39+
// Default is the value DRBD-9 uses when the option is unset.
40+
// Empty string means "no default; let DRBD decide" (rarely useful
41+
// — most are typed).
42+
Default string
43+
44+
// Description is a one-line summary, lifted from the DRBD man pages.
45+
Description string
46+
}
47+
48+
// Section names for `.res` block placement. Exposed so callers can
49+
// route a flat property bag into the correct block without re-parsing
50+
// the LINSTOR key namespace.
51+
const (
52+
SectionNet = "net"
53+
SectionDisk = "disk"
54+
SectionPeerDevice = "peer-device"
55+
SectionOptions = "options"
56+
SectionHandlers = "handlers"
57+
)
58+
59+
// Options is the catalogue of DRBD knobs blockstor knows about. Far
60+
// from exhaustive — DRBD has 100+ options and we add them on demand —
61+
// but covers the keys cozystack-style clusters actually need. Built
62+
// up by section to keep each subset reviewable.
63+
func Options() []Option {
64+
net := netOptions()
65+
disk := diskOptions()
66+
peerDev := peerDeviceOptions()
67+
res := resourceOptions()
68+
69+
out := make([]Option, 0, len(net)+len(disk)+len(peerDev)+len(res))
70+
out = append(out, net...)
71+
out = append(out, disk...)
72+
out = append(out, peerDev...)
73+
out = append(out, res...)
74+
75+
return out
76+
}
77+
78+
// netOptions returns the `net { … }` section catalogue.
79+
func netOptions() []Option {
80+
return []Option{
81+
{
82+
Name: "protocol",
83+
Section: SectionNet,
84+
LinstorKey: "DrbdOptions/Net/protocol",
85+
Default: "C",
86+
Description: "Replication protocol — A (async) / B (semi-sync) / C (sync). Cozystack runs C.",
87+
},
88+
{
89+
Name: "shared-secret",
90+
Section: SectionNet,
91+
LinstorKey: "DrbdOptions/Net/shared-secret",
92+
Description: "HMAC secret authenticating peer-to-peer connections.",
93+
},
94+
{
95+
Name: "max-buffers",
96+
Section: SectionNet,
97+
LinstorKey: "DrbdOptions/Net/max-buffers",
98+
Default: "8000",
99+
Description: "Receive ring depth in 4 KiB pages.",
100+
},
101+
{
102+
Name: "after-sb-0pri",
103+
Section: SectionNet,
104+
LinstorKey: "DrbdOptions/Net/after-sb-0pri",
105+
Default: "discard-zero-changes",
106+
Description: "Split-brain recovery policy when neither side was Primary.",
107+
},
108+
{
109+
Name: "after-sb-1pri",
110+
Section: SectionNet,
111+
LinstorKey: "DrbdOptions/Net/after-sb-1pri",
112+
Default: "discard-secondary",
113+
Description: "Split-brain recovery policy when one side was Primary.",
114+
},
115+
}
116+
}
117+
118+
// diskOptions returns the `disk { … }` section catalogue.
119+
func diskOptions() []Option {
120+
return []Option{
121+
{
122+
Name: "on-io-error",
123+
Section: SectionDisk,
124+
LinstorKey: "DrbdOptions/Disk/on-io-error",
125+
Default: "detach",
126+
Description: "Behaviour on lower-level IO failure (pass-on / detach / call-local-io-error).",
127+
},
128+
{
129+
Name: "al-extents",
130+
Section: SectionDisk,
131+
LinstorKey: "DrbdOptions/Disk/al-extents",
132+
Default: "1237",
133+
Description: "Activity-log size; trades resync time against random-write throughput.",
134+
},
135+
}
136+
}
137+
138+
// peerDeviceOptions returns the `peer-device { … }` section catalogue.
139+
func peerDeviceOptions() []Option {
140+
return []Option{
141+
{
142+
Name: "c-max-rate",
143+
Section: SectionPeerDevice,
144+
LinstorKey: "DrbdOptions/PeerDevice/c-max-rate",
145+
Default: "100M",
146+
Description: "Upper bound on resync rate in bytes/second.",
147+
},
148+
}
149+
}
150+
151+
// resourceOptions returns the top-level `options { … }` catalogue.
152+
func resourceOptions() []Option {
153+
return []Option{
154+
{
155+
Name: "auto-promote",
156+
Section: SectionOptions,
157+
LinstorKey: "DrbdOptions/Resource/auto-promote",
158+
Default: "yes",
159+
Description: "Auto-promote to Primary on first open(2).",
160+
},
161+
{
162+
Name: "quorum",
163+
Section: SectionOptions,
164+
LinstorKey: "DrbdOptions/Resource/quorum",
165+
Default: "majority",
166+
Description: "Quorum policy — off / majority / all / <count>.",
167+
},
168+
{
169+
Name: "on-no-quorum",
170+
Section: SectionOptions,
171+
LinstorKey: "DrbdOptions/Resource/on-no-quorum",
172+
Default: "io-error",
173+
Description: "Behaviour when quorum is lost: io-error or suspend-io.",
174+
},
175+
}
176+
}

pkg/drbd/options_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
Copyright 2026 Cozystack contributors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package drbd_test
18+
19+
import (
20+
"strings"
21+
"testing"
22+
23+
"github.com/cozystack/blockstor/pkg/drbd"
24+
)
25+
26+
// TestOptionsHaveLinstorKey: every catalogue entry surfaces a
27+
// `DrbdOptions/<Section>/<Name>` LINSTOR-compatible property name. We
28+
// rely on this so users / golinstor clients can set keys verbatim.
29+
func TestOptionsHaveLinstorKey(t *testing.T) {
30+
for _, opt := range drbd.Options() {
31+
if opt.LinstorKey == "" {
32+
t.Errorf("%s: missing LinstorKey", opt.Name)
33+
}
34+
35+
if !strings.HasPrefix(opt.LinstorKey, "DrbdOptions/") {
36+
t.Errorf("%s: LinstorKey %q has wrong prefix", opt.Name, opt.LinstorKey)
37+
}
38+
}
39+
}
40+
41+
// TestOptionsHaveSection: every entry pins a `.res` section so the
42+
// satellite reconciler can route values without re-parsing the key.
43+
func TestOptionsHaveSection(t *testing.T) {
44+
allowed := map[string]bool{
45+
drbd.SectionNet: true,
46+
drbd.SectionDisk: true,
47+
drbd.SectionPeerDevice: true,
48+
drbd.SectionOptions: true,
49+
drbd.SectionHandlers: true,
50+
}
51+
52+
for _, opt := range drbd.Options() {
53+
if !allowed[opt.Section] {
54+
t.Errorf("%s: unknown section %q", opt.Name, opt.Section)
55+
}
56+
}
57+
}
58+
59+
// TestOptionsUniqueLinstorKeys: catalogue must not have duplicates,
60+
// otherwise property validation gets ambiguous.
61+
func TestOptionsUniqueLinstorKeys(t *testing.T) {
62+
seen := map[string]struct{}{}
63+
64+
for _, opt := range drbd.Options() {
65+
if _, dup := seen[opt.LinstorKey]; dup {
66+
t.Errorf("duplicate LinstorKey %q", opt.LinstorKey)
67+
}
68+
69+
seen[opt.LinstorKey] = struct{}{}
70+
}
71+
}
72+
73+
// TestOptionsCoverWellKnownKeys: a few keys cozystack-style clusters
74+
// rely on must be present.
75+
func TestOptionsCoverWellKnownKeys(t *testing.T) {
76+
want := []string{
77+
"DrbdOptions/Net/protocol",
78+
"DrbdOptions/Net/shared-secret",
79+
"DrbdOptions/Resource/auto-promote",
80+
"DrbdOptions/Resource/quorum",
81+
"DrbdOptions/Resource/on-no-quorum",
82+
}
83+
84+
got := map[string]bool{}
85+
for _, opt := range drbd.Options() {
86+
got[opt.LinstorKey] = true
87+
}
88+
89+
for _, key := range want {
90+
if !got[key] {
91+
t.Errorf("missing well-known key %q from catalogue", key)
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)