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
23 KiB
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 in01: [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 andHDD_PATHapps 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 andsys_drivenever 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_driveas the media drive — it is insidemp0, so films enter every whole-guest copy, local and off-site. - Grow
mp0without a cap — the same problem, at full size. - Keep
st_devand 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.
12. Decisions for the operator (D1–D6) — RULED 2026-10-15: all six as recommended
| # | 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_devrule and logs it once. - §11 rejects
sys_drivefor household data, but the drives page still offers/mnt/sys_driveto 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.