Files
felhom.eu/documentation/audits/SPIKE-raw-drive-enroll-2026-06-15.md
T
admin f24125ecdd docs: SPIKE raw-drive plug-and-enroll (GATE, 2026-06-15)
Report-only spike + one live throwaway FORMAT experiment on /dev/sdc.
No production code, no version bump.

- Q1: raw-disk discovery gap confirmed — Observe AND localapi GET /disks
  are both built only from PVE storages (ListStorage/NodeStorage); a
  not-yet-storage disk is invisible. proxmox client has no /disks/* method.
- Q3: safety classifier (RoleForRawDevice/SystemDisks/RoleForStorage)
  already protects sda/felhom-pbs live, but needs two hardening checks:
  whole-disk-granularity enrolled-subtraction + LVM-PV membership.
- Q6: FORMAT path proven end-to-end live — pvesh disks/directory create
  -> agent Observe -> controller "Regisztralas" enroll. sdc left as the
  clean enrolled felhom-flash B3 migration target. sda/sdb untouched.
- Premise correction: felhom-pbs is remote (no local PBS disk).
- Fork list for the build spec in the audit doc + REPORT.md.
2026-06-15 10:57:33 +02:00

271 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPIKE — Self-serve raw-drive plug-and-enroll
**Date:** 2026-06-15
**Type:** GATE spike — report-only + one live throwaway experiment on `/dev/sdc`.
**Host:** `felhom-pve` (node `demo-felhom`), PVE 9, guest 9201.
**Status:** findings complete; live FORMAT path proven end-to-end; sdc left as a clean enrolled `felhom-flash` PVE storage (B3 migration target).
**No production code, no version bump.** The build spec is gated on the fork picks at the end.
> **Goal (operator's vision):** a customer plugs in a drive and enrolls it from the controller UI with **no operator/Proxmox tinkering**. Flow: agent detects the raw drive → decides usable-as-is vs needs-format → `storage/init` shows it (model/size/serial, partitions+filesystems, recommendation) → customer chooses → controller tells agent → agent creates a PVE dir storage (formatting only if chosen) → binds the `felhom-data` namespace into the guest (existing `AttachBind`) → drive usable.
---
## One-screen summary
The flow is **mostly assemblable from primitives that already exist**; the genuine net-new is a **raw-disk discovery source** (the agent is blind to any disk that isn't already a PVE storage) and a **create-storage call** (the agent never creates PVE storages today). The security classifier is **already written** (`RoleForRawDevice`) and correctly protects the system disk live — but it needs **two hardening additions** (LVM-PV membership + whole-disk-granularity "already-enrolled" subtraction) before it can drive a *discovery* list.
Proven live on this host:
| Disk | What it is | disks/list `used` | Classifies | Why |
|---|---|---|---|---|
| **sda** SSD 512G | boot/root: ESP + LVM VG `pve` (root, swap, local-lvm thinpool, **guest 9201's own disks**) | `BIOS boot` | **PROTECTED (system)** | only LVM PV (`/dev/sda3`); `/boot/efi`=`sda2` pins it |
| **sdb** HDD 1T | enrolled data drive `felhom-usb`, ext4 `/mnt/felhom-usb` | `ext4` | **PROTECTED (enrolled data)** | backs registered storage `felhom-usb` |
| **sdc** USB 128G | throwaway flash, junk iso9660+EFI+hfsplus | `iso9660`→(after)`ext4` | **OFFERABLE** | removable, backs nothing, not a system disk |
The live FORMAT experiment (Q6) turned sdc into a registered `felhom-flash` dir storage in **one PVE API call**, and it **immediately appeared in the controller's agent-drive view as user-data / data-bearing / "Nem regisztrált" with a `Regisztrálás` button** — the existing enroll path picks it up with no agent restart.
**Spec premise correction:** there is **no local PBS disk** on this host. `felhom-pbs` is a *remote* `pbs:` storage (`server 192.168.0.180`); it has no backing block device. The "PBS disk classifies PROTECTED" assertion is satisfied at the *storage* tier (`RoleForStorage``RoleBackup`), not at any raw-disk tier.
---
## Q1 — Current discovery (the gap), confirmed in source
`internal/storage/observe.go:131,139``Observe` builds its entire view from exactly two PVE calls and nothing else:
```go
cfg, _ := o.api.ListStorage(ctx) // observe.go:131 → GET /storage
node, _ := o.api.NodeStorage(ctx) // observe.go:139 → GET /nodes/{node}/storage
```
The proxmox client (`internal/proxmox/query.go`) has **no raw-disk method**. Full read surface: `Version, Nodes, NodeStatus, ListLXC, GuestStatus, GuestConfig, ListStorage(:58), NodeStorage(:64), StorageContent, LatestBackupVolID`. None call `/nodes/{node}/disks/*`.
Critically, the localapi disk endpoint is **also** sourced from `Observe`, not from any block-device scan:
```go
// internal/localapi/disks.go:130 (handleDisks, GET /disks)
targets, err := s.storage.Observe(r.Context())
```
**Conclusion:** a raw `/dev/sdc` that is **not yet a PVE storage is invisible to both the hub-facing `Observe` and the localapi `GET /disks`.** A drive must *already* be a registered PVE storage before anything in the agent can see it. That is precisely the missing primitive. (`host.Removable`, observe.go:182, only annotates a storage that is *already* in the view — it does not discover.)
**What's missing, exactly:**
1. A proxmox-client (or host-level) method to enumerate raw block devices.
2. A discovery path in the agent that lists *candidate* disks (not-yet-storages) and runs the safety classifier over them.
3. A create-storage primitive (Q5).
---
## Q2 — Raw-disk discovery API, validated live
`pvesh get /nodes/demo-felhom/disks/list --output-format json` (abridged):
```json
[
{"devpath":"/dev/sda","type":"ssd","size":512110190592,"model":"AirDisk_512GB_SSD","serial":"QDF922W009654S30EX","used":"BIOS boot","gpt":1,"health":"PASSED","osdid":-1,"by_id_link":"/dev/disk/by-id/ata-AirDisk_512GB_SSD_QDF922W009654S30EX"},
{"devpath":"/dev/sdb","type":"hdd","size":1000204886016,"model":"TOSHIBA_MQ04ABF100","serial":"65NOP3HDT","used":"ext4","gpt":1,"rpm":"5400"},
{"devpath":"/dev/sdc","type":"usb","size":128320801792,"model":"Flash_Drive","serial":"0375123010001586","used":"iso9660","gpt":1,"health":"UNKNOWN"}
]
```
`disks/list` gives: `devpath, by_id_link, type (ssd/hdd/usb), size, model, serial, wwn, health, gpt, osdid, used`. The `used` enum is the single most useful built-in signal but it is **insufficient alone**:
- It distinguishes sda (`BIOS boot`) but reports both sdb (enrolled data) and sdc (junk) as just a filesystem type (`ext4` / `iso9660`). After enrollment sdc *also* reads `ext4` — identical to the protected sdb. `used` cannot separate "offerable" from "already-enrolled data".
- It does **not** report removable, nor which disk holds LVM PVs / the guest's image / the root mount.
Companion endpoints:
- `pvesh get /nodes/demo-felhom/disks/lvm` → VG `pve` on `/dev/sda3`, `lvcount 9`. **Authoritative LVM-PV-membership signal.**
- `pvesh get /nodes/demo-felhom/disks/directory``[]` **before** the experiment; **after**, lists only `felhom-flash`. **It does NOT list `felhom-usb`** — PVE's directory tracking only reflects storages *it* created via the `disks/directory` API, not the agent's own systemd mounts. So `disks/directory` is **not** a reliable enumerator of dir storages.
**Recommendation — discovery source:** use **`disks/list` as the candidate enumerator**, cross-referenced with **`disks/lvm`** (PV membership) and the agent's **existing host-mount read** (`SystemDisks`, which already parses the mount table) and the **`Observe` storage view** (to subtract already-enrolled disks). The removable flag is already available to the agent via `/sys/block/<disk>/removable` (`hostread.go`, `ProcHostReader.Removable`) and via `lsblk RM`; PVE's `disks/list` does not expose it, so keep reading it host-side. A small `lsblk -J` is still worthwhile to get serial/by-uuid/child-partition detail per disk for the `storage/init` display, but the *safety* decision is driven by `disks/list` + `disks/lvm` + mount-table + storage-view, not by labels.
---
## Q3 — The safety gate (security-critical core)
**Good news: the classifier primitive already exists and is correct for the system disk.** `internal/storage/role.go`:
- `SystemDisks(host)` (role.go:46) — resolves the OS whole-disks from the **mount table** (`/`, `/boot`, `/boot/efi` → whole disk), root-free. On this host `/` is on `pve-root` (device-mapper, untraceable to a raw disk), but **`/boot/efi` = `sda2` pins `sda`** exactly as the code comment intends.
- `RoleForRawDevice(device, sysDisks, sysKnown)` (role.go:133) — system-vs-user-data for a raw `/dev` path; **defaults to system on ambiguity**.
- `RoleForStorage(...)` (role.go:105) — tiers a *registered* storage; PBS→backup, USB/local-dir-on-non-system-disk→user-data, everything else→system.
- `deviceRole(...)` (disks.go:836) — wraps both: prefers a matching storage target's role, else `RoleForRawDevice`.
**Validated live** (reproducing the agent's logic; `sysDisks={/dev/sda}` via `/boot/efi`=`sda2`, `sysKnown=true`):
- `RoleForRawDevice("/dev/sda")``isSystemBacked``sysDisks["/dev/sda"]` = **system** ✓ PROTECTED.
- `RoleForRawDevice("/dev/sdc")` → not in set → **user-data** ✓ offerable.
- `felhom-pbs` (`RoleForStorage` PBS) → **backup** ✓ PROTECTED (storage tier; no device).
Confirmed at the UI: the controller's agent-drive view renders `local`=🔒system, `local-lvm`=🔒system, `felhom-pbs`=🔒backup, `felhom-usb`/`felhom-flash`=user-data.
### Two hardening gaps that MUST close before this drives a *discovery* list
1. **Whole-disk-granularity "already-enrolled" subtraction.** `deviceRole` matches a registered storage by **exact** `t.BackingDevice == device` (disks.go:840). The format endpoint passes the exact backing device, so this works *there*. But a discovery flow enumerates **whole disks** (`/dev/sdb`), while `felhom-usb`'s backing device is the **partition** `/dev/sdb1`. The exact match fails → fall-through to `RoleForRawDevice("/dev/sdb")`**user-data → would be offered for format**, even though it holds live customer data. **Fix:** the offerable filter must subtract any disk whose **whole-disk** equals `wholeDiskOf(t.BackingDevice)` for *any* `Observe` target — compare at whole-disk granularity, not device-path equality.
2. **LVM-PV / guest-image membership.** `SystemDisks` resolves the system disk via the **mount table only**. On this host that catches `sda` because the ESP is on it. But a disk that holds `local-lvm` PVs or guest disk images **without** carrying `/boot`/`/boot/efi` would be **missed** (→ misclassified user-data). **Fix:** add a positive PV-membership check from `disks/lvm` (any disk whose partition is a PV of the `local-lvm` VG, or the VG that backs any guest's `rootfs`/`mp`), folding it into the protected set alongside `SystemDisks`.
### Proposed offerable predicate (the gate)
A whole disk `D` is **OFFERABLE** iff **all** of:
- `RoleForRawDevice(D) == user-data` (not the OS/boot disk; fail-safe system on ambiguity), **and**
- `D` is **not** in the LVM-PV/guest-image protected set (`disks/lvm` + guest-config backing), **and**
- `wholeDiskOf(D)` backs **no** registered `Observe` target (subtract already-enrolled, whole-disk granularity), **and**
- `D` is **not** a local PBS datastore disk (here: N/A — PBS is remote).
Everything else (incl. on **any** ambiguity) → **PROTECTED, never listed for format**. This extends the existing *"mkfs-not-on-data-bearing-device"* discipline (`disks.go:526`, `InspectDevice` blkid+lsblk) to *"mkfs-not-on-system-disk."*
### Non-hollow test shape
A test that **FAILS if the system disk is ever offerable**, fed a realistic topology fixture (sda=ESP+LVM-PV+guest images; sdb=ext4 backing a registered `felhom-usb`; sdc=removable junk):
```
TestOfferableDisks_NeverOffersProtected:
offer := OfferableDisks(disksList, lvmView, observeTargets, hostMounts)
assert sda NOT in offer // ESP + sole LVM PV → system
assert sdb NOT in offer // backs registered felhom-usb (whole-disk subtraction)
assert sdc IN offer // removable, backs nothing, not system
// adversarial: relabel sdc's disks/list "used" to "BIOS boot" (lying label)
// → still offerable iff state says so; and relabel sda "used"="" (lying blank)
// → MUST still be protected via ESP/LVM-PV state, not the label.
// mutation guard: drop the LVM-PV check → test MUST fail on a PV-only protected disk fixture.
```
The mutation clause (removing either hardening check makes the test red) is what keeps it non-hollow.
---
## Q4 — Usable-as-is vs needs-format
Decision driven by the agent's **existing** `InspectDevice` (`internal/storage/hostops.go`, blkid `-p -o export` + `lsblk -J`) which already yields `HasFilesystem/FSType/HasPartitionTable/HasPartitions/Mounted`:
- **Enroll-as-is** iff the disk presents **exactly one mountable, supported data filesystem** we can own — i.e. a single partition (or whole-disk fs) with `FSType ∈ {ext4, xfs}` (the fs types the agent's mount machinery + PVE dir storage support), not currently mounted by the host/another storage, and not the system/protected set.
- **Needs-format** iff: no filesystem, an unsupported fs (the live sdc had **iso9660** + a **vfat** ESP + **hfsplus** — none ownable), a partition table with **multiple data partitions** (ambiguous which to adopt — sdc had 4), or any signature we will not silently adopt.
- **On ambiguity → recommend format** (never silently adopt an unknown layout), mirroring the destructive-on-ambiguity invariant inverted for safety: we don't *destroy* on ambiguity, we *refuse to enroll-as-is* and require an explicit format choice.
**What `storage/init` should display** per candidate:
- Identity: model, size, serial, bus type (ssd/hdd/usb), removable badge — all from `disks/list` + `lsblk`.
- Layout: each partition with size + detected fs (`lsblk -J -o NAME,FSTYPE,SIZE,MOUNTPOINT`), plus the data-bearing verdict + reason from `InspectDevice`.
- Recommendation: **"Enroll partition X as-is (ext4, N GB)"** when the as-is predicate holds, else **"Format required — current contents will be erased"** with the detected junk listed (e.g. "iso9660 + EFI + hfsplus").
---
## Q5 — PVE-storage creation via the agent — BOTH paths
### FORMAT path — validated live (one call does everything)
```
pvesh create /nodes/demo-felhom/disks/directory \
--name felhom-flash --device /dev/sdc --filesystem ext4 --add_storage 1
```
The single call ran synchronously and performed, in order:
```
# /sbin/sgdisk -n1 -t1:8300 /dev/sdc → partition sdc1
# /sbin/mkfs -t ext4 /dev/sdc1 → fs UUID 81a26531-62d8-408d-812f-a178b1d35310
# /sbin/blkid /dev/sdc1 -o export
Created symlink …/mnt-pve-felhom\x2dflash.mount → systemd mount unit, enabled
add_storage 1 → wrote storage.cfg
```
Resulting `storage.cfg` entry:
```
dir: felhom-flash
path /mnt/pve/felhom-flash
content rootdir,images,backup,iso,snippets,vztmpl
is_mountpoint 1
nodes demo-felhom
```
Resulting systemd unit (`mnt-pve-felhom\x2dflash.mount`):
```
[Mount]
What=/dev/disk/by-uuid/81a26531-62d8-408d-812f-a178b1d35310
Where=/mnt/pve/felhom-flash
Type=ext4
Options=defaults
```
**Conventions check:**
- `is_mountpoint 1` ✓ (locked convention).
- Mount unit keyed by **fs-UUID** (`/dev/disk/by-uuid/…`) ✓ — matches the agent's own `mountunit.go` (`What=/dev/disk/by-uuid/<UUID>`).
- Durable-id will derive **`uuid:81a26531-…`** from the storage's own fs UUID ✓ (`durableid.go:deriveDurableID`, USB/local-dir → `uuid:`).
- **Mount path = `/mnt/pve/felhom-flash`** ✗ — diverges from the felhom `/mnt/<name>` convention (`felhom-usb` is `/mnt/felhom-usb`). PVE's `disks/directory` API has **no option to override the path**; it is always `/mnt/pve/<name>`. → **FORK.**
- **`content` = all types** ✗ — `felhom-usb` is `content backup`. The data drive that holds the bind-mounted `felhom-data` namespace doesn't *use* PVE content types, but the entry should be normalized for consistency. → minor fork.
### ENROLL-AS-IS path — validated by the living reference
`felhom-usb` itself **is** the enroll-as-is proof: it is a `dir` storage with `is_mountpoint 1` at `/mnt/felhom-usb`, **absent from `disks/directory`** → it was created by **mounting an existing fs + `pvesm add dir`**, *not* by PVE's formatting `disks/directory` API. The agent already owns both halves:
- `EnsureMount` / `renderMountUnit` (`internal/storage/mountunit.go`, `HostOps.EnsureMount`) — stages a by-UUID systemd `.mount` unit at `/mnt/<name>`, `daemon-reload`, `enable --now`. **No mkfs.**
- the missing half is the `pvesm add dir <id> --path /mnt/<name> --is_mountpoint 1 --content …` call (the agent does not create PVE storages today — see Q5 net-new).
So enroll-as-is = `EnsureMount(existing-fs-UUID, /mnt/<name>)` + `add-dir-storage` — both primitives understood, one not-yet-wired.
### Both paths feed the existing enroll flow
`Observe` iterates `NodeStorage`, so **either** path's resulting storage is picked up with **no agent restart** (proven for the format path below). The customer-facing enroll (`AttachBind`, disks.go `handleDiskGuestAttach`) then binds `<where>/felhom-data` into the guest.
---
## Q6 — Live experiment (end-to-end, sets up B3)
Every call run against `felhom-pve`, all guarded by a **serial-identity fence** (`/dev/sdc` serial must equal `0375123010001586`, must be removable `RM=1`, must have no mountpoint — abort otherwise) so sda/sdb could never be touched.
1. **Guard + wipe.** Guard passed (serial matched, RM=1). `wipefs -a /dev/sdc` erased the iso9660 + 2×GPT + PMBR signatures; `sgdisk --zap-all` cleared the GPT. After: `lsblk` shows bare `sdc` (no fs), and `disks/list` reports `sdc used=None gpt=0`**PVE's `used=None` is the "genuinely blank" signal.**
2. **Create (format path).** The `pvesh create …/disks/directory` call above — partition + mkfs ext4 + systemd mount + `add_storage`, one call, synchronous success (`UPID:…:dircreate:felhom-flash:…`).
3. **Verify storage.** `storage.cfg` entry written (above); `findmnt /mnt/pve/felhom-flash``/dev/sdc1 ext4 rw`; fs UUID `81a26531-…`; `disks/directory` now lists felhom-flash (by-uuid device); `disks/list` sdc now `used=ext4`.
4. **Verify Observe.** `pvesh get /nodes/demo-felhom/storage``felhom-flash dir active=1 total=125704151040` (~117 GiB) — the agent's `Observe` source now contains it.
5. **Verify enrollable in controller.** The controller settings page → *"Meghajtók (ügynök nézet)"* now shows:
> **felhom-flash** — `usb · /dev/sdc1 · /mnt/pve/felhom-flash` · **Felhasználói adat** · **Adatot tartalmaz** · **Nem regisztrált** · buttons **Regisztrálás** / Leválasztás / Törlés…
i.e. classified **user-data**, data-bearing detected, **not yet enrolled, with a one-click `Regisztrálás` (enroll = `AttachBind`)**. End-to-end mechanism proven.
**State left for B3:** sdc is a **clean, registered `felhom-flash` PVE storage** (ext4, mounted `/mnt/pve/felhom-flash`, is_mountpoint, in the agent view, classified user-data). It is **PVE-enrolled and "ready-to-enroll-into-guest"** — the final `AttachBind` + guest reboot was intentionally **not** run to avoid disrupting live guest 9201; that is the one remaining step a real customer enroll performs. This is the B3 migration target.
---
## Q7 — Wiring map (net-new vs reused)
### Reused (already exists, no change)
| Capability | Where |
|---|---|
| System-disk resolution (mount table → whole disk) | `storage/role.go:46 SystemDisks` |
| Raw-device role classifier (system vs user-data, fail-safe) | `storage/role.go:133 RoleForRawDevice` |
| Registered-storage role tiering (PBS=backup, etc.) | `storage/role.go:105 RoleForStorage` |
| Data-bearing inspection (blkid + lsblk) | `storage/hostops.go InspectDevice` + `disks.go:526` gate |
| Mount existing fs (by-UUID systemd unit at `/mnt/<name>`) | `storage/mountunit.go`, `HostOps.EnsureMount` |
| Storage view the controller consumes | `storage/observe.go Observe` (iterates ListStorage/NodeStorage) |
| Bind `felhom-data` namespace into guest + reboot | `localapi/guestbind.go:48 AttachBind` |
| Enroll/assign/eject/decommission endpoints | `localapi/disks.go` (`/disks`, `/disks/assign`, `/disks/guest-attach`, …) |
| Durable-id (uuid:) derivation | `storage/durableid.go deriveDurableID` |
### Net-new
| Capability | Lands in | Note |
|---|---|---|
| Raw-disk enumeration (`disks/list`, `disks/lvm`) | `internal/proxmox` (new `ListDisks`, `ListDisksLVM`) | the client has no `/disks/*` method today |
| Whole-disk **wipe** (`wipefs`/`sgdisk`) | `storage.HostOps` (new) | gated by the Q3 offerable predicate |
| **Create dir storage** — format path (`disks/directory` create) | `internal/proxmox` (new `CreateDirectoryStorage`) **or** a host-side `pvesh` call in `HostOps` | one call = partition+mkfs+mount+add_storage |
| Create dir storage — **enroll-as-is** (`pvesm add dir … --is_mountpoint 1`) after `EnsureMount` | `storage.HostOps` (new `AddDirStorage`) | the agent creates **no** PVE storages today |
| Raw-disk **discovery + Q3 offerable filter** (combine disks/list + disks/lvm + SystemDisks + Observe subtraction, whole-disk granularity) | `internal/storage` (new `OfferableDisks`) + a new localapi endpoint e.g. `GET /disks/raw` | the security core; needs the two Q3 hardening checks |
| New localapi endpoints: list-raw, init (format), attach-as-is | `internal/localapi/disks.go` (new handlers) | mirror the existing `/disks/*` handler shape + `withGuest` scoping |
| Controller `storage/init` UI: raw-disk list, identity/layout/recommendation, format-vs-enroll choice | controller (felhom-controller) | the *"🔧 Új meghajtó inicializálása"* / *"🔗 Meglévő meghajtó csatolása"* buttons already exist on the settings page as entry points |
### The controller-decision → agent-task → done handshake
Lives exactly where the existing disk ops live: the controller POSTs the customer's choice to a **new localapi endpoint** (init/format or attach-as-is), `withGuest`-scoped + TLS-pinned, same as `POST /disks/assign` / `POST /disks/guest-attach` today. The agent **re-derives** the safety verdict from device state (never trusting the controller's claim — same discipline as `handleDiskFormat`), executes, and the result surfaces back through `Observe` → the controller's agent-drive view (proven live). No new transport, no hub round-trip: this is host-agent-local, exactly like the current enroll path. Final guest usability is the existing `AttachBind` + reboot.
---
## FORK LIST (operator decides before the build spec)
1. **Discovery source.** `disks/list` + `disks/lvm` + agent mount-table + Observe subtraction (recommended, no extra shell-out for the *safety* decision) — vs additionally shelling `lsblk -J` for richer per-disk display. *Recommendation: both — disks/list+lvm for the safety gate, lsblk for the init-screen detail.*
2. **Safety-gate signals — ship vs hardened.** Mount-table `SystemDisks` only (works on this host via the ESP) — vs **+LVM-PV membership +guest-image backing +whole-disk-granularity enrolled-subtraction** (required to be robust on a host where the system isn't all on one ESP-bearing disk). *Recommendation: hardened — the two Q3 checks are non-negotiable for a customer-facing format gate; the mount-table-only version can offer a non-boot LVM disk.*
3. **Mount-path convention.** Accept PVE default `/mnt/pve/<name>` (free with `disks/directory`, but leaks PVE internals into the guest bind path) — vs roll-our-own `/mnt/<name>` via `EnsureMount` + `pvesm add dir` (matches `felhom-usb`, reuses agent machinery, but re-implements the format steps). *Recommendation: `/mnt/<name>` for consistency with the existing enrolled drive and a clean in-guest bind path.*
4. **Format-vs-enroll-as-is rules.** Single supported-fs partition → enroll-as-is; everything else (no fs / unsupported / multi-partition / ambiguous) → format. *Recommendation: as stated; format on any ambiguity, never silently adopt.*
5. **Endpoint shapes.** New `GET /disks/raw` (offerable list) + `POST /disks/init` (format path) + `POST /disks/attach-existing` (enroll-as-is), all `withGuest`-scoped, agent re-derives the gate. *Recommendation: as stated, mirroring `/disks/assign`.*
6. **v1 scope.** **Format-only first** (the path proven live this spike), enroll-as-is in v1.1 — vs both in v1. *Recommendation: format-only v1 (covers the plug-in-a-blank/junk-drive case, which is the common one), enroll-as-is fast-follow (the `felhom-usb`-style reuse-an-existing-ext4 case).*
7. **Content normalization.** Set `content backup` (or an explicit minimal set) on creation rather than PVE's all-types default. *Recommendation: yes, normalize.*
8. **`content`/storage create transport.** Typed proxmox-client method (`CreateDirectoryStorage`) vs host-side `pvesh` in `HostOps`. *Recommendation: typed client method, consistent with the rest of `internal/proxmox`.*
---
## Live cleanup state
- `sdc` → registered PVE storage **`felhom-flash`** (ext4, `/mnt/pve/felhom-flash`, is_mountpoint, fs-UUID `81a26531-62d8-408d-812f-a178b1d35310`), classified user-data, **not** guest-bound. Intentionally left as the B3 migration target.
- `sda`, `sdb` untouched (serial-fenced throughout).
- No agent/controller code changed; no version bump; no service restarted.