Fix guest agent disk data regression on Proxmox 8.3+

Related to #630

Proxmox 8.3+ changed the VM status API to return the `agent` field as an
object ({"enabled":1,"available":1}) instead of an integer (0 or 1). This
caused Pulse to incorrectly treat VMs as having no guest agent, resulting
in missing disk usage data (disk:-1) even when the guest agent was running
and functional.

The issue manifested as:
- VMs showing "Guest details unavailable" or missing disk data
- Pulse logs showing no "Guest agent enabled, querying filesystem info" messages
- `pvesh get /nodes/<node>/qemu/<vmid>/agent/get-fsinfo` working correctly
  from the command line, confirming the agent was functional

Root cause:
The VMStatus struct defined `Agent` as an int field. When Proxmox 8.3+ sent
the new object format, JSON unmarshaling silently left the field at zero,
causing Pulse to skip all guest agent queries.

Changes:
- Created VMAgentField type with custom UnmarshalJSON to handle both formats:
  * Legacy (Proxmox <8.3): integer (0 or 1)
  * Modern (Proxmox 8.3+): object {"enabled":N,"available":N}
- Updated VMStatus.Agent from `int` to `VMAgentField`
- Updated all references to `detailedStatus.Agent` to use `.Agent.Value`
- The unmarshaler prioritizes the "available" field over "enabled" to ensure
  we only query when the agent is actually responding

This fix maintains backward compatibility with older Proxmox versions while
supporting the new format introduced in Proxmox 8.3+.
This commit is contained in:
rcourtman 2025-11-06 18:42:46 +00:00
parent 7ed9203e4b
commit 1a78dcbba2
4 changed files with 67 additions and 28 deletions

View file

@ -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",

View file

@ -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 {

View file

@ -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).

View file

@ -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