mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
feat: surface LXC mountpoints in UI (related to #715)
This commit is contained in:
@@ -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
|
||||
|
||||
133
internal/monitoring/monitor_container_test.go
Normal file
133
internal/monitoring/monitor_container_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user