912f7c127e
gates / gates (push) Successful in 27s
- internal place: the record /var/lib/felhom-agent/internal-place.json -> mounted at /mnt/belso by fs-UUID like an enrolled drive (startup + 20 s tick), kind "internal" in GET /disks; never takes a household drive's /mnt/belso; eject/decommission/format refused (dm on the system disk). - configs/felhom-internal-place: the ruled size rule and the only lvcreate/mkfs of a system-disk volume (installer + the new signed op add_internal_place, operator key, no parameters). - partitions listed per free disk (empty / filesystem / in_use); a data-bearing part's wipe id binds disk id + part number + fs-UUID. - felhom-mkfs-guarded: Felhom's own sibling drive no longer blocks formatting the free part; an unlistable disk refuses. Tests red-proved (script mutations, Go mutations, the old wrapper measured against the new tests). Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
105 lines
4.2 KiB
Go
105 lines
4.2 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)
|
|
// 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
|
|
}
|
|
|
|
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
|
|
}
|
|
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
|
|
}
|
|
}
|
|
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
|
|
}
|