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
180 lines
7.4 KiB
Go
180 lines
7.4 KiB
Go
package storage
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Durable BLOCK-DEVICE identity for the slice-10B operator-signed wipe (anti-retarget). The
|
|
// signed op binds a DURABLE id (a WWN / hardware serial, falling back to a filesystem UUID), and
|
|
// execution re-resolves it to the CURRENT /dev path — so "wipe device X" wipes that exact physical
|
|
// device, never whatever happens to be at /dev/sdb now. These reads are world-readable udev
|
|
// symlinks under /dev/disk/by-id and /dev/disk/by-uuid — NO privilege and NO subprocess (the
|
|
// root-CLI fence is untouched).
|
|
//
|
|
// devDiskRoot is overridable for tests (a fake /dev/disk layout).
|
|
var devDiskRoot = "/dev/disk"
|
|
|
|
// durable-id scheme prefixes. byid: a /dev/disk/by-id/<name> entry (preferred — wwn/serial,
|
|
// survives reformat + re-cabling); byuuid: a filesystem UUID (fallback — survives re-cabling but
|
|
// not a reformat, acceptable for a one-shot wipe resolved immediately before wiping).
|
|
const (
|
|
durableByID = "byid:"
|
|
durableByUUID = "byuuid:"
|
|
// partFSUUIDSep joins a partition's by-id link and its fs-UUID in one wipe id (v0.158.0, `16` §6).
|
|
partFSUUIDSep = "+fsuuid:"
|
|
)
|
|
|
|
// rePartLink matches a by-id link of a PARTITION (udev appends -part<N> to the disk's link).
|
|
var rePartLink = regexp.MustCompile(`-part[1-9][0-9]{0,2}$`)
|
|
|
|
// byIDPriority ranks /dev/disk/by-id link prefixes most-stable-first: wwn (hardware world-wide
|
|
// name) > nvme-eui (NVMe EUI) > nvme-/ata-/scsi-/usb- (model+serial). dm-/lvm-/md- names are
|
|
// excluded (they are mapper constructs, not the physical disk we want to bind a wipe to).
|
|
var byIDPriority = []string{"wwn-", "nvme-eui.", "nvme-", "ata-", "scsi-", "usb-"}
|
|
|
|
// DeviceDurableID derives a stable durable identifier for a block device by scanning the
|
|
// world-readable udev symlinks. Prefers a by-id (wwn/serial) link, then a filesystem UUID. Errors
|
|
// when neither is resolvable (a device with no durable identity cannot be safely wipe-bound).
|
|
func DeviceDurableID(device string) (string, error) {
|
|
target, err := filepath.EvalSymlinks(device)
|
|
if err != nil {
|
|
return "", fmt.Errorf("storage: resolve %s: %w", device, err)
|
|
}
|
|
// 1. best by-id link pointing at this device.
|
|
if name := bestByIDLink(target); name != "" {
|
|
// v0.158.0 (felhom.eu `16` §6): a PARTITION's link already carries the disk id + the part number
|
|
// (wwn-…-part2). When the part holds a filesystem, its fs-UUID is bound too, so a part that was reformatted —
|
|
// or another disk's part2 that took the link — derives a different id and the wipe refuses.
|
|
if rePartLink.MatchString(name) {
|
|
if uuid := byUUIDFor(target); uuid != "" {
|
|
return durableByID + name + partFSUUIDSep + uuid, nil
|
|
}
|
|
}
|
|
return durableByID + name, nil
|
|
}
|
|
// 2. fallback: a filesystem UUID symlink.
|
|
if uuid := byUUIDFor(target); uuid != "" {
|
|
return durableByUUID + uuid, nil
|
|
}
|
|
return "", fmt.Errorf("storage: no durable id (wwn/serial/uuid) for device %s", device)
|
|
}
|
|
|
|
// ResolveDurableDevice resolves a durable id back to the CURRENT canonical /dev path, or errors if
|
|
// it no longer resolves (the device was physically removed/replaced — the wipe must then refuse).
|
|
func ResolveDurableDevice(durableID string) (string, error) {
|
|
switch {
|
|
case strings.HasPrefix(durableID, durableByID):
|
|
name := strings.TrimPrefix(durableID, durableByID)
|
|
fsUUID := ""
|
|
if i := strings.Index(name, partFSUUIDSep); i >= 0 {
|
|
name, fsUUID = name[:i], name[i+len(partFSUUIDSep):]
|
|
if !rePartLink.MatchString(name) || !safeLinkName(fsUUID) {
|
|
return "", fmt.Errorf("storage: unsafe durable id %q", durableID)
|
|
}
|
|
}
|
|
if !safeLinkName(name) {
|
|
return "", fmt.Errorf("storage: unsafe durable id %q", durableID)
|
|
}
|
|
dev, err := filepath.EvalSymlinks(filepath.Join(devDiskRoot, "by-id", name))
|
|
if err != nil || fsUUID == "" {
|
|
return dev, err
|
|
}
|
|
// Both halves must name the SAME device now (`16` §6): the part by its disk id + number, and its filesystem.
|
|
byFS, ferr := filepath.EvalSymlinks(filepath.Join(devDiskRoot, "by-uuid", fsUUID))
|
|
if ferr != nil {
|
|
return "", fmt.Errorf("storage: durable id %q: its filesystem %s no longer resolves: %w", durableID, fsUUID, ferr)
|
|
}
|
|
if byFS != dev {
|
|
return "", fmt.Errorf("storage: durable id %q: the part (%s) and its filesystem (%s) are different devices now", durableID, dev, byFS)
|
|
}
|
|
return dev, nil
|
|
case strings.HasPrefix(durableID, durableByUUID):
|
|
uuid := strings.TrimPrefix(durableID, durableByUUID)
|
|
if !safeLinkName(uuid) {
|
|
return "", fmt.Errorf("storage: unsafe durable id %q", durableID)
|
|
}
|
|
return filepath.EvalSymlinks(filepath.Join(devDiskRoot, "by-uuid", uuid))
|
|
default:
|
|
// A bare path or unknown scheme is REFUSED — a wipe must bind to a durable id, never a
|
|
// mutable /dev path (the anti-retarget invariant).
|
|
return "", fmt.Errorf("storage: durable id %q is not a durable scheme (byid:/byuuid:) — refusing path-only binding", durableID)
|
|
}
|
|
}
|
|
|
|
// ResolveStorageDevice resolves an enrolled STORAGE durable-id (the `uuid:<fs-uuid>` scheme that
|
|
// deriveDurableID emits for usb/local-dir drives) to its CURRENT backing /dev path by re-scanning
|
|
// /dev/disk/by-uuid. This is the load-bearing host-reboot fix: kernel re-enumeration can move a drive
|
|
// from /dev/sdb to /dev/sdc, so a remount MUST re-resolve by UUID (never trust a remembered node) — the
|
|
// reshuffle is then a no-op. Errors if the UUID no longer resolves (the drive is genuinely absent), so a
|
|
// caller can skip re-mounting a gone drive instead of failing on a stale node.
|
|
func ResolveStorageDevice(durableID string) (string, error) {
|
|
const scheme = "uuid:"
|
|
if !strings.HasPrefix(durableID, scheme) {
|
|
return "", fmt.Errorf("storage: durable id %q is not the uuid: scheme — cannot resolve by filesystem UUID", durableID)
|
|
}
|
|
uuid := strings.TrimPrefix(durableID, scheme)
|
|
if !safeLinkName(uuid) {
|
|
return "", fmt.Errorf("storage: unsafe durable id %q", durableID)
|
|
}
|
|
return filepath.EvalSymlinks(filepath.Join(devDiskRoot, "by-uuid", uuid))
|
|
}
|
|
|
|
// bestByIDLink returns the highest-priority /dev/disk/by-id link name whose target is `device`
|
|
// (already symlink-resolved), or "" if none.
|
|
func bestByIDLink(device string) string {
|
|
dir := filepath.Join(devDiskRoot, "by-id")
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
// collect matching names, then pick by priority (then lexically for determinism).
|
|
var matches []string
|
|
for _, e := range entries {
|
|
name := e.Name()
|
|
if tgt, err := filepath.EvalSymlinks(filepath.Join(dir, name)); err == nil && tgt == device {
|
|
matches = append(matches, name)
|
|
}
|
|
}
|
|
if len(matches) == 0 {
|
|
return ""
|
|
}
|
|
sort.Strings(matches)
|
|
for _, pfx := range byIDPriority {
|
|
for _, name := range matches {
|
|
if strings.HasPrefix(name, pfx) {
|
|
return name
|
|
}
|
|
}
|
|
}
|
|
return matches[0] // some by-id link exists but not a recognized prefix — still durable enough
|
|
}
|
|
|
|
// byUUIDFor returns the filesystem UUID whose by-uuid link targets `device`, or "".
|
|
func byUUIDFor(device string) string {
|
|
dir := filepath.Join(devDiskRoot, "by-uuid")
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
for _, e := range entries {
|
|
if tgt, err := filepath.EvalSymlinks(filepath.Join(dir, e.Name())); err == nil && tgt == device {
|
|
return e.Name()
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// safeLinkName rejects a durable-id component that could escape the by-id/by-uuid dir (path
|
|
// traversal / separators) — defense even though the value is operator-signed.
|
|
func safeLinkName(s string) bool {
|
|
if s == "" || strings.ContainsAny(s, "/\x00") || s == "." || s == ".." {
|
|
return false
|
|
}
|
|
return true
|
|
}
|