From 13e2577c57ea3a2fcc6f5e3abd981280ed5ab0b3 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 22 Oct 2025 14:06:45 +0000 Subject: [PATCH] Handle FreeBSD guest agent disk counters Refs #580 --- pkg/proxmox/client.go | 89 ++++++++++++++++++++++++++++++++++++++ pkg/proxmox/client_test.go | 35 +++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/pkg/proxmox/client.go b/pkg/proxmox/client.go index d704e1d..393e2b2 100644 --- a/pkg/proxmox/client.go +++ b/pkg/proxmox/client.go @@ -1050,6 +1050,95 @@ type VMFileSystem struct { DiskRaw []interface{} `json:"disk"` // Raw disk device info from API } +func (fs *VMFileSystem) UnmarshalJSON(data []byte) error { + type rawVMFileSystem struct { + Name string `json:"name"` + Type string `json:"type"` + Mountpoint string `json:"mountpoint"` + TotalBytes interface{} `json:"total-bytes"` + UsedBytes interface{} `json:"used-bytes"` + DiskRaw []interface{} `json:"disk"` + } + + var raw rawVMFileSystem + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + total, err := parseUint64Flexible(raw.TotalBytes) + if err != nil { + return err + } + used, err := parseUint64Flexible(raw.UsedBytes) + if err != nil { + return err + } + + fs.Name = raw.Name + fs.Type = raw.Type + fs.Mountpoint = raw.Mountpoint + fs.TotalBytes = total + fs.UsedBytes = used + fs.DiskRaw = raw.DiskRaw + fs.Disk = "" + return nil +} + +func parseUint64Flexible(value interface{}) (uint64, error) { + switch v := value.(type) { + case nil: + return 0, nil + case uint64: + return v, nil + case int: + if v < 0 { + return 0, nil + } + return uint64(v), nil + case int64: + if v < 0 { + return 0, nil + } + return uint64(v), nil + case float64: + if v < 0 { + return 0, nil + } + return uint64(v), nil + case json.Number: + return parseUint64Flexible(v.String()) + case string: + s := strings.TrimSpace(v) + if s == "" { + return 0, nil + } + if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") { + u, err := strconv.ParseUint(s[2:], 16, 64) + if err != nil { + return 0, err + } + return u, nil + } + if strings.ContainsAny(s, ".eE") { + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0, err + } + if f < 0 { + return 0, nil + } + return uint64(f), nil + } + u, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return 0, err + } + return u, nil + default: + return 0, fmt.Errorf("unsupported type %T for uint64 conversion", value) + } +} + type VMIpAddress struct { Address string `json:"ip-address"` Prefix int `json:"prefix"` diff --git a/pkg/proxmox/client_test.go b/pkg/proxmox/client_test.go index 01a36ed..c9e12a1 100644 --- a/pkg/proxmox/client_test.go +++ b/pkg/proxmox/client_test.go @@ -73,6 +73,41 @@ func TestDiskUnmarshalWearout(t *testing.T) { } } +func TestVMFileSystemUnmarshalFlexibleNumbers(t *testing.T) { + t.Run("accepts numeric values", func(t *testing.T) { + payload := `{"name":"rootfs","type":"zfs","mountpoint":"/","total-bytes":8589934592,"used-bytes":3221225472,"disk":[{"dev":"/dev/vtbd0p2"}]}` + var fs VMFileSystem + if err := json.Unmarshal([]byte(payload), &fs); err != nil { + t.Fatalf("unexpected error unmarshalling numeric fields: %v", err) + } + if fs.TotalBytes != 8589934592 || fs.UsedBytes != 3221225472 { + t.Fatalf("unexpected numeric values: got total=%d used=%d", fs.TotalBytes, fs.UsedBytes) + } + }) + + t.Run("accepts numeric strings", func(t *testing.T) { + payload := `{"name":"rootfs","type":"ufs","mountpoint":"/","total-bytes":"5368709120","used-bytes":"2147483648","disk":[{"dev":"/dev/vtbd0p3"}]}` + var fs VMFileSystem + if err := json.Unmarshal([]byte(payload), &fs); err != nil { + t.Fatalf("unexpected error unmarshalling string fields: %v", err) + } + if fs.TotalBytes != 5368709120 || fs.UsedBytes != 2147483648 { + t.Fatalf("unexpected string values: got total=%d used=%d", fs.TotalBytes, fs.UsedBytes) + } + }) + + t.Run("accepts float-like strings", func(t *testing.T) { + payload := `{"name":"rootfs","type":"ufs","mountpoint":"/","total-bytes":"1073741824.0","used-bytes":"536870912.0","disk":[{"dev":"/dev/vtbd0p4"}]}` + var fs VMFileSystem + if err := json.Unmarshal([]byte(payload), &fs); err != nil { + t.Fatalf("unexpected error unmarshalling float-like strings: %v", err) + } + if fs.TotalBytes != 1073741824 || fs.UsedBytes != 536870912 { + t.Fatalf("unexpected float string values: got total=%d used=%d", fs.TotalBytes, fs.UsedBytes) + } + }) +} + func TestMemoryStatusEffectiveAvailable(t *testing.T) { t.Run("nil receiver returns zero", func(t *testing.T) { var status *MemoryStatus