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
74 lines
2.9 KiB
Go
74 lines
2.9 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
|
|
)
|
|
|
|
// The report rows carry the `16` §3 identity per storage type: local + lvmthin → the system disk; a dir
|
|
// on its own partition → that disk, kind "partition"; nfs/pbs → network; an UNMOUNTED dir → not computed
|
|
// (never the root disk — the same rule as backing_device).
|
|
func TestObserve_FillsPhysicalDiskIdentity(t *testing.T) {
|
|
f := newFx(t)
|
|
oneDiskLVM(f)
|
|
f.node("sdb", "", "ID_BUS=ata", "ID_MODEL=TOSHIBA_MQ04ABF100", "ID_SERIAL_SHORT=65NOP3HDT")
|
|
f.node("sdb1", "sdb")
|
|
f.mount("/dev/sdb1", "/mnt/usb-backup", "ext4")
|
|
res := f.resolver()
|
|
|
|
api := &fakeStorageAPI{
|
|
node: "demo",
|
|
cluster: []proxmox.Storage{
|
|
{Storage: "local", Type: "dir", Path: "/var/lib/vz"},
|
|
{Storage: "local-lvm", Type: "lvmthin", VGName: "pve", ThinPool: "data"},
|
|
{Storage: "usb-backup", Type: "dir", Path: "/mnt/usb-backup"},
|
|
{Storage: "gone", Type: "dir", Path: "/mnt/gone"},
|
|
{Storage: "nfs-arch", Type: "nfs", Server: "10.0.0.5", Export: "/x"},
|
|
},
|
|
nodeSt: []proxmox.Storage{
|
|
{Storage: "local", Type: "dir", Path: "/var/lib/vz", Active: 1},
|
|
{Storage: "local-lvm", Type: "lvmthin", Active: 1},
|
|
{Storage: "usb-backup", Type: "dir", Path: "/mnt/usb-backup", Active: 1},
|
|
{Storage: "gone", Type: "dir", Path: "/mnt/gone", Active: 1},
|
|
{Storage: "nfs-arch", Type: "nfs", Active: 1},
|
|
},
|
|
}
|
|
host := &fakeHostReader{
|
|
mounts: []Mount{
|
|
{Device: "/dev/mapper/pve-root", MountPoint: "/", FSType: "ext4"},
|
|
{Device: "/dev/sdb1", MountPoint: "/mnt/usb-backup", FSType: "ext4"},
|
|
},
|
|
exists: map[string]bool{"/dev/sdb1": true, "/dev/mapper/pve-root": true},
|
|
}
|
|
ob := NewObserver(api, host, nil, quietLogger())
|
|
ob.SetIdentity(res)
|
|
got, err := ob.Observe(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m := byName(got)
|
|
ssd := []string{"serial:ata:SanDisk_X600:182195804614"}
|
|
for _, n := range []string{"local", "local-lvm"} {
|
|
if tg := m[n]; tg.Kind != "system" || tg.Identity != "proven" || !reflect.DeepEqual(tg.PhysicalDisks, ssd) {
|
|
t.Errorf("%s: want system/proven on the SSD, got kind=%q id=%q disks=%v", n, tg.Kind, tg.Identity, tg.PhysicalDisks)
|
|
}
|
|
}
|
|
if tg := m["usb-backup"]; tg.Kind != "partition" || tg.Identity != "proven" || tg.PhysicalDisks[0] != "serial:ata:TOSHIBA_MQ04ABF100:65NOP3HDT" {
|
|
t.Errorf("usb-backup: got %+v", tg)
|
|
}
|
|
if tg := m["nfs-arch"]; tg.Kind != "network" || tg.IdentityReason != "network" {
|
|
t.Errorf("nfs: got kind=%q reason=%q", tg.Kind, tg.IdentityReason)
|
|
}
|
|
if tg := m["gone"]; tg.Identity != "" || len(tg.PhysicalDisks) != 0 {
|
|
t.Errorf("an unmounted dir must not be computed (never root's disk), got %+v", tg)
|
|
}
|
|
// Without a resolver the fields stay empty (the pre-0.157 shape).
|
|
plain, _ := NewObserver(api, host, nil, quietLogger()).Observe(context.Background())
|
|
if tg := byName(plain)["local"]; tg.Identity != "" || tg.Kind != "" {
|
|
t.Errorf("no resolver: want empty identity, got %+v", tg)
|
|
}
|
|
}
|