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: — 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//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//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 }