Files
felhom-agent/internal/storage/partitions.go
T
admin 98f3cdd76a
gates / gates (push) Successful in 25s
Security review of 912f7c1: the mkfs wrapper trusts no sibling claim; the internal-place helper fails closed
- felhom-mkfs-guarded: the 0.158.0 exemption for a sibling part mounted at /mnt/<name> + under /mnt/felhom-drives is
  removed — the agent can make that corroborating mount itself. Every mount of every node outside /mnt/felhom-drives
  refuses (kernel mount table); an unlistable disk refuses. CandidatePartition.Formattable is false for every part of a
  disk with a mounted node, so it is never offered for formatting (attach still is).
- felhom-internal-place: a failed lvs listing, a listing without the pool, or a blkid exit other than 0/2 refuses
  (they read as "nothing promised" / "no signature" before).
Every new test was seen failing against the 0.158.0 versions.

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

124 lines
4.9 KiB
Go

package storage
import (
"encoding/json"
"os"
"path/filepath"
"strconv"
"strings"
)
// Partitions as places (agent v0.158.0) — felhom.eu architecture/16-storage-physical-identity.md §6.
//
// A disk that passed the WHOLE-disk unclaimed filter (classifyClaim: not the system disk, no LVM/ZFS/RAID/LUKS/swap
// member on any node, no foreign mount on any node) also lists its partitions, each with its own state:
//
// empty no filesystem, no children → „format this part" (self-serve: the agent's probe says blank) —
// only while NO part of the disk is mounted (Formattable)
// filesystem ext4/xfs (mountable) or another fs → „use this part" when mountable; „format this part" only through
// the customer-confirmed data-bearing path (the agent's own probe)
// in_use already Felhom's own drive → shown, never offered (it is a registered drive already)
//
// A partition with children (LVM, a mapper) is not listed. The design's separate „a filesystem with no files" state is
// folded into `filesystem` on purpose: telling the two apart needs a root read-only mount, and the data-bearing path's
// confirmation is the safer default for both (`16` §13). Creating, deleting or resizing a partition: never here (D5).
// The format binding is the partition's own by-id link (…-partN: the disk id + the partition number) via
// DeviceDurableID, re-resolved before mkfs (wipe_reresolve.go); the attach key is its fs-UUID.
type CandidatePartition struct {
Device string `json:"device"` // e.g. /dev/sdb2
Number int `json:"number,omitempty"`
SizeBytes int64 `json:"size_bytes"`
FSType string `json:"fstype,omitempty"`
State string `json:"state"` // empty | filesystem | in_use
Mountable bool `json:"mountable"`
DurableID string `json:"durable_id,omitempty"` // uuid:<fs-uuid> — the attach key ("" when empty)
MountedAt string `json:"mounted_at,omitempty"` // in_use: Felhom's own mount
// Formattable is false when ANY node of the disk is mounted (another part in use): the root mkfs wrapper refuses
// every mount outside /mnt/felhom-drives on the disk and deliberately does not trust a sibling that claims to be
// Felhom's own (a compromised agent could fake that). Such a part can still be USED (attach erases nothing).
Formattable bool `json:"formattable"`
}
const (
PartEmpty = "empty"
PartFilesystem = "filesystem"
PartInUse = "in_use"
)
// candidatePartitions is the pure lister: the whole disk's lsblk tree → its partitions. sizeOf/numberOf read /sys,
// uuidOf the by-uuid links, owned is the set of mountpoints corroborated as Felhom's own (felhomOwnedMounts).
func candidatePartitions(lsblkJSON []byte, wholeName string, sizeOf func(string) int64, numberOf func(string) int,
uuidOf func(string) (string, bool), owned map[string]bool) []CandidatePartition {
var doc struct {
BlockDevices []lsblkDev `json:"blockdevices"`
}
if json.Unmarshal(lsblkJSON, &doc) != nil {
return nil
}
var out []CandidatePartition
for _, d := range doc.BlockDevices {
if d.Name != wholeName {
continue
}
diskMounted := d.MountPoint != ""
var walk func(n lsblkDev)
walk = func(n lsblkDev) {
if n.MountPoint != "" {
diskMounted = true
}
for _, c := range n.Children {
walk(c)
}
}
for _, c := range d.Children {
walk(c)
}
for _, p := range d.Children {
if len(p.Children) > 0 || memberFSTypes[p.FSType] {
continue // LVM/mapper on it, or a member: not a place (the disk filter refuses these anyway)
}
c := CandidatePartition{Device: "/dev/" + p.Name, Number: numberOf(p.Name), SizeBytes: sizeOf(p.Name), FSType: p.FSType}
switch {
case p.MountPoint != "" && (underFelhomDrives(p.MountPoint) || owned[p.MountPoint]):
c.State, c.MountedAt = PartInUse, p.MountPoint
case p.MountPoint != "":
continue // mounted by something else: never offered (the disk filter refuses the disk anyway)
case p.FSType == "":
c.State = PartEmpty
default:
c.State = PartFilesystem
c.Mountable = mountableFSTypes[p.FSType]
if u, ok := uuidOf(c.Device); ok && u != "" {
c.DurableID = "uuid:" + u
}
}
c.Formattable = c.State != PartInUse && !diskMounted
out = append(out, c)
}
}
return out
}
// readSysPartNumber reads /sys/class/block/<part>/partition (0 when unreadable).
func readSysPartNumber(name string) int {
b, err := os.ReadFile(filepath.Join("/sys/class/block", name, "partition"))
if err != nil {
return 0
}
n, _ := strconv.Atoi(strings.TrimSpace(string(b)))
return n
}
// readSysPartSize reads a partition's size in bytes from /sys/class/block/<part>/size (512-byte sectors).
func readSysPartSize(name string) int64 {
b, err := os.ReadFile(filepath.Join("/sys/class/block", name, "size"))
if err != nil {
return 0
}
s, err := strconv.ParseInt(strings.TrimSpace(string(b)), 10, 64)
if err != nil {
return 0
}
return s * 512
}