feat: surface LXC mountpoints in UI (related to #715)

This commit is contained in:
rcourtman
2025-11-18 22:57:20 +00:00
parent 6e77c4dbea
commit 15d32adb10
2 changed files with 234 additions and 9 deletions

View File

@@ -2873,6 +2873,7 @@ func (m *Monitor) enrichContainerMetadata(ctx context.Context, client PVEClientI
}
rootDeviceHint := ""
var mountMetadata map[string]containerMountMetadata
addressSet := make(map[string]struct{})
addressOrder := make([]string, 0, 4)
@@ -2952,8 +2953,16 @@ func (m *Monitor) enrichContainerMetadata(ctx context.Context, client PVEClientI
Int("vmid", container.VMID).
Msg("Container config metadata unavailable")
} else if len(configData) > 0 {
if hint := extractContainerRootDeviceFromConfig(configData); hint != "" {
rootDeviceHint = hint
mountMetadata = parseContainerMountMetadata(configData)
if rootDeviceHint == "" {
if meta, ok := mountMetadata["rootfs"]; ok && meta.Source != "" {
rootDeviceHint = meta.Source
}
}
if rootDeviceHint == "" {
if hint := extractContainerRootDeviceFromConfig(configData); hint != "" {
rootDeviceHint = hint
}
}
for _, detail := range parseContainerConfigNetworks(configData) {
if len(detail.Addresses) > 0 {
@@ -3043,7 +3052,7 @@ func (m *Monitor) enrichContainerMetadata(ctx context.Context, client PVEClientI
container.NetworkInterfaces = networkIfaces
}
if disks := convertContainerDiskInfo(status); len(disks) > 0 {
if disks := convertContainerDiskInfo(status, mountMetadata); len(disks) > 0 {
container.Disks = disks
}
@@ -3091,7 +3100,7 @@ func ensureContainerRootDiskEntry(container *models.Container) {
}
}
func convertContainerDiskInfo(status *proxmox.Container) []models.Disk {
func convertContainerDiskInfo(status *proxmox.Container, metadata map[string]containerMountMetadata) []models.Disk {
if status == nil || len(status.DiskInfo) == 0 {
return nil
}
@@ -3119,15 +3128,39 @@ func convertContainerDiskInfo(status *proxmox.Container) []models.Disk {
}
label := strings.TrimSpace(name)
lowerLabel := strings.ToLower(label)
mountpoint := ""
device := ""
if metadata != nil {
if meta, ok := metadata[lowerLabel]; ok {
mountpoint = strings.TrimSpace(meta.Mountpoint)
device = strings.TrimSpace(meta.Source)
}
}
if strings.EqualFold(label, "rootfs") || label == "" {
disk.Mountpoint = "/"
if mountpoint == "" {
mountpoint = "/"
}
disk.Type = "rootfs"
if device := sanitizeRootFSDevice(status.RootFS); device != "" {
disk.Device = device
if device == "" {
device = sanitizeRootFSDevice(status.RootFS)
}
} else {
disk.Mountpoint = label
disk.Type = strings.ToLower(label)
if mountpoint == "" {
mountpoint = label
}
if lowerLabel != "" {
disk.Type = lowerLabel
} else {
disk.Type = "disk"
}
}
disk.Mountpoint = mountpoint
if disk.Device == "" && device != "" {
disk.Device = device
}
disks = append(disks, disk)
@@ -3276,6 +3309,12 @@ type containerNetworkDetails struct {
Addresses []string
}
type containerMountMetadata struct {
Key string
Mountpoint string
Source string
}
func parseContainerConfigNetworks(config map[string]interface{}) []containerNetworkDetails {
if len(config) == 0 {
return nil
@@ -3338,6 +3377,59 @@ func parseContainerConfigNetworks(config map[string]interface{}) []containerNetw
return results
}
func parseContainerMountMetadata(config map[string]interface{}) map[string]containerMountMetadata {
if len(config) == 0 {
return nil
}
results := make(map[string]containerMountMetadata)
for rawKey, rawValue := range config {
key := strings.ToLower(strings.TrimSpace(rawKey))
if key != "rootfs" && !strings.HasPrefix(key, "mp") {
continue
}
value := strings.TrimSpace(fmt.Sprint(rawValue))
if value == "" {
continue
}
meta := containerMountMetadata{
Key: key,
}
parts := strings.Split(value, ",")
if len(parts) > 0 {
meta.Source = strings.TrimSpace(parts[0])
}
for _, part := range parts[1:] {
kv := strings.SplitN(strings.TrimSpace(part), "=", 2)
if len(kv) != 2 {
continue
}
k := strings.ToLower(strings.TrimSpace(kv[0]))
v := strings.TrimSpace(kv[1])
switch k {
case "mp", "mountpoint":
meta.Mountpoint = v
}
}
if meta.Mountpoint == "" && key == "rootfs" {
meta.Mountpoint = "/"
}
results[key] = meta
}
if len(results) == 0 {
return nil
}
return results
}
func mergeContainerNetworkInterface(target *[]models.GuestNetworkInterface, detail containerNetworkDetails) {
if target == nil {
return

View File

@@ -0,0 +1,133 @@
package monitoring
import (
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/pkg/proxmox"
)
func TestParseContainerMountMetadata(t *testing.T) {
config := map[string]interface{}{
"rootfs": "local-lvm:vm-100-disk-0,size=8G",
"mp0": "/mnt/pve/media/subvol-100-disk-1,mp=/mnt/media,acl=1",
"MP1": "local:100/vm-100-disk-2,mp=/srv/backup",
"unused": "ignored",
}
meta := parseContainerMountMetadata(config)
if len(meta) != 3 {
t.Fatalf("expected 3 entries, got %d", len(meta))
}
root, ok := meta["rootfs"]
if !ok {
t.Fatalf("missing rootfs metadata")
}
if root.Mountpoint != "/" {
t.Fatalf("expected root mountpoint '/', got %q", root.Mountpoint)
}
if root.Source != "local-lvm:vm-100-disk-0" {
t.Fatalf("unexpected root source %q", root.Source)
}
mp0, ok := meta["mp0"]
if !ok {
t.Fatalf("missing mp0 metadata")
}
if mp0.Mountpoint != "/mnt/media" {
t.Fatalf("expected mp0 mountpoint '/mnt/media', got %q", mp0.Mountpoint)
}
if mp0.Source != "/mnt/pve/media/subvol-100-disk-1" {
t.Fatalf("unexpected mp0 source %q", mp0.Source)
}
mp1, ok := meta["mp1"]
if !ok {
t.Fatalf("missing mp1 metadata")
}
if mp1.Mountpoint != "/srv/backup" {
t.Fatalf("expected mp1 mountpoint '/srv/backup', got %q", mp1.Mountpoint)
}
if mp1.Source != "local:100/vm-100-disk-2" {
t.Fatalf("unexpected mp1 source %q", mp1.Source)
}
}
func TestConvertContainerDiskInfoUsesMountMetadata(t *testing.T) {
status := &proxmox.Container{
RootFS: "local-lvm:vm-200-disk-0,size=20G",
DiskInfo: map[string]proxmox.ContainerDiskUsage{
"rootfs": {Total: 20 * 1024 * 1024 * 1024, Used: 10 * 1024 * 1024 * 1024},
"mp0": {Total: 100 * 1024 * 1024 * 1024, Used: 50 * 1024 * 1024 * 1024},
},
}
meta := map[string]containerMountMetadata{
"rootfs": {Key: "rootfs", Mountpoint: "/", Source: "local-lvm:vm-200-disk-0"},
"mp0": {Key: "mp0", Mountpoint: "/mnt/media", Source: "/mnt/pve/media/subvol-200-disk-1"},
}
disks := convertContainerDiskInfo(status, meta)
if len(disks) != 2 {
t.Fatalf("expected 2 disks, got %d", len(disks))
}
var rootDisk, mpDisk *models.Disk
for i := range disks {
if disks[i].Mountpoint == "/" {
rootDisk = &disks[i]
}
if disks[i].Mountpoint == "/mnt/media" {
mpDisk = &disks[i]
}
}
if rootDisk == nil {
t.Fatalf("root disk not found in %+v", disks)
}
if rootDisk.Device != "local-lvm:vm-200-disk-0" {
t.Fatalf("unexpected root device %q", rootDisk.Device)
}
if rootDisk.Type != "rootfs" {
t.Fatalf("expected root type rootfs, got %q", rootDisk.Type)
}
if mpDisk == nil {
t.Fatalf("mp0 disk not found in %+v", disks)
}
if mpDisk.Device != "/mnt/pve/media/subvol-200-disk-1" {
t.Fatalf("unexpected mp0 device %q", mpDisk.Device)
}
if mpDisk.Type != "mp0" {
t.Fatalf("expected mp0 type 'mp0', got %q", mpDisk.Type)
}
}
func TestConvertContainerDiskInfoFallsBackWithoutMetadata(t *testing.T) {
status := &proxmox.Container{
RootFS: "local-lvm:vm-300-disk-0,size=16G",
DiskInfo: map[string]proxmox.ContainerDiskUsage{
"rootfs": {Total: 16 * 1024 * 1024 * 1024, Used: 8 * 1024 * 1024 * 1024},
"mp1": {Total: 5 * 1024 * 1024 * 1024, Used: 1 * 1024 * 1024 * 1024},
},
}
disks := convertContainerDiskInfo(status, nil)
if len(disks) != 2 {
t.Fatalf("expected 2 disks, got %d", len(disks))
}
var mpDisk *models.Disk
for i := range disks {
if disks[i].Type == "mp1" {
mpDisk = &disks[i]
}
}
if mpDisk == nil {
t.Fatalf("mp1 disk not found in %+v", disks)
}
if mpDisk.Mountpoint != "mp1" {
t.Fatalf("expected fallback mountpoint 'mp1', got %q", mpDisk.Mountpoint)
}
}