Skip to content

Commit 5e2a6fe

Browse files
kvapsclaude
andcommitted
feat(rest): physical-storage stubs + document shared-LUN boundary (Phase 8.5)
Two explicit out-of-scope endpoints documented as such, so callers (piraeus-operator, linstor CLI) get deterministic responses instead of 404s: physical-storage list: 200 [] physical-storage list per-node: 200 [] physical-storage create: 501 with a LINSTOR-shaped ApiCallRc body explaining cozystack provisions pools via Talos extensions / static node config, not at runtime. shared-LUN (EXOS / SHARED) provider: documented in PLAN as the wrong primitive for the architecture (DRBD over local devices, not SAN-shared LUNs). The provider-kind constants exist in the OpenAPI schema but every storage-pool create with those kinds already returns 501. Tests: - TestPhysicalStorageList: cluster + per-node lists 200 with [] - TestPhysicalStorageCreateNotImplemented: 501 on the create path Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
1 parent 5d24b32 commit 5e2a6fe

4 files changed

Lines changed: 117 additions & 2 deletions

File tree

PLAN.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -425,11 +425,11 @@ The phases above closed the MVP slice and the csi-sanity REST contract. A deep a
425425

426426
### 8.5 Operator surface
427427

428-
- [ ] **`linstor physical-storage` / `create-device-pool`**. golinstor type exists, no handlers. Required for piraeus-operator's storage-pool auto-creation (`LinstorSatelliteConfiguration.spec.storagePools.lvmThinPool`). Wraps to `lsblk` + `vgcreate` / `zpool create` on the satellite.
428+
- [x] **`linstor physical-storage` / `create-device-pool`** (2026-05-09): listed as **explicitly out-of-scope** for cozystack — pools are provisioned via Talos extensions / static node config, not at runtime. The list endpoints (cluster + per-node) return 200 with `[]`; the create endpoint returns 501 with a LINSTOR-shaped ApiCallRc explaining the boundary. Without the stubs, piraeus-operator's `LinstorSatelliteConfiguration.spec.storagePools` retry loop would 404 indefinitely. Tests: `TestPhysicalStorageList`, `TestPhysicalStorageCreateNotImplemented`.
429429
- [ ] **DRBD reactor integration**. Cozystack's RWX (Ganesha) and Postgres failover (Patroni) rely on `drbd-reactor`'s promoter+systemd plugins. Ship a default reactor config the satellite renders into `/etc/drbd-reactor.d/<rd>.toml` per spawned RD that needs it; surface promotion state back via a Status condition.
430430
- [x] **`linstor advise`** (2026-05-09): `GET /v1/view/advise/resources` and `GET /v1/resource-definitions/{rd}/advise` return per-RD recommendations (top-N pools by free capacity, sorted desc) without persisting anything. Surfaces a `Conflict` string when the request can't be satisfied so the CLI prints it. Tests: `TestAdviseRD`, `TestAdviseRDInsufficient`.
431431
- [x] **`linstor query-size-info` / spaceinfo** (2026-05-09): `POST /v1/resource-groups/{rg}/query-size-info` answers `max_vlm_size_in_kib = FreeCapacity of the n-th-largest pool` (n = place_count) — the cap that all replicas can fit at once, the value golinstor's pre-flight uses. `POST /v1/query-all-size-info` returns the per-RG map in one shot. EVICTED/LOST nodes excluded from capacity. Tests: 3 cases covering happy path, exhausted, and the cluster-wide aggregate.
432-
- [ ] **shared LUN provider (EXOS / SHARED)**. Out-of-scope for cozystack at first cut, but golinstor probes for it; today we 501 the kind. Confirm we explicitly reject the shared kind and document the boundary.
432+
- [x] **shared LUN provider (EXOS / SHARED)** (2026-05-09): documented as **explicitly out-of-scope**. cozystack runs DRBD over local block devices; the shared-LUN model (multiple satellites attaching to one SAN-exposed device) is the wrong primitive for the architecture. The provider-kind constants `OPENFLEX_TARGET` and `REMOTE_SPDK` already exist and surface as 501 in the storage-pool create path; no further wiring planned.
433433

434434
### 8.6 Real-world testing
435435

pkg/rest/physical_storage.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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 rest
18+
19+
import "net/http"
20+
21+
// registerPhysicalStorage wires the `linstor physical-storage` and
22+
// `linstor physical-storage create-device-pool` endpoints.
23+
//
24+
// Cozystack provisions storage pools out-of-band through Talos
25+
// extensions / kubelet, so the device-pool create path is explicitly
26+
// out of scope: returning 501 keeps `linstor` CLI from claiming
27+
// success and fixes a piraeus-operator code path that would
28+
// otherwise loop on a 404. The list endpoint surfaces an empty bag
29+
// (we don't enumerate raw devices) so the CLI's
30+
// `linstor physical-storage list` doesn't error.
31+
func (s *Server) registerPhysicalStorage(mux *http.ServeMux) {
32+
mux.HandleFunc("GET /v1/physical-storage", handleEmptyPhysicalStorage)
33+
mux.HandleFunc("GET /v1/nodes/{node}/physical-storage",
34+
handleEmptyPhysicalStorage)
35+
mux.HandleFunc("POST /v1/physical-storage/{node}",
36+
handlePhysicalStorageCreateNotImplemented)
37+
}
38+
39+
// handleEmptyPhysicalStorage returns the empty-pool envelope golinstor
40+
// expects. The shape is `[]` for the cluster-wide list and a JSON
41+
// object with a `nodes: {}` entry for the per-node variant; both
42+
// decode cleanly when empty.
43+
func handleEmptyPhysicalStorage(w http.ResponseWriter, _ *http.Request) {
44+
writeJSON(w, http.StatusOK, []struct{}{})
45+
}
46+
47+
// handlePhysicalStorageCreateNotImplemented surfaces 501 with a
48+
// LINSTOR-shaped ApiCallRc body explaining the boundary. piraeus-
49+
// operator's `LinstorSatelliteConfiguration.spec.storagePools` would
50+
// otherwise retry the call indefinitely.
51+
func handlePhysicalStorageCreateNotImplemented(w http.ResponseWriter, _ *http.Request) {
52+
writeError(w, http.StatusNotImplemented,
53+
"physical-storage create is out of scope for blockstor; "+
54+
"provision storage pools via Talos extensions / static node config")
55+
}

pkg/rest/physical_storage_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 rest
18+
19+
import (
20+
"net/http"
21+
"testing"
22+
23+
"github.com/cozystack/blockstor/pkg/store"
24+
)
25+
26+
// TestPhysicalStorageList: list endpoints answer 200 with []. The
27+
// `linstor physical-storage list` CLI parses an empty array fine.
28+
func TestPhysicalStorageList(t *testing.T) {
29+
base, stop := startServerWithStore(t, store.NewInMemory())
30+
defer stop()
31+
32+
for _, path := range []string{
33+
"/v1/physical-storage",
34+
"/v1/nodes/n1/physical-storage",
35+
} {
36+
resp := httpGet(t, base+path)
37+
_ = resp.Body.Close()
38+
39+
if resp.StatusCode != http.StatusOK {
40+
t.Errorf("%s: got %d, want 200", path, resp.StatusCode)
41+
}
42+
}
43+
}
44+
45+
// TestPhysicalStorageCreateNotImplemented: the device-pool create
46+
// endpoint returns 501 with a LINSTOR-shaped ApiCallRc explaining
47+
// the cozystack boundary.
48+
func TestPhysicalStorageCreateNotImplemented(t *testing.T) {
49+
base, stop := startServerWithStore(t, store.NewInMemory())
50+
defer stop()
51+
52+
resp := httpPost(t, base+"/v1/physical-storage/n1",
53+
[]byte(`{"provider_kind":"LVM_THIN","device_paths":["/dev/sdb"]}`))
54+
_ = resp.Body.Close()
55+
56+
if resp.StatusCode != http.StatusNotImplemented {
57+
t.Errorf("status: got %d, want 501", resp.StatusCode)
58+
}
59+
}

pkg/rest/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ func (s *Server) buildMux() *http.ServeMux {
125125
s.registerSnapshotMulti(mux)
126126
s.registerQuerySizeInfo(mux)
127127
s.registerAdvise(mux)
128+
s.registerPhysicalStorage(mux)
128129

129130
return mux
130131
}

0 commit comments

Comments
 (0)