98f3cdd76a
gates / gates (push) Successful in 25s
- 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
124 lines
4.9 KiB
Go
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
|
|
}
|