diff --git a/docs/metrics.md b/docs/metrics.md index 7bec9a3..be4462e 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -141,6 +141,12 @@ Enabled with `metrics.sdn`. Labels are `cluster`, `node`, `sdn`, and `sdn_id`. | --- | --- | | `pve_sdn_state` | `1` when the SDN resource status is `ok`, otherwise `0`. | +PVE 8 reports SDN zones as `sdn` cluster resources, PVE 9 reports them as +`network` resources (zones and fabrics). Both are exported as `pve_sdn_state`; +the `sdn` label holds the zone or fabric name and `sdn_id` the resource ID, +which differs between the two formats (`sdn//` versus +`network///`). + ## LXC containers Enabled with `metrics.lxc`. Templates are skipped. Labels are `cluster`, diff --git a/metrics/pve_node_sdn_collector.go b/metrics/pve_node_sdn_collector.go index 4f5da40..0c12f7b 100644 --- a/metrics/pve_node_sdn_collector.go +++ b/metrics/pve_node_sdn_collector.go @@ -53,7 +53,7 @@ func (c *PveSdnCollector) CollectMetrics() error { labels := prometheus.Labels{ "cluster": cluster.GetClusterName(), "node": node.Name, - "sdn": sdn.SDN, + "sdn": sdn.GetName(), "sdn_id": sdn.ID, } diff --git a/proxmox/model.go b/proxmox/model.go index a81c570..4e1ef19 100644 --- a/proxmox/model.go +++ b/proxmox/model.go @@ -39,7 +39,7 @@ type PveClusterStatus struct { // PveResource represents a generic PVE resource object. type PveResource struct { - Type string `mapstructure:"type"` // Type of resource (e.g., "lxc", "qemu", "node", "storage", "sdn"). + Type string `mapstructure:"type"` // Type of resource (e.g., "lxc", "qemu", "node", "storage", "sdn", "network"). Node string `mapstructure:"node"` // Node where the resource is located. Status string `mapstructure:"status"` // Status of the resource (e.g., "running", "stopped", "online", "available"). ID string `mapstructure:"id"` // Unique identifier for the resource. @@ -106,9 +106,12 @@ type PveStorageResource struct { } // PveSdnResource represents a PVE software-defined network (SDN) resource. +// PVE 8 reports these as "sdn" resources, PVE 9 as "network" resources. type PveSdnResource struct { PveResource - SDN string `mapstructure:"sdn"` // Name of the SDN. + SDN string `mapstructure:"sdn"` // Name of the SDN (only "sdn" resources). + Network string `mapstructure:"network"` // Name of the network entity (only "network" resources). + NetworkType string `mapstructure:"network-type"` // Type of the network entity, e.g. "zone" or "fabric" (only "network" resources). } // PVE resources. @@ -455,6 +458,15 @@ func (r *PveResources) FindNodeSDN(nodeName string) *[]PveSdnResource { return &sdns } +// GetName returns the name of an SDN resource, regardless of whether it was +// reported as an "sdn" (PVE 8) or a "network" (PVE 9) resource. +func (r *PveSdnResource) GetName() string { + if r.SDN != "" { + return r.SDN + } + return r.Network +} + // GetStatusNumeric returns the numeric status of an SDN resource. // Returns 1 if the status is "ok", otherwise returns 0. func (r *PveSdnResource) GetStatusNumeric() float64 { diff --git a/proxmox/model_test.go b/proxmox/model_test.go index bf6628a..7c86e3b 100644 --- a/proxmox/model_test.go +++ b/proxmox/model_test.go @@ -3,6 +3,8 @@ package proxmox import ( "encoding/json" "testing" + + "github.com/mitchellh/mapstructure" ) func TestPveClusterStatusGetClusterModeNumeric(t *testing.T) { @@ -112,3 +114,63 @@ func TestPveZfsPoolStatusUnmarshal(t *testing.T) { t.Fatal("missing cache counters must stay absent instead of becoming zero metrics") } } + +func TestPveSdnResourceGetName(t *testing.T) { + tests := []struct { + name string + resource PveSdnResource + want string + }{ + { + name: "sdn resource", + resource: PveSdnResource{SDN: "localnetwork"}, + want: "localnetwork", + }, + { + name: "network resource", + resource: PveSdnResource{Network: "localnetwork", NetworkType: "zone"}, + want: "localnetwork", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.resource.GetName(); got != tt.want { + t.Fatalf("GetName() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPveSdnResourceDecodeNetwork(t *testing.T) { + // Resource object as reported by PVE 9 for SDN entities. + obj := map[string]interface{}{ + "id": "network/pve1/zone/localnetwork", + "type": "network", + "node": "pve1", + "status": "ok", + "network": "localnetwork", + "network-type": "zone", + } + + var resource PveSdnResource + if err := mapstructure.Decode(obj, &resource); err != nil { + t.Fatalf("Decode() error = %v", err) + } + if err := mapstructure.Decode(obj, &resource.PveResource); err != nil { + t.Fatalf("Decode() error = %v", err) + } + + if got, want := resource.GetName(), "localnetwork"; got != want { + t.Fatalf("GetName() = %v, want %v", got, want) + } + if got, want := resource.NetworkType, "zone"; got != want { + t.Fatalf("NetworkType = %v, want %v", got, want) + } + if got, want := resource.ID, "network/pve1/zone/localnetwork"; got != want { + t.Fatalf("ID = %v, want %v", got, want) + } + if got, want := resource.GetStatusNumeric(), float64(1); got != want { + t.Fatalf("GetStatusNumeric() = %v, want %v", got, want) + } +} diff --git a/proxmox/pve_api_client.go b/proxmox/pve_api_client.go index d835e7f..fd6666c 100644 --- a/proxmox/pve_api_client.go +++ b/proxmox/pve_api_client.go @@ -64,18 +64,18 @@ func (instance *PveApiClient) GetClusterStatus() (*PveClusterStatus, error) { switch obj["type"] { case "cluster": if err := mapstructure.Decode(obj, &cluster); err != nil { - log.Errorf("Unable to decode cluster status object. Error:", err) + log.Errorf("Unable to decode cluster status object. Error: %s", err) continue } case "node": var node PveNodeStatus if err := mapstructure.Decode(obj, &node); err != nil { - log.Errorf("Unable to decode node status object. Error:", err) + log.Errorf("Unable to decode node status object. Error: %s", err) continue } cluster.NodeStatuses = append(cluster.NodeStatuses, node) default: - log.Errorf("Unable to decode cluster status object. Unknown type:", obj["type"]) + log.Warnf("Skipping cluster status object of unknown type '%v'.", obj["type"]) } } @@ -106,60 +106,60 @@ func (instance *PveApiClient) GetClusterResources() (*PveResources, error) { case "lxc": var resource PveLxcResource if err := mapstructure.Decode(obj, &resource); err != nil { - log.Errorf("Unable to decode cluster resource object. Error:", err) + log.Errorf("Unable to decode cluster resource object. Error: %s", err) continue } if err := mapstructure.Decode(obj, &resource.PveResource); err != nil { - log.Errorf("Unable to decode cluster resource object. Error:", err) + log.Errorf("Unable to decode cluster resource object. Error: %s", err) continue } resources.CTs = append(resources.CTs, resource) case "qemu": var resource PveQemuResource if err := mapstructure.Decode(obj, &resource); err != nil { - log.Errorf("Unable to decode cluster resource object. Error:", err) + log.Errorf("Unable to decode cluster resource object. Error: %s", err) continue } if err := mapstructure.Decode(obj, &resource.PveResource); err != nil { - log.Errorf("Unable to decode cluster resource object. Error:", err) + log.Errorf("Unable to decode cluster resource object. Error: %s", err) continue } resources.VMs = append(resources.VMs, resource) case "node": var resource PveNodeResource if err := mapstructure.Decode(obj, &resource); err != nil { - log.Errorf("Unable to decode cluster resource object. Error:", err) + log.Errorf("Unable to decode cluster resource object. Error: %s", err) continue } if err := mapstructure.Decode(obj, &resource.PveResource); err != nil { - log.Errorf("Unable to decode cluster resource object. Error:", err) + log.Errorf("Unable to decode cluster resource object. Error: %s", err) continue } resources.Nodes = append(resources.Nodes, resource) case "storage": var resource PveStorageResource if err := mapstructure.Decode(obj, &resource); err != nil { - log.Errorf("Unable to decode cluster resource object. Error:", err) + log.Errorf("Unable to decode cluster resource object. Error: %s", err) continue } if err := mapstructure.Decode(obj, &resource.PveResource); err != nil { - log.Errorf("Unable to decode cluster resource object. Error:", err) + log.Errorf("Unable to decode cluster resource object. Error: %s", err) continue } resources.Storages = append(resources.Storages, resource) - case "sdn": + case "sdn", "network": var resource PveSdnResource if err := mapstructure.Decode(obj, &resource); err != nil { - log.Errorf("Unable to decode cluster resource object. Error:", err) + log.Errorf("Unable to decode cluster resource object. Error: %s", err) continue } if err := mapstructure.Decode(obj, &resource.PveResource); err != nil { - log.Errorf("Unable to decode cluster resource object. Error:", err) + log.Errorf("Unable to decode cluster resource object. Error: %s", err) continue } resources.SDNs = append(resources.SDNs, resource) default: - log.Errorf("Unable to decode cluster resource object. Unknown type:", obj["type"]) + log.Warnf("Skipping cluster resource object of unknown type '%v'.", obj["type"]) } }