diff --git a/internal/api/diagnostics.go b/internal/api/diagnostics.go index 1fe6322..4111014 100644 --- a/internal/api/diagnostics.go +++ b/internal/api/diagnostics.go @@ -1260,7 +1260,7 @@ func (r *Router) checkVMDiskMonitoring(ctx context.Context, client *proxmox.Clie Status: vm.Status, Issue: "Failed to get VM status: " + errStr, }) - } else if vmStatus != nil && vmStatus.Agent > 0 { + } else if vmStatus != nil && vmStatus.Agent.Value > 0 { result.VMsWithAgent++ // Try to get filesystem info @@ -1359,7 +1359,7 @@ func (r *Router) checkVMDiskMonitoring(ctx context.Context, client *proxmox.Clie "Verify the node is reachable and API token is valid", ) } - } else if vmStatus == nil || vmStatus.Agent == 0 { + } else if vmStatus == nil || vmStatus.Agent.Value == 0 { result.TestResult = "Guest agent not enabled in VM configuration" result.Recommendations = append(result.Recommendations, "Enable QEMU Guest Agent in VM Options", diff --git a/internal/monitoring/monitor.go b/internal/monitoring/monitor.go index 055b33b..d6c07df 100644 --- a/internal/monitoring/monitor.go +++ b/internal/monitoring/monitor.go @@ -2113,7 +2113,7 @@ func (m *Monitor) fetchGuestAgentMetadata(ctx context.Context, client PVEClientI return nil, nil, "", "", "" } - if vmStatus.Agent <= 0 { + if vmStatus.Agent.Value <= 0 { m.clearGuestMetadataCache(instanceName, nodeName, vmid) return nil, nil, "", "", "" } @@ -6069,7 +6069,7 @@ func (m *Monitor) pollVMsAndContainersEfficient(ctx context.Context, instanceNam guestRaw.StatusFreeMem = detailedStatus.FreeMem guestRaw.Balloon = detailedStatus.Balloon guestRaw.BalloonMin = detailedStatus.BalloonMin - guestRaw.Agent = detailedStatus.Agent + guestRaw.Agent = detailedStatus.Agent.Value memAvailable := uint64(0) if detailedStatus.MemInfo != nil { guestRaw.MemInfoUsed = detailedStatus.MemInfo.Used @@ -6145,12 +6145,12 @@ func (m *Monitor) pollVMsAndContainersEfficient(ctx context.Context, instanceNam // Always try to get filesystem info if agent is enabled // Prefer guest agent data over cluster/resources data for accuracy - if detailedStatus.Agent > 0 { + if detailedStatus.Agent.Value > 0 { log.Debug(). Str("instance", instanceName). Str("vm", res.Name). Int("vmid", res.VMID). - Int("agent", detailedStatus.Agent). + Int("agent", detailedStatus.Agent.Value). Uint64("current_disk", diskUsed). Uint64("current_maxdisk", diskTotal). Msg("Guest agent enabled, querying filesystem info for accurate disk usage") @@ -6425,7 +6425,7 @@ func (m *Monitor) pollVMsAndContainersEfficient(ctx context.Context, instanceNam Str("instance", instanceName). Str("vm", res.Name). Int("vmid", res.VMID). - Int("agent", detailedStatus.Agent). + Int("agent", detailedStatus.Agent.Value). Msg("VM does not have guest agent enabled in config") } } else { diff --git a/internal/monitoring/monitor_polling.go b/internal/monitoring/monitor_polling.go index 8e347c4..b7fc17b 100644 --- a/internal/monitoring/monitor_polling.go +++ b/internal/monitoring/monitor_polling.go @@ -288,7 +288,7 @@ func (m *Monitor) pollVMsWithNodes(ctx context.Context, instanceName string, cli guestRaw.StatusFreeMem = status.FreeMem guestRaw.Balloon = status.Balloon guestRaw.BalloonMin = status.BalloonMin - guestRaw.Agent = status.Agent + guestRaw.Agent = status.Agent.Value memAvailable := uint64(0) if status.MemInfo != nil { guestRaw.MemInfoUsed = status.MemInfo.Used @@ -465,14 +465,14 @@ func (m *Monitor) pollVMsWithNodes(ctx context.Context, instanceName string, cli Str("instance", instanceName). Str("vm", vm.Name). Int("vmid", vm.VMID). - Int("agent", vmStatus.Agent). + Int("agent", vmStatus.Agent.Value). Uint64("diskUsed", diskUsed). Uint64("diskTotal", diskTotal). Msg("VM has 0 disk usage, checking guest agent") } // Check if agent is enabled - if vmStatus.Agent == 0 { + if vmStatus.Agent.Value == 0 { diskStatusReason = "agent-disabled" if logging.IsLevelEnabled(zerolog.DebugLevel) { log.Debug(). @@ -480,7 +480,7 @@ func (m *Monitor) pollVMsWithNodes(ctx context.Context, instanceName string, cli Str("vm", vm.Name). Msg("Guest agent disabled in VM config") } - } else if vmStatus.Agent > 0 || diskUsed == 0 { + } else if vmStatus.Agent.Value > 0 || diskUsed == 0 { if logging.IsLevelEnabled(zerolog.DebugLevel) { log.Debug(). Str("instance", instanceName). diff --git a/pkg/proxmox/client.go b/pkg/proxmox/client.go index 306e9bf..307784c 100644 --- a/pkg/proxmox/client.go +++ b/pkg/proxmox/client.go @@ -1780,25 +1780,64 @@ type VMMemInfo struct { Shared uint64 `json:"shared,omitempty"` } +// VMAgentField handles the polymorphic agent field that changed in Proxmox 8.3+. +// Older versions: integer (0 or 1) +// Proxmox 8.3+: object {"enabled":1,"available":1} or similar +type VMAgentField struct { + Value int +} + +// UnmarshalJSON implements custom JSON unmarshaling to handle both int and object formats +func (a *VMAgentField) UnmarshalJSON(data []byte) error { + // Try parsing as int first (older Proxmox versions) + var intValue int + if err := json.Unmarshal(data, &intValue); err == nil { + a.Value = intValue + return nil + } + + // Try parsing as object (Proxmox 8.3+) + var objValue struct { + Enabled int `json:"enabled"` + Available int `json:"available"` + } + if err := json.Unmarshal(data, &objValue); err == nil { + // Agent is considered enabled if either field is > 0 + // Typically we want to check "available" for actual functionality + if objValue.Available > 0 { + a.Value = objValue.Available + } else if objValue.Enabled > 0 { + a.Value = objValue.Enabled + } else { + a.Value = 0 + } + return nil + } + + // If neither worked, default to 0 (agent disabled) + a.Value = 0 + return nil +} + // VMStatus represents detailed VM status returned by Proxmox. type VMStatus struct { - Status string `json:"status"` - CPU float64 `json:"cpu"` - CPUs int `json:"cpus"` - Mem uint64 `json:"mem"` - MaxMem uint64 `json:"maxmem"` - Balloon uint64 `json:"balloon"` - BalloonMin uint64 `json:"balloon_min"` - FreeMem uint64 `json:"freemem"` - MemInfo *VMMemInfo `json:"meminfo,omitempty"` - Disk uint64 `json:"disk"` - MaxDisk uint64 `json:"maxdisk"` - DiskRead uint64 `json:"diskread"` - DiskWrite uint64 `json:"diskwrite"` - NetIn uint64 `json:"netin"` - NetOut uint64 `json:"netout"` - Uptime uint64 `json:"uptime"` - Agent int `json:"agent"` + Status string `json:"status"` + CPU float64 `json:"cpu"` + CPUs int `json:"cpus"` + Mem uint64 `json:"mem"` + MaxMem uint64 `json:"maxmem"` + Balloon uint64 `json:"balloon"` + BalloonMin uint64 `json:"balloon_min"` + FreeMem uint64 `json:"freemem"` + MemInfo *VMMemInfo `json:"meminfo,omitempty"` + Disk uint64 `json:"disk"` + MaxDisk uint64 `json:"maxdisk"` + DiskRead uint64 `json:"diskread"` + DiskWrite uint64 `json:"diskwrite"` + NetIn uint64 `json:"netin"` + NetOut uint64 `json:"netout"` + Uptime uint64 `json:"uptime"` + Agent VMAgentField `json:"agent"` } // GetZFSPoolStatus gets the status of ZFS pools on a node