From 7316a9e4f6e9393280f0171e9f1dd910f06c1ed5 Mon Sep 17 00:00:00 2001 From: Markus Wieland <44964229+SoWieMarkus@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:03:58 +0200 Subject: [PATCH 1/2] feat: add cortex evalution kpis Signed-off-by: Markus Wieland <44964229+SoWieMarkus@users.noreply.github.com> --- .../kpis/plugins/infrastructure/shared.go | 16 ++++--- .../plugins/infrastructure/shared_test.go | 47 ++++++++++--------- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/internal/knowledge/kpis/plugins/infrastructure/shared.go b/internal/knowledge/kpis/plugins/infrastructure/shared.go index 4647cc28f..656756073 100644 --- a/internal/knowledge/kpis/plugins/infrastructure/shared.go +++ b/internal/knowledge/kpis/plugins/infrastructure/shared.go @@ -7,7 +7,6 @@ import ( "fmt" "regexp" "strconv" - "strings" "github.com/cobaltcore-dev/cortex/internal/knowledge/extractor/plugins/compute" hv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1" @@ -69,6 +68,7 @@ var vmwareHostLabels = []string{ var kvmHostLabels = []string{ "compute_host", + "compute_cluster", "availability_zone", "building_block", "cpu_architecture", @@ -95,11 +95,14 @@ func (h kvmHost) getHostLabels() []string { availabilityZone = "unknown" } - buildingBlock := "unknown" - // Assuming hypervisor names are in the format nodeXXX-bbYY - parts := strings.Split(h.Name, "-") - if len(parts) > 1 { - buildingBlock = parts[1] + buildingBlock := h.Labels["kubernetes.metal.cloud.sap/bb"] + if buildingBlock == "" { + buildingBlock = "unknown" + } + + computeCluster := h.Labels["kubernetes.metal.cloud.sap/cluster"] + if computeCluster == "" { + computeCluster = "unknown" } osVersion := h.Status.OperatingSystem.Version @@ -124,6 +127,7 @@ func (h kvmHost) getHostLabels() []string { return []string{ h.Name, + computeCluster, availabilityZone, buildingBlock, cpuArchitecture, diff --git a/internal/knowledge/kpis/plugins/infrastructure/shared_test.go b/internal/knowledge/kpis/plugins/infrastructure/shared_test.go index 68d5941f0..3fb8f47c2 100644 --- a/internal/knowledge/kpis/plugins/infrastructure/shared_test.go +++ b/internal/knowledge/kpis/plugins/infrastructure/shared_test.go @@ -4,7 +4,6 @@ package infrastructure import ( - "strings" "testing" "github.com/cobaltcore-dev/cortex/internal/knowledge/extractor/plugins/compute" @@ -13,15 +12,11 @@ import ( ) func mockKVMHostLabels(host, az string) map[string]string { - bb := "unknown" - parts := strings.Split(host, "-") - if len(parts) > 1 { - bb = parts[1] - } return map[string]string{ "compute_host": host, + "compute_cluster": "unknown", "availability_zone": az, - "building_block": bb, + "building_block": "unknown", "cpu_architecture": "cascade-lake", "workload_type": "general-purpose", "enabled": "true", @@ -126,7 +121,7 @@ func TestKVMHost_GetHostLabels(t *testing.T) { host: kvmHost{hv1.Hypervisor{ ObjectMeta: metav1.ObjectMeta{Name: "node001-bb01"}, }}, - want: []string{"node001-bb01", "unknown", "bb01", "cascade-lake", "general-purpose", "true", "false", "false", "false"}, + want: []string{"node001-bb01", "unknown", "unknown", "unknown", "cascade-lake", "general-purpose", "true", "false", "false", "false", "unknown"}, }, { name: "availability zone from label", @@ -136,14 +131,20 @@ func TestKVMHost_GetHostLabels(t *testing.T) { Labels: map[string]string{"topology.kubernetes.io/zone": "az1"}, }, }}, - want: []string{"node001-bb01", "az1", "bb01", "cascade-lake", "general-purpose", "true", "false", "false", "false"}, + want: []string{"node001-bb01", "unknown", "az1", "unknown", "cascade-lake", "general-purpose", "true", "false", "false", "false", "unknown"}, }, { - name: "name without dash results in unknown building block", + name: "bb and cluster from labels", host: kvmHost{hv1.Hypervisor{ - ObjectMeta: metav1.ObjectMeta{Name: "nodewithoutdash"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "node001-bb01", + Labels: map[string]string{ + "kubernetes.metal.cloud.sap/bb": "bb01", + "kubernetes.metal.cloud.sap/cluster": "cluster-a", + }, + }, }}, - want: []string{"nodewithoutdash", "unknown", "unknown", "cascade-lake", "general-purpose", "true", "false", "false", "false"}, + want: []string{"node001-bb01", "cluster-a", "unknown", "bb01", "cascade-lake", "general-purpose", "true", "false", "false", "false", "unknown"}, }, { name: "sapphire rapids trait", @@ -151,7 +152,7 @@ func TestKVMHost_GetHostLabels(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "node001-bb01"}, Status: hv1.HypervisorStatus{Traits: []string{"CUSTOM_HW_SAPPHIRE_RAPIDS"}}, }}, - want: []string{"node001-bb01", "unknown", "bb01", "sapphire-rapids", "general-purpose", "true", "false", "false", "false"}, + want: []string{"node001-bb01", "unknown", "unknown", "unknown", "sapphire-rapids", "general-purpose", "true", "false", "false", "false", "unknown"}, }, { name: "hana exclusive host trait", @@ -159,7 +160,7 @@ func TestKVMHost_GetHostLabels(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "node001-bb01"}, Status: hv1.HypervisorStatus{Traits: []string{"CUSTOM_HANA_EXCLUSIVE_HOST"}}, }}, - want: []string{"node001-bb01", "unknown", "bb01", "cascade-lake", "hana", "true", "false", "false", "false"}, + want: []string{"node001-bb01", "unknown", "unknown", "unknown", "cascade-lake", "hana", "true", "false", "false", "false", "unknown"}, }, { name: "decommissioning trait", @@ -167,7 +168,7 @@ func TestKVMHost_GetHostLabels(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "node001-bb01"}, Status: hv1.HypervisorStatus{Traits: []string{"CUSTOM_DECOMMISSIONING"}}, }}, - want: []string{"node001-bb01", "unknown", "bb01", "cascade-lake", "general-purpose", "true", "true", "false", "false"}, + want: []string{"node001-bb01", "unknown", "unknown", "unknown", "cascade-lake", "general-purpose", "true", "true", "false", "false", "unknown"}, }, { name: "external customer exclusive trait", @@ -175,7 +176,7 @@ func TestKVMHost_GetHostLabels(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "node001-bb01"}, Status: hv1.HypervisorStatus{Traits: []string{"CUSTOM_EXTERNAL_CUSTOMER_EXCLUSIVE"}}, }}, - want: []string{"node001-bb01", "unknown", "bb01", "cascade-lake", "general-purpose", "true", "false", "true", "false"}, + want: []string{"node001-bb01", "unknown", "unknown", "unknown", "cascade-lake", "general-purpose", "true", "false", "true", "false", "unknown"}, }, { name: "maintenance set", @@ -183,14 +184,18 @@ func TestKVMHost_GetHostLabels(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "node001-bb01"}, Spec: hv1.HypervisorSpec{Maintenance: hv1.MaintenanceManual}, }}, - want: []string{"node001-bb01", "unknown", "bb01", "cascade-lake", "general-purpose", "true", "false", "false", "true"}, + want: []string{"node001-bb01", "unknown", "unknown", "unknown", "cascade-lake", "general-purpose", "true", "false", "false", "true", "unknown"}, }, { name: "all traits and maintenance set", host: kvmHost{hv1.Hypervisor{ ObjectMeta: metav1.ObjectMeta{ - Name: "node001-bb42", - Labels: map[string]string{"topology.kubernetes.io/zone": "az3"}, + Name: "node001-bb42", + Labels: map[string]string{ + "topology.kubernetes.io/zone": "az3", + "kubernetes.metal.cloud.sap/bb": "bb42", + "kubernetes.metal.cloud.sap/cluster": "cluster-b", + }, }, Spec: hv1.HypervisorSpec{Maintenance: hv1.MaintenanceAuto}, Status: hv1.HypervisorStatus{Traits: []string{ @@ -200,7 +205,7 @@ func TestKVMHost_GetHostLabels(t *testing.T) { "CUSTOM_EXTERNAL_CUSTOMER_EXCLUSIVE", }}, }}, - want: []string{"node001-bb42", "az3", "bb42", "sapphire-rapids", "hana", "true", "true", "true", "true"}, + want: []string{"node001-bb42", "cluster-b", "az3", "bb42", "sapphire-rapids", "hana", "true", "true", "true", "true", "unknown"}, }, { name: "os version set", @@ -208,7 +213,7 @@ func TestKVMHost_GetHostLabels(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "node001-bb01"}, Status: hv1.HypervisorStatus{OperatingSystem: hv1.OperatingSystemStatus{Version: "1.1.1"}}, }}, - want: []string{"node001-bb01", "unknown", "bb01", "cascade-lake", "general-purpose", "true", "false", "false", "false", "1.1.1"}, + want: []string{"node001-bb01", "unknown", "unknown", "unknown", "cascade-lake", "general-purpose", "true", "false", "false", "false", "1.1.1"}, }, } From 1ac09eb4220b412fd8cd3c14019b8851be6bfe69 Mon Sep 17 00:00:00 2001 From: Markus Wieland <44964229+SoWieMarkus@users.noreply.github.com> Date: Thu, 23 Jul 2026 23:27:18 +0200 Subject: [PATCH 2/2] feat: add physical capacity metrics for KVM hosts --- .../infrastructure/kvm_host_capacity.go | 26 ++++++++++++++++--- .../kpis/plugins/infrastructure/shared.go | 11 ++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/internal/knowledge/kpis/plugins/infrastructure/kvm_host_capacity.go b/internal/knowledge/kpis/plugins/infrastructure/kvm_host_capacity.go index 399648403..53bc90fa4 100644 --- a/internal/knowledge/kpis/plugins/infrastructure/kvm_host_capacity.go +++ b/internal/knowledge/kpis/plugins/infrastructure/kvm_host_capacity.go @@ -27,9 +27,10 @@ type hostReservationResources struct { type KVMHostCapacityKPI struct { // Common base for all KPIs that provides standard functionality. - plugins.BaseKPI[struct{}] // No options passed through yaml config - totalCapacityPerHost *prometheus.Desc - capacityPerHost *prometheus.Desc + plugins.BaseKPI[struct{}] // No options passed through yaml config + totalCapacityPerHost *prometheus.Desc + totalPhysicalCapacityPerHost *prometheus.Desc + capacityPerHost *prometheus.Desc } func (KVMHostCapacityKPI) GetName() string { @@ -40,6 +41,12 @@ func (k *KVMHostCapacityKPI) Init(db *db.DB, client client.Client, opts conf.Raw if err := k.BaseKPI.Init(db, client, opts); err != nil { return err } + k.totalPhysicalCapacityPerHost = prometheus.NewDesc( + "cortex_kvm_host_physical_capacity_total", + "Total physical resource capacity on the KVM hosts (individually by host, ignoring overcommit factor). CPU in vCPUs, memory in bytes.", + append(kvmHostLabels, "resource"), + nil, + ) k.totalCapacityPerHost = prometheus.NewDesc( "cortex_kvm_host_capacity_total", "Total resource capacity on the KVM hosts (individually by host). CPU in vCPUs, memory in bytes.", @@ -57,6 +64,7 @@ func (k *KVMHostCapacityKPI) Init(db *db.DB, client client.Client, opts conf.Raw func (k *KVMHostCapacityKPI) Describe(ch chan<- *prometheus.Desc) { ch <- k.totalCapacityPerHost + ch <- k.totalPhysicalCapacityPerHost ch <- k.capacityPerHost } @@ -160,7 +168,6 @@ func (k *KVMHostCapacityKPI) Collect(ch chan<- prometheus.Metric) { for _, hypervisor := range hypervisors { cpuTotal, hasCPUTotal := hypervisor.getResourceCapacity(hv1.ResourceCPU) - ramTotal, hasRAMTotal := hypervisor.getResourceCapacity(hv1.ResourceMemory) if !hasCPUTotal || !hasRAMTotal { @@ -168,6 +175,14 @@ func (k *KVMHostCapacityKPI) Collect(ch chan<- prometheus.Metric) { continue } + cpuPhysical, hasCPUPhysical := hypervisor.getPhysicalCapacity(hv1.ResourceCPU) + ramPhysical, hasRAMPhysical := hypervisor.getPhysicalCapacity(hv1.ResourceMemory) + + if !hasCPUPhysical || !hasRAMPhysical { + slog.Warn("hypervisor missing physical cpu or ram capacity, skipping", "host", hypervisor.Name) + continue + } + cpuUsed := hypervisor.getResourceAllocation(hv1.ResourceCPU) ramUsed := hypervisor.getResourceAllocation(hv1.ResourceMemory) @@ -183,6 +198,9 @@ func (k *KVMHostCapacityKPI) Collect(ch chan<- prometheus.Metric) { labels := hypervisor.getHostLabels() + ch <- prometheus.MustNewConstMetric(k.totalPhysicalCapacityPerHost, prometheus.GaugeValue, cpuPhysical.AsApproximateFloat64(), append(labels, "cpu")...) + ch <- prometheus.MustNewConstMetric(k.totalPhysicalCapacityPerHost, prometheus.GaugeValue, ramPhysical.AsApproximateFloat64(), append(labels, "ram")...) + ch <- prometheus.MustNewConstMetric(k.totalCapacityPerHost, prometheus.GaugeValue, cpuTotal.AsApproximateFloat64(), append(labels, "cpu")...) ch <- prometheus.MustNewConstMetric(k.totalCapacityPerHost, prometheus.GaugeValue, ramTotal.AsApproximateFloat64(), append(labels, "ram")...) diff --git a/internal/knowledge/kpis/plugins/infrastructure/shared.go b/internal/knowledge/kpis/plugins/infrastructure/shared.go index 656756073..d66757c77 100644 --- a/internal/knowledge/kpis/plugins/infrastructure/shared.go +++ b/internal/knowledge/kpis/plugins/infrastructure/shared.go @@ -140,6 +140,17 @@ func (h kvmHost) getHostLabels() []string { } } +func (k kvmHost) getPhysicalCapacity(resourceName hv1.ResourceName) (capacity resource.Quantity, ok bool) { + if k.Status.Capacity == nil { + return resource.Quantity{}, false + } + qty, exists := k.Status.Capacity[resourceName] + if !exists || qty.IsZero() { + return resource.Quantity{}, false + } + return qty, true +} + // getResourceCapacity attempts to retrieve the effective capacity for the specified resource from the hypervisor status, falling back to the physical capacity if effective capacity is not available. It returns the capacity quantity and a boolean indicating whether any capacity information was found. func (k kvmHost) getResourceCapacity(resourceName hv1.ResourceName) (capacity resource.Quantity, ok bool) { if k.Status.EffectiveCapacity != nil {