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
208 lines
7.7 KiB
Go
208 lines
7.7 KiB
Go
package localapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sync"
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-agent/internal/storage"
|
|
)
|
|
|
|
// The internal data place („Belső tárhely", agent v0.158.0) — felhom.eu architecture/16-storage-physical-identity.md
|
|
// §5, §7. On a one-disk box the root helper configs/felhom-internal-place makes a second thin volume in local-lvm
|
|
// (<vg>/felhom-belso, ext4, label felhom-belso). The installer (a new box) or the signed add_internal_place job (an old
|
|
// box) then writes InternalPlaceRecord to the agent's state dir. From then on the agent treats the volume exactly like
|
|
// an enrolled drive: a Felhom mount unit by fs-UUID at /mnt/belso (EnsureMount), the `enrolled` intent, and — when the
|
|
// controller asks with its own token (POST /disks/guest-attach) — the bind at /mnt/felhom-drives/belso. So it is a
|
|
// HOST mount under the guest's mp8 parent: not in the guest config, not in vzdump, not in PBS (`16` §5).
|
|
//
|
|
// What it is NOT: a drive the household can format, wipe, eject or decommission. Its device is a device-mapper volume
|
|
// on the system disk, so the role gate (roleForMountPath → system) and the format path (ValidateBlockDevice refuses
|
|
// dm) refuse all four — pinned by internalplace_test.go. Its identity is the system disk's (build A), so a Tier-2 copy
|
|
// between it and sys_drive is never a second copy.
|
|
const (
|
|
InternalPlaceName = "belso"
|
|
InternalPlaceWhere = "/mnt/belso"
|
|
// InternalPlaceFile is the record's name inside the agent's state dir.
|
|
InternalPlaceFile = "internal-place.json"
|
|
)
|
|
|
|
var reFSUUID = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
|
|
|
|
// InternalPlaceRecord is the state file the maker writes once the volume exists and is formatted.
|
|
type InternalPlaceRecord struct {
|
|
FSUUID string `json:"fs_uuid"`
|
|
GiB int `json:"gib,omitempty"`
|
|
MadeBy string `json:"made_by,omitempty"` // "installer" | "signed-job"
|
|
MadeAt string `json:"made_at,omitempty"`
|
|
}
|
|
|
|
// ReadInternalPlace reads the record; ok=false when there is none (the common case: a box with no internal place).
|
|
func ReadInternalPlace(file string) (InternalPlaceRecord, bool, error) {
|
|
var rec InternalPlaceRecord
|
|
b, err := os.ReadFile(file)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return rec, false, nil
|
|
}
|
|
if err != nil {
|
|
return rec, false, err
|
|
}
|
|
if err := json.Unmarshal(b, &rec); err != nil {
|
|
return rec, false, fmt.Errorf("internal place record %s: %w", file, err)
|
|
}
|
|
if !reFSUUID.MatchString(rec.FSUUID) {
|
|
return rec, false, fmt.Errorf("internal place record %s: fs_uuid %q is not a filesystem UUID", file, rec.FSUUID)
|
|
}
|
|
return rec, true, nil
|
|
}
|
|
|
|
// WriteInternalPlace writes the record atomically (0600).
|
|
func WriteInternalPlace(file string, rec InternalPlaceRecord) error {
|
|
if !reFSUUID.MatchString(rec.FSUUID) {
|
|
return fmt.Errorf("internal place: fs_uuid %q is not a filesystem UUID", rec.FSUUID)
|
|
}
|
|
b, err := json.MarshalIndent(rec, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmp := file + ".tmp"
|
|
if err := os.WriteFile(tmp, append(b, '\n'), 0o600); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmp, file)
|
|
}
|
|
|
|
// internalPlaceState memoises what EnsureInternalPlace last saw, so the 20-second tick logs a state CHANGE once and
|
|
// does no work on the steady state.
|
|
type internalPlaceState struct {
|
|
mu sync.Mutex
|
|
done string // the fs-UUID that is mounted and recorded enrolled
|
|
lastWhy string // the last reason it could not be ensured (logged once per change)
|
|
}
|
|
|
|
// internalPlaceUUID returns the configured internal place's fs-UUID, or "" when this box has none (or the record is
|
|
// unreadable — logged by EnsureInternalPlace).
|
|
func (s *Server) internalPlaceUUID() string {
|
|
if s.internalPlaceFile == "" {
|
|
return ""
|
|
}
|
|
rec, ok, err := ReadInternalPlace(s.internalPlaceFile)
|
|
if err != nil || !ok {
|
|
return ""
|
|
}
|
|
return rec.FSUUID
|
|
}
|
|
|
|
// isInternalPlace reports whether a host path is this box's internal place: its raw mount (/mnt/belso) or its bind
|
|
// under the guests' shared parent (/mnt/felhom-drives/belso — the path POST /disks/same-disk judges).
|
|
func (s *Server) isInternalPlace(hostPath string) bool {
|
|
p := path.Clean(hostPath)
|
|
return (p == InternalPlaceWhere || p == StableParentDir+"/"+InternalPlaceName) && s.internalPlaceUUID() != ""
|
|
}
|
|
|
|
// EnsureInternalPlace mounts the internal place on the host at /mnt/belso by fs-UUID and records it `enrolled`,
|
|
// when this box has one. Idempotent and cheap on the steady state; safe on the 20-second tick. It never formats,
|
|
// never unmounts, and never mounts anything at /mnt/belso that is not the recorded UUID: a household drive that
|
|
// happens to be named „belso" keeps its mount, and the internal place waits (logged once).
|
|
func (s *Server) EnsureInternalPlace(ctx context.Context) {
|
|
if s.internalPlaceFile == "" {
|
|
return
|
|
}
|
|
rec, ok, err := ReadInternalPlace(s.internalPlaceFile)
|
|
if err != nil {
|
|
s.internalPlaceNote("record unreadable: " + err.Error())
|
|
return
|
|
}
|
|
if !ok {
|
|
return
|
|
}
|
|
uuid := rec.FSUUID
|
|
st := &s.ipState
|
|
st.mu.Lock()
|
|
done := st.done == uuid
|
|
st.mu.Unlock()
|
|
|
|
// What is at /mnt/belso now, if anything?
|
|
mountedUUID, mounted := "", false
|
|
if mounts, merr := s.hostReader().Mounts(); merr == nil {
|
|
for _, m := range mounts {
|
|
if path.Clean(m.MountPoint) == InternalPlaceWhere {
|
|
mounted = true
|
|
if u, okU := s.hostReader().ResolveUUID(m.Device); okU {
|
|
mountedUUID = u
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if mounted && mountedUUID != "" && mountedUUID != uuid {
|
|
s.internalPlaceNote("another filesystem (uuid " + mountedUUID + ") is mounted at " + InternalPlaceWhere + " — the internal place waits")
|
|
return
|
|
}
|
|
// A Felhom unit for /mnt/belso that names another UUID (a household drive called „belso", unplugged): never
|
|
// overwrite it — that would silently take the drive's name and mount point.
|
|
if s.driveTargets != nil {
|
|
if drives, derr := s.driveTargets.Known(ctx); derr == nil {
|
|
for _, d := range drives {
|
|
if path.Clean(d.MountPath) == InternalPlaceWhere && d.UUID != "" && d.UUID != uuid {
|
|
s.internalPlaceNote("a drive unit for " + InternalPlaceWhere + " names another uuid (" + d.UUID + ") — the internal place waits")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if mounted && mountedUUID == uuid {
|
|
if !done {
|
|
s.recordIntent(ctx, InternalPlaceWhere, "enrolled")
|
|
st.mu.Lock()
|
|
st.done, st.lastWhy = uuid, ""
|
|
st.mu.Unlock()
|
|
s.logger.Info("internal place: mounted and enrolled", "where", InternalPlaceWhere, "fs_uuid", uuid, "gib", rec.GiB)
|
|
}
|
|
return
|
|
}
|
|
if s.disks == nil {
|
|
s.internalPlaceNote("disk management is not configured on this host")
|
|
return
|
|
}
|
|
if s.resolveStorageDevice != nil {
|
|
if _, rerr := s.resolveStorageDevice("uuid:" + uuid); rerr != nil {
|
|
s.internalPlaceNote("its volume is not present (uuid " + uuid + ")")
|
|
return
|
|
}
|
|
}
|
|
start := time.Now()
|
|
if err := s.disks.EnsureMount(ctx, storage.MountSpec{Name: InternalPlaceName, UUID: uuid, Where: InternalPlaceWhere, FSType: "ext4"}); err != nil {
|
|
s.internalPlaceNote("mount failed: " + err.Error())
|
|
return
|
|
}
|
|
s.recordIntent(ctx, InternalPlaceWhere, "enrolled")
|
|
st.mu.Lock()
|
|
st.done, st.lastWhy = uuid, ""
|
|
st.mu.Unlock()
|
|
s.logger.Info("internal place: mounted on the host", "where", InternalPlaceWhere, "fs_uuid", uuid, "gib", rec.GiB,
|
|
"duration_ms", time.Since(start).Milliseconds())
|
|
}
|
|
|
|
func (s *Server) internalPlaceNote(why string) {
|
|
st := &s.ipState
|
|
st.mu.Lock()
|
|
changed := st.lastWhy != why
|
|
st.lastWhy, st.done = why, ""
|
|
st.mu.Unlock()
|
|
if changed {
|
|
s.logger.Warn("internal place: not ensured", "why", why)
|
|
} else {
|
|
s.logger.Debug("internal place: still not ensured", "why", why)
|
|
}
|
|
}
|
|
|
|
// InternalPlaceFileIn is the record's path inside a state dir.
|
|
func InternalPlaceFileIn(stateDir string) string { return filepath.Join(stateDir, InternalPlaceFile) }
|