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 // (/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) }