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:
parent
7ed9203e4b
commit
1a78dcbba2
4 changed files with 67 additions and 28 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue