Files
felhom.eu/documentation/architecture/16-storage-physical-identity.md
T
admin 920c1aaf84
gates / gates (push) Successful in 6m39s
storage identity build B: live proof, R-1068/R-1079/R-1082/R-1084 closed, R-1093/R-1094 opened, D15, golden 0.318.0 record, backup page
A fresh one-disk install (512 GB VM, installer 1.33.0, agent 0.158.1 + golden 0.318.0 vouched): data volume 100 GiB, data
area 213 GiB registered as the default drive with no press, Jellyfin with no extra step, nothing of the area in a whole-box
copy; partitions formatted and used; the honest backup offer on a VM; /mnt/sys_drive never offered (no box had it).
16 §13 build B status; capability map D15; poster facts (drawing needs a refresh, STATUS); the backup page HU+EN now that
it is true; REPORT-storage-identity-buildB-2026-10-16.md.

Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
2026-10-16 10:07:13 +02:00

23 KiB
Raw Blame History

16 — Storage that knows its physical disks

What this is: the design that lets Felhom know, for every storage place, which physical disk(s) it sits on — and use that to make backup tiers honest, to give one-disk boxes a place for household data, and to allow partitions. Operator ruling 2026-10-15: „Felhom must support drives split into parts; the agent and the controller must know whether two storage places sit on the same physical disk." Triggered by the OMV test (audits/omv-vm-2026-10-15/, R-1068). Spike: audits/storage-identity-2026-10-15/ (Part B there). Marks as in 01: [FACT] read in a named file or measured; [DESIGN] a proposal. Status: ACCEPTED 2026-10-15 — the operator ruled D1–D6 as recommended (§12). Build A shipped 2026-10-15 (§3, §4, §5's D4 sentence, §8, §10); build B shipped 2026-10-16 (§5's internal place, §6, §7, §9) — what is built, and where the build differs from this text, is §13. Paths: A = felhom-agent/, C = felhom-controller/controller/, E = felhom.eu/.

1. Today, in one table (Part A)

Place Found by Identified by Mounted What lives there The backup page says
Host root / PVE local PVE storage local [FACT] A/internal/storage/observe.go:424; never a candidate [FACT] A/internal/storage/role.go:53-79 path: / store:local [FACT] A/internal/storage/durableid.go:54-67; role system host /, /var/lib/vz the local whole-guest copies (vzdump, retention 3) [FACT] E/scripts/felhom-host-install.sh:730-734; 07 §6.1 backup.target.degraded „…ugyanazon a lemezen van, mint a rendszer…" [FACT] C/internal/web/backup_target_offer.go:78-100
Guest mp0 /var/lib/felhom with folders docker/ and sys_drive/ (bound to /var/lib/docker, /mnt/sys_drive) config default system_data_path=/mnt/sys_drive [FACT] C/internal/config/config.go:166 path only; „same disk" = st_dev [FACT] C/internal/system/mounts_linux.go:225-245 thin LV in local-lvm, backup=1 [FACT] A/configs/build-golden.sh:105, :221-225; measured on all 3 boxes app volumes; Tier-1 of apps with no drive [FACT] C/internal/backup/backup.go:429-438; Tier-2 state-only fallback [FACT] C/internal/backup/tier2.go:157-178; inside every whole-guest copy „Csak egy másolat készül (nincs második meghajtó)…" — a Go constant, not i18n [FACT] backup.go:359-361, :1387-1410
Enrolled drive (/mnt/<name> on the host → /mnt/felhom-drives/<name>) GET /disks/candidates: whole disks only [FACT] A/internal/storage/candidates.go:33-53 mount: uuid:<fs-uuid>; wipe: byid: wwn > nvme-eui > … [FACT] A/internal/storage/durable_device.go:32-51 systemd unit by fs-UUID through FELHOM_MOUNT [FACT] A/internal/storage/mountunit.go:50-65; host bind into mp8 HDD_PATH apps, media binds [FACT] C/internal/stacks/media_library.go:24-33; Tier-1/Tier-2; optionally the whole-guest target felhom-backup; not in vzdump (mp8 is a host bind) [FACT] 07:477-480 Tier-2 badges; „Nincs 2. (off-drive) másolat" [FACT] C/internal/i18n/locales/hu.json:303
Network share customer input [FACT] A/internal/localapi/netstorage.go:36-40 none („no durable-id, no SMART") [FACT] netstorage.go:15-19 .automount under /mnt/felhom-drives/<name> [FACT] A/internal/storage/netmount.go:18-31 browse only; never media, never Tier-2 [FACT] media_library.go:83; tier2.go:141-143 err.backup.halozati_tarolo_nem_lehet_a_2 [FACT] tier2.go:37-40
USB disk same as an enrolled drive; transport never decides [FACT] C/internal/settings/settings.go:609-614 same; the bridge may pass the real identity (N100: yes) [FACT] spike case 4a same same same; SMART may be missing → „Nincs adat"

[FACT] What compares physical disks today: the controller compares only paths and st_dev (its own comment: „two partitions on one physical disk look "different" here", mounts_linux.go:240-242). The agent has a whole-disk compare (SameWholeDisk, role.go:207-216) but uses it only for eject/decommission roles. [FACT] What knows about partitions: the agent accepts /dev/sdb1 for inspect and mkfs (A/internal/storage/validate.go:28-32) but judges the whole disk (claim.go:144-163), lists only whole disks, and calls a disk with a partition table or any filesystem „data-bearing" (candidates.go:66-85). Measured: an empty partition next to another is never offered (spike).

2. The spike in five lines (Part B)

[FACT] A read-only walk (partition → parent; dm/LVM → slaves; loop → backing file → its disk) named the right physical disk in every case on real devices: one-disk N100 and demo-hp (every system place → the one SSD), two disks (system vs enrolled drive), two partitions of one disk (both → the same disk), a USB bridge that passes the identity. Inside a VM (Tester 1) it can only say „unknown": virtual disks on the same host disk and on a different host disk look identical (QEMU HARDDISK, serial = the slot name drive-scsiN). Not measured: a USB box that hides the serial (R-1077). Seen: a cheap SSD with a vendor-less WWN (OUI 000000, R-1076). Full table: audits/storage-identity-2026-10-15/README.md.

3. The model

[DESIGN] A storage place is anything Felhom stores on: the system data (mp0), the new internal data place (§5), an enrolled whole disk, an enrolled partition, a network share. Each place carries:

Field Meaning
kind system · internal · disk · partition · network
physical_disks the ids of the whole physical disks under it, e.g. ["serial:ATA:SanDisk_X600:182195804614"]
identity proven — every disk under it has a stable id the agent trusts; unknown — otherwise, with a reason code: vm, no_serial, weak_wwn, usb_bridge, network

Disk id rule [DESIGN]: serial:<bus>:<model>:<serial> from udev (ID_BUS, ID_MODEL, ID_SERIAL_SHORT); NVMe eui/nguid when present. A WWN is used only when its NAA OUI is not 000000 (spike case 4c). Model + serial, not WWN, is the primary key: two disks are „the same" only when the key is equal.

Unknown rule [DESIGN]: systemd-detect-virt ≠ none → every disk is unknown/vm. A disk with no serial → no_serial. A USB disk whose serial is the bridge's (all the same digit, or equal to a known bridge list) → usb_bridge. A network share → network, physical_disks: [].

How it is computed [DESIGN]: by the agent, as root, from /sys and udev only (no controller input): the walk the spike used, built on what exists — walkSlaves/physicalDisksOf (role.go:81-131), parentDisk (A/internal/storage/hostread.go:183-201) — plus the loop step and the mapping guest path → host mount (/mnt/felhom-drives/<name> → /mnt/<name> → device; mp0 → its LV).

When [DESIGN]: at agent start, on every disk hot-plug event the agent already reacts to, on enrol/attach, and again right before each Tier-2 run (a disk swapped in the same bay must not inherit the old verdict).

Reported [DESIGN]: GET /disks gains physical_disks + identity per place; the hub report's storage_targets gains the same two fields (A/internal/hub/report.go:379-438), so the hub sees the agent's verdict directly. The controller stores nothing of it; it asks.

4. Backup tiers

[DESIGN] The rule: a Tier-2 copy counts as a second copy only when the source place and the target place share no physical disk and both identities are proven. The decision is the agent's (POST /disks/same-disk with the two guest paths → {different: bool, proven: bool, reason}), replacing sameDevice/st_dev in tier2.go:83-179 and hasOffDriveTarget (backup.go:1390-1402).

Situation Tier-2 The one-copy notice Household sentence (HU / EN)
Different disks, both proven made, counted hidden —
Same disk (two partitions, internal place + system) per decision D1 shown „Csak egy helyi másolat készül: nincs második lemez, vagy ugyanazon a lemezen van, mint az adatok. Ez a véletlen törlés ellen véd, lemezhiba ellen nem. Lemezhiba ellen egy másik lemez vagy a távoli mentés véd." / „Only one local copy is made: there is no second disk, or it is on the same disk as the data. This protects against mistakes, not against a disk failure. A different disk or the off-site copy protects against a disk failure."
Unknown (VM, hidden serial) per decision D2 shown „A Felhom nem tudja ellenőrizni, hogy a második meghajtó valóban másik lemez-e (virtuális gépben és egyes USB-házakban ez nem látszik), ezért nem számolja második másolatnak." / „Felhom cannot check that the second drive is really a different disk (inside a virtual machine and behind some USB enclosures this is not visible), so it does not count it as a second copy."
Network share never (unchanged) shown unchanged

The notice moves from a Go constant to i18n, in the informal voice (closes R-1072's half). Off-site (Tier-3, PBS) and the local whole-guest copy are unchanged. [FACT] Today Tester 1 counts Tier-2 copies between virtual disks that all sit on one host NVMe (R-1074).

5. One-disk boxes — the internal data place

[FACT] The install button for a data-folder app finds no drive on a one-disk box and refuses with the raw field name (R-1068). [FACT] The only place on that disk today, sys_drive, is a folder of mp0, which is backup=1: anything put there rides in every local and off-site whole-guest copy (07 §6.1, §7.5.1). Films there would overflow the local copy (R-548) and travel to ep0.

[DESIGN] The installer (new installs) creates a second thin LV on local-lvm, the internal data place („Belső tárhely"), formats it, and the agent mounts it on the host by fs-UUID at /mnt/belso, bound to /mnt/felhom-drives/belso — exactly like an enrolled drive. So:

  • it is not in vzdump (a host mount, like every drive) and not in PBS;
  • the controller registers it as the default drive (kind: internal), so media and HDD_PATH apps install with no extra step — no sentence at all; the media binds cover it like any drive;
  • its physical_disks = the system disk, so a Tier-2 copy between it and sys_drive never counts (§4);
  • the agent's format/wipe guard keeps refusing it as a system disk (role system, claim.go:72) — only the installer makes or removes it.

On a one-disk box without an internal place (old boxes, §9), the install of a data-folder app answers in words: „Ehhez az alkalmazáshoz egy adattároló kell. A Meghajtók oldalon adhatsz hozzá egyet." — never the field name. Registering sys_drive as a drive (the „way through" R-1068 guessed) is rejected (§10).

6. Partitions

[DESIGN] A place may be one partition. The agent lists, under each whole disk, its partitions as separate candidates with their own state:

Partition state Offered as Who may
empty (no signature, no children) „format this part" the household, self-serve (it holds nothing)
a filesystem with no files (agent mounts read-only, counts entries) „use this part" or „format this part" the household
a filesystem with files „use this part" (attach) only; format = the customer-confirmed data-bearing path, bound to the partition's durable id the household with confirmation (today's user-data rule, A/internal/localapi/disks.go:912-946)
LVM / RAID / LUKS / swap / mounted elsewhere / on the system disk not offered nobody (claim.go:72-103, unchanged)

The classifier judges the partition (its own signature, children, mounts) and still refuses on the whole disk's system role and on any sibling that is foreign-mounted or a PV — those siblings make the disk unsafe, not the partition. A partition's durable id = <disk id>:part<N> + its fs-UUID; the wipe re-resolve (wipe_reresolve.go:53-93) binds both. Creating or deleting partitions: never from the dashboard in this round — the installer only (D5).

7. The installer

[FACT] Today step_grows gives the guest's one data volume (mp0) 70 / 130 / 250 GiB by the thin pool's free space (<150 / ≥150 / ≥300 GiB), and nothing above 250 (E/scripts/felhom-host-install.sh:2261-2275). Measured pools: 128 GB disk → 54 GiB, 200 GB → 113 GiB, 512 GB → 349 GiB (PVE default layout; root capped at 96 GiB).

[DESIGN] The rule: mp0 = 70 GiB (pool < 300), 130 (300–800), 250 (≥ 800). Reserve = max(16 GiB, 10 % of the pool), kept free for thin snapshots during a whole-guest copy, image pulls during updates, and the restore test (which sizes itself, A19). Internal place = pool − 32 (rootfs) − mp0 − reserve, made only when ≥ 20 GiB. The local whole-guest copies stay on host local (root), which the data place does not touch — R-548 is not made worse: mp0 does not grow.

Disk Thin pool (GiB) Today: data volume / unused Proposed mp0 Reserve Internal place
256 GB ≈ 140 (estimate) 70 / ≈ 38 70 16 ≈ 22 (settings and small apps, not a film library)
512 GB 349 (N100, measured) 250 / ≈ 67 130 35 ≈ 152
1 TB ≈ 800 (estimate) 250 / ≈ 518 250 80 ≈ 438
2 TB ≈ 1,720 (estimate) 250 / ≈ 1,438 250 172 ≈ 1,266

RULED 2026-10-16 (operator, after the build B space check audits/storage-identity-buildB-2026-10-15/SPACE-CHECK.md): these numbers replace the table above. mp0 = 50 GiB (pool < 300), 100 (300–800), 130 (≥ 800); reserve = max(16 GiB, 10 %), at most 48 GiB; internal place = pool − 32 − mp0 − reserve, only when ≥ 20 GiB; nothing promised beyond pool − reserve. Result: ≈ 42 / 182 / 590 / 1,510 GiB for 256 GB / 512 GB / 1 TB / 2 TB. The host root is left as the Proxmox installer makes it (SPACE-CHECK §3).

8. Virtual machines

[FACT] Inside a VM the agent cannot tell virtual disks apart by host disk (spike cases 5–6). [DESIGN] Every place on a VM box is identity: unknown/vm; Tier-2 there never counts (D2), and the page says why. The OMV guide tells the household: a second disk given to the Felhom VM must be a different physical disk on the host, passed whole, on bus sata (R-1069); otherwise it adds space, not safety. Disk health is OMV's job there (R-1073).

9. Existing boxes

[DESIGN] The agent computes identities on its next update — no migration, no change to what is stored. A box with two real disks keeps every Tier-2 copy and every sentence (both proven, different). A box where two places share a disk (none known in the field; Tester 1 in its VM) shows the honest notice. One-disk boxes get no internal place by themselves; adding one later is a signed operator job (an LV in free thin space), not a household button. Hub: the two new fields appear in reports; nothing is backfilled.

10. Threat note

[DESIGN] „Different disks" is agent-computed evidence, like the data-bearing verdict (03 §6): the controller can only ask, never assert. A compromised controller could show a false sentence on its own page, but cannot make the agent call two places different, cannot change the hub's copy of the verdict, and cannot format a partition the agent judges data-bearing. The agent never trusts a guest path it cannot map to a host mount it made.

11. Rejected options (one line each)

  • Register sys_drive as the media drive — it is inside mp0, so films enter every whole-guest copy, local and off-site.
  • Grow mp0 without a cap — the same problem, at full size.
  • Keep st_dev and add a partition check in the controller — the controller cannot see LVM, loop or the host; the agent can.
  • WWN as the primary disk key — cheap disks ship vendor-less WWNs (N100).
  • Ask the hypervisor (OMV/libvirt) which host disk backs a virtual disk — Felhom has no access there, and it would trust a party it does not control.
  • Let the dashboard create partitions — a new destructive power for the household with no recovery; not needed for the cases in hand.
# Question Options Recommendation If you do nothing
D1 A second copy on the same disk (two partitions, internal place → sys_drive)? (a) do not make it; say „only one copy" honestly · (b) make it, label it „same disk: against mistakes, not against a disk failure" (a) — the first copy already protects against mistakes; (b) spends space and invites a false sense of safety today's st_dev rule stays: two partitions would count as two disks
D2 Unknown identity (VM, hidden serial)? (a) never count it; the copy is still made (it may be a real second disk) and the notice stays · (b) the household confirms „these are different disks" once, recorded with the disk ids (a) for now; revisit (b) when a VM install is a supported shape VM boxes keep counting same-disk copies as second copies (Tester 1 does today)
D3 The installer's internal place on a one-disk box, and its reserve (a) §7: mp0 70/130/250, reserve max(16 GiB, 10 %), the rest internal · (b) a larger reserve, 20 % · (c) no internal place; household adds a drive (a) new one-disk boxes keep refusing media apps (R-1068) and leave most of a big disk unused
D4 Media and data-folder apps on a one-disk old box (a) a plain sentence pointing to Meghajtók; internal place only by a signed operator job · (b) register sys_drive (a) — (b) puts films into the whole-guest copies the raw field error stays (R-1068)
D5 Who creates or deletes partitions (a) the installer only, this round · (b) also a dashboard wizard (a) — (nothing is built either way)
D6 R-1071 — films from a NAS share, read-only, for Jellyfin (a) a separate decision; this design only labels a NAS place network, never a copy target · (b) fold a read-only media bind kind into this build (a) — the model makes it easier (a NAS becomes one more place kind with clear rules), but it is a media decision, not an identity one stays open as R-1071

After the rulings, the build task changes: agent (place identity in /disks and the report, the same-disk call, partition candidates, the loop step) · controller (Tier-2 and the notice ask the agent; i18n sentences; the install refusal in words; register the internal place) · installer (step_grows + the internal LV) · hub (store and show the two fields) · website and guides (the OMV guide's second-disk line; the backup explanation page).

13. Build status

Build A — honest backup copies (D1, D2, D4), shipped 2026-10-15 (agent v0.157.0, controller v0.317.0 with MinAgent 0.157.0, hub v0.154.0; evidence audits/storage-identity-buildA-2026-10-15/):

Section Built Proven
§3 model the agent computes kind, physical_disks, identity + reason for every PVE storage and every own mount; GET /disks and the hub report carry them live: N100 and demo-hp proven, Tester 1 unknown/vm, the PBS store network; the hub stores and shows them
§4 tiers POST /disks/same-disk; Tier-2 choice, the pre-run guard and the one-copy notice ask it (D1: same disk → no copy; D2: unknown → copy made, not counted); a NAS never counts and never hides the notice (R-1075) live: Tester 1 shows the „cannot check" notice and its copies are made, not counted; demo-hp and N100 count theirs, no notice. Unit only: the same-disk skip (no box has two places on one disk), the NAS notice (no box with a NAS can reach the agent, R-1083)
§5 D4 the install of a data-folder app on a box with no drive answers „Ehhez az alkalmazáshoz egy adattároló kell. A Meghajtók oldalon adhatsz hozzá egyet." with a link to the Meghajtók page; never the field name unit only — no one-disk box was at hand
§8 VMs every place on a VM box is unknown/vm live, Tester 1
§10 the agent maps only paths to mounts it made; an absent drive (its folder on the root disk) is unmapped, never „the system disk" unit, and live (unmapped for an absent drive on demo-hp)

Where the build differs from the text above:

  • §3 „When": the identity is computed on every read (each GET /disks, each report, each same-disk call), not cached at start and on hot-plug. The rule's purpose — a swapped disk never inherits an old verdict — holds with no cache.
  • usb_bridge: built from the spike's rule, never seen on a real device (the N100's USB enclosure passes the real serial).
  • §4 said the notice closes „R-1072's half": it closed all of R-1072 — the other sentence was already informal.
  • An older agent (no same-disk call): the controller keeps the pre-build st_dev rule and logs it once.
  • §11 rejects sys_drive for household data, but the drives page still offers /mnt/sys_drive to attach (R-280) — R-1082.

Build B — the internal place, partitions, the installer (2026-10-16) (agent v0.158.1 — v0.158.0 superseded; controller v0.318.0; installer 1.33.0; evidence audits/storage-identity-buildB-2026-10-15/; the D3 space check: its SPACE-CHECK.md, sizes ruled above in §7):

Section Built Where it differs from the text above
§5 internal place the root helper felhom-internal-place (bundle) makes pve/felhom-belso by the ruled rule; the installer's step 5b (appliance only) and the signed add_internal_place (an old box) run it; the agent mounts it at /mnt/belso like a drive (kind internal); the controller binds and registers it as the default drive the place is made only in appliance mode (a byo host's pool is the owner's); every failing read in the helper refuses (security review of 0.158.0)
§6 partitions each part of a free disk is listed (empty / filesystem / in_use); „format this part", „use this part"; a data-bearing part's wipe id binds disk id + part number + fs-UUID the „filesystem with no files" state is folded into filesystem (no root read-only mount; the typed confirmation covers both). A part can be formatted only while nothing on its disk is mounted: the root mkfs wrapper refuses any mounted node and deliberately trusts no „it is Felhom's own drive" claim (the agent could fake it — security review of 0.158.0). So a household formats its free parts before using any part; a free part next to an in-use drive can still be USED, not formatted
§7 installer step_grows by the ruled table; step 5b the host root is left as Proxmox makes it (SPACE-CHECK §3)
§9 old boxes the signed add_internal_place (no parameters; the size is the rule's) —

Proven live 2026-10-16 (audits/storage-identity-buildB-2026-10-15/README.md): a fresh one-disk install (512 GB VM, the public installer 1.33.0, agent 0.158.1 and golden 0.318.0 vouched) → data volume 100 GiB, internal place 213 GiB, registered as the default drive with no press; Jellyfin installed with no extra step onto it; a whole-box copy holds nothing of it; partitions formatted and used through the wizard; the honest backup offer on a VM. Not as written: an empty part asks for the typed confirmation (the probe reads the parent's partition table — R-1093); a 200 GB disk gets no internal place under the ruled sizes (15 GiB < 20).

Also in build B: R-1084 (the whole-box backup offer asks the same-disk question) and R-1082 (/mnt/sys_drive is never offered to attach, by device). A bundle that adds a path needs a step bundle on boxes with an older wrapper (R16, R-880): 0.158.1 was delivered through 0.158.1-step1.