c5daa9480d
gates / gates (push) Successful in 31s
The agent names the physical disk(s) under each storage place from /sys, udev and /proc/mounts only
(partition -> disk, dm/LVM thin -> slaves, loop -> backing file), keys them (eui / serial:<bus>:<model>
:<serial> / vendor WWN) and says proven or unknown (vm, network, usb_bridge, weak_wwn, no_serial).
GET /disks and the hub report's storage_targets gain kind, physical_disks, identity,
identity_reason (golden updated; the hub copy changes in the same session). POST /disks/same-disk
maps two guest paths to places the agent itself made and answers {different, proven, reason};
an absent drive is unmapped, never the root disk. Tests on fixture trees for every spike case,
red-proofed. CHANGELOG entry follows the release (release-complete gate).
Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
147 lines
5.9 KiB
Go
147 lines
5.9 KiB
Go
package localapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
|
|
"gitea.dooplex.hu/admin/felhom-agent/internal/storage"
|
|
)
|
|
|
|
// POST /disks/same-disk (`16` §4) — the controller's Tier-2 question, answered from the agent's own read.
|
|
|
|
type fakeIdentHost struct {
|
|
mounts []storage.Mount
|
|
parent map[string]string
|
|
props map[string]map[string]string
|
|
virt string
|
|
}
|
|
|
|
func (f *fakeIdentHost) Mounts() ([]storage.Mount, error) { return f.mounts, nil }
|
|
func (f *fakeIdentHost) KernelName(dev string) (string, bool) {
|
|
if len(dev) > 5 && dev[:5] == "/dev/" {
|
|
return dev[5:], true
|
|
}
|
|
return "", false
|
|
}
|
|
func (f *fakeIdentHost) Parent(n string) (string, bool) { p, ok := f.parent[n]; return p, ok }
|
|
func (f *fakeIdentHost) Slaves(string) []string { return nil }
|
|
func (f *fakeIdentHost) LoopBackingFile(string) (string, bool) { return "", false }
|
|
func (f *fakeIdentHost) UdevProps(n string) map[string]string { return f.props[n] }
|
|
func (f *fakeIdentHost) Virt() string { return f.virt }
|
|
|
|
// sameDiskRig: guest 8200 (token A) with mp0 on local-lvm (the system SSD, proven) + the given mounts.
|
|
func sameDiskRig(t *testing.T, host *fakeIdentHost, localLVM hub.StorageTarget) http.Handler {
|
|
t.Helper()
|
|
fg := &fakeGuestsCfg{mounts: map[int]map[string]string{8200: {
|
|
"mp0": "local-lvm:vm-8200-disk-1,mp=/var/lib/felhom,backup=1,size=70G",
|
|
"mp8": "/mnt/felhom-drives,mp=/mnt/felhom-drives",
|
|
}}}
|
|
srv, err := NewServer(Options{
|
|
ListenAddr: "127.0.0.1:0", Guests: fg, Backups: &fakeBackups{}, Store: &fakeStore{},
|
|
Storage: fakeStorage{targets: []hub.StorageTarget{localLVM}}, Tokens: staticTokens{"A": 8200},
|
|
HostReader: fakeHostReader{mounts: host.mounts},
|
|
Identity: &storage.IdentityResolver{Host: host},
|
|
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srv.baseCtx = context.Background()
|
|
return srv.Handler()
|
|
}
|
|
|
|
var provenSSD = hub.StorageTarget{Name: "local-lvm", Type: hub.StorageTypeLVMThin, Kind: "system",
|
|
PhysicalDisks: []string{"serial:ata:SanDisk:1"}, Identity: "proven"}
|
|
|
|
func baseHost() *fakeIdentHost {
|
|
return &fakeIdentHost{
|
|
virt: "none",
|
|
parent: map[string]string{"sdc1": "sdc", "sdc2": "sdc"},
|
|
props: map[string]map[string]string{
|
|
"sdb": {"ID_BUS": "ata", "ID_MODEL": "TOSHIBA", "ID_SERIAL_SHORT": "65NOP3HDT"},
|
|
"sdc": {"ID_BUS": "usb", "ID_MODEL": "Portable", "ID_SERIAL_SHORT": "NAAB7C21"},
|
|
},
|
|
mounts: []storage.Mount{
|
|
{Device: "/dev/mapper/pve-root", MountPoint: "/", FSType: "ext4"},
|
|
{Device: "/dev/mapper/pve-root", MountPoint: "/mnt/felhom-drives", FSType: "ext4"},
|
|
{Device: "/dev/sdb", MountPoint: "/mnt/felhom-drives/hdd_1", FSType: "ext4"},
|
|
{Device: "/dev/sdc1", MountPoint: "/mnt/felhom-drives/a", FSType: "ext4"},
|
|
{Device: "/dev/sdc2", MountPoint: "/mnt/felhom-drives/b", FSType: "ext4"},
|
|
{Device: "//nas/films", MountPoint: "/mnt/felhom-drives/nas", FSType: "cifs"},
|
|
},
|
|
}
|
|
}
|
|
|
|
func sameDisk(t *testing.T, h http.Handler, a, b string) (SameDiskResponse, int) {
|
|
t.Helper()
|
|
body, _ := json.Marshal(map[string]string{"a": a, "b": b})
|
|
w := do(t, h, "POST", "/disks/same-disk", "A", string(body))
|
|
var env struct {
|
|
Data SameDiskResponse `json:"data"`
|
|
}
|
|
_ = json.Unmarshal(w.Body.Bytes(), &env)
|
|
return env.Data, w.Code
|
|
}
|
|
|
|
func TestSameDisk_DriveVsSystemData_DifferentProven(t *testing.T) {
|
|
h := sameDiskRig(t, baseHost(), provenSSD)
|
|
got, code := sameDisk(t, h, "/mnt/felhom-drives/hdd_1/felhom-data/paperless", "/mnt/sys_drive")
|
|
if code != http.StatusOK || !got.Different || !got.Proven || got.Reason != "" {
|
|
t.Fatalf("drive vs sys_drive: want different+proven, got %d %+v", code, got)
|
|
}
|
|
if got.B.Kind != "system" || got.A.Kind != "disk" {
|
|
t.Fatalf("kinds: got a=%q b=%q", got.A.Kind, got.B.Kind)
|
|
}
|
|
}
|
|
|
|
func TestSameDisk_TwoPartitionsOfOneDisk_SameProven(t *testing.T) {
|
|
h := sameDiskRig(t, baseHost(), provenSSD)
|
|
got, _ := sameDisk(t, h, "/mnt/felhom-drives/a", "/mnt/felhom-drives/b/x")
|
|
if got.Different || !got.Proven || got.Reason != "same_disk" || got.A.Kind != "partition" {
|
|
t.Fatalf("two partitions: want same+proven, got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestSameDisk_InsideAVM_NotProven(t *testing.T) {
|
|
host := baseHost()
|
|
host.virt = "kvm"
|
|
h := sameDiskRig(t, host, hub.StorageTarget{Name: "local-lvm", Identity: "unknown", IdentityReason: "vm",
|
|
PhysicalDisks: []string{"serial:scsi:QEMU_HARDDISK:drive-scsi0"}})
|
|
got, _ := sameDisk(t, h, "/mnt/felhom-drives/hdd_1", "/mnt/sys_drive")
|
|
if got.Proven || got.Reason != "vm" {
|
|
t.Fatalf("inside a VM: want not proven (vm), got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestSameDisk_NetworkAndUnmapped(t *testing.T) {
|
|
h := sameDiskRig(t, baseHost(), provenSSD)
|
|
if got, _ := sameDisk(t, h, "/mnt/felhom-drives/nas", "/mnt/sys_drive"); got.Proven || got.Reason != "network" {
|
|
t.Fatalf("a NAS: want not proven (network), got %+v", got)
|
|
}
|
|
// An ABSENT drive: its directory sits on the root disk — must be unmapped, never "the system disk".
|
|
if got, _ := sameDisk(t, h, "/mnt/felhom-drives/gone", "/mnt/sys_drive"); got.Proven || got.Reason != "unmapped" || got.A.Mapped {
|
|
t.Fatalf("an absent drive: want unmapped, got %+v", got)
|
|
}
|
|
if got, _ := sameDisk(t, h, "/opt/elsewhere", "/mnt/sys_drive"); got.Proven || got.Reason != "unmapped" {
|
|
t.Fatalf("an unknown guest path: want unmapped, got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestSameDisk_ScopedAndValidated(t *testing.T) {
|
|
h := sameDiskRig(t, baseHost(), provenSSD)
|
|
if w := do(t, h, "POST", "/disks/same-disk", "A", `{"vmid":9300,"a":"/mnt/sys_drive","b":"/mnt/sys_drive"}`); w.Code != http.StatusForbidden {
|
|
t.Fatalf("another guest's vmid: got %d want 403", w.Code)
|
|
}
|
|
if w := do(t, h, "POST", "/disks/same-disk", "A", `{"a":"relative","b":"/mnt/sys_drive"}`); w.Code != http.StatusBadRequest {
|
|
t.Fatalf("a relative path: got %d want 400", w.Code)
|
|
}
|
|
if w := do(t, h, "POST", "/disks/same-disk", "", `{"a":"/x","b":"/y"}`); w.Code != http.StatusUnauthorized {
|
|
t.Fatalf("no token: got %d want 401", w.Code)
|
|
}
|
|
}
|