diff --git a/helm/bundles/cortex-nova/templates/kpis.yaml b/helm/bundles/cortex-nova/templates/kpis.yaml index 5ce7c41f2..df9ea8d4d 100644 --- a/helm/bundles/cortex-nova/templates/kpis.yaml +++ b/helm/bundles/cortex-nova/templates/kpis.yaml @@ -192,15 +192,17 @@ spec: apiVersion: cortex.cloud/v1alpha1 kind: KPI metadata: - name: vmware-resource-commitments + name: vmware-project-commitments spec: schedulingDomain: nova - impl: vmware_resource_commitments_kpi + impl: vmware_project_commitments_kpi dependencies: datasources: - name: nova-servers - name: nova-flavors - name: limes-project-commitments + - name: identity-domains + - name: identity-projects description: | This KPI tracks the resource commitments of projects running VMs on VMware hosts. --- diff --git a/internal/knowledge/kpis/plugins/infrastructure/vmware_resource_commitments.go b/internal/knowledge/kpis/plugins/infrastructure/vmware_project_commitments.go similarity index 71% rename from internal/knowledge/kpis/plugins/infrastructure/vmware_resource_commitments.go rename to internal/knowledge/kpis/plugins/infrastructure/vmware_project_commitments.go index 0d3d5d3ed..14744b205 100644 --- a/internal/knowledge/kpis/plugins/infrastructure/vmware_resource_commitments.go +++ b/internal/knowledge/kpis/plugins/infrastructure/vmware_project_commitments.go @@ -7,6 +7,7 @@ import ( "log/slog" "strings" + "github.com/cobaltcore-dev/cortex/internal/knowledge/datasources/plugins/openstack/identity" "github.com/cobaltcore-dev/cortex/internal/knowledge/datasources/plugins/openstack/limes" "github.com/cobaltcore-dev/cortex/internal/knowledge/datasources/plugins/openstack/nova" "github.com/cobaltcore-dev/cortex/internal/knowledge/db" @@ -24,7 +25,7 @@ import ( // For general purpose workloads its not possible to differentiate the cpu architecture. To avoid weird behavior in a dashboard we don't export this label for the metric. // For HANA flavors the cpu architecture is part of the flavor name (_v2 suffix for sapphire rapids, without suffix for cascade lake). // For both types of workload however we can not determine on which host the commitment is fulfilled. -type VMwareResourceCommitmentsKPI struct { +type VMwareProjectCommitmentsKPI struct { // BaseKPI provides common fields and methods for all KPIs, such as database connection and Kubernetes client. plugins.BaseKPI[struct{}] @@ -32,11 +33,11 @@ type VMwareResourceCommitmentsKPI struct { unusedHanaCommittedResourcesPerProject *prometheus.Desc } -func (k *VMwareResourceCommitmentsKPI) GetName() string { - return "vmware_resource_commitments_kpi" +func (k *VMwareProjectCommitmentsKPI) GetName() string { + return "vmware_project_commitments_kpi" } -func (k *VMwareResourceCommitmentsKPI) Init(dbConn *db.DB, c client.Client, opts conf.RawOpts) error { +func (k *VMwareProjectCommitmentsKPI) Init(dbConn *db.DB, c client.Client, opts conf.RawOpts) error { if err := k.BaseKPI.Init(dbConn, c, opts); err != nil { return err } @@ -44,38 +45,44 @@ func (k *VMwareResourceCommitmentsKPI) Init(dbConn *db.DB, c client.Client, opts k.unusedGeneralPurposeCommitmentsPerProject = prometheus.NewDesc( "cortex_vmware_commitments_general_purpose", "Committed general purpose resources that are currently unused. CPU (resource=cpu) in vCPUs, memory (resource=ram) in bytes.", - []string{"availability_zone", "resource", "project_id"}, nil, + []string{"availability_zone", "resource", "project_id", "project_name", "domain_id", "domain_name"}, nil, ) k.unusedHanaCommittedResourcesPerProject = prometheus.NewDesc( "cortex_vmware_commitments_hana_resources", "Total committed HANA instances capacity that is currently unused, translated to resources. CPU in vCPUs, memory and disk in bytes.", - []string{"availability_zone", "cpu_architecture", "resource", "project_id"}, nil, + []string{"availability_zone", "cpu_architecture", "resource", "project_id", "project_name", "domain_id", "domain_name"}, nil, ) return nil } -func (k *VMwareResourceCommitmentsKPI) Describe(ch chan<- *prometheus.Desc) { +func (k *VMwareProjectCommitmentsKPI) Describe(ch chan<- *prometheus.Desc) { ch <- k.unusedGeneralPurposeCommitmentsPerProject ch <- k.unusedHanaCommittedResourcesPerProject } -func (k *VMwareResourceCommitmentsKPI) Collect(ch chan<- prometheus.Metric) { +func (k *VMwareProjectCommitmentsKPI) Collect(ch chan<- prometheus.Metric) { if k.DB == nil { return } flavorsByName, err := k.getFlavorsByName() if err != nil { - slog.Error("vmware_resource_commitments: failed to load flavors", "err", err) + slog.Error("vmware_project_commitments: failed to load flavors", "err", err) return } - k.collectGeneralPurpose(ch, flavorsByName) - k.collectHana(ch, flavorsByName) + projects, err := k.getProjectsWithDomains() + if err != nil { + slog.Error("vmware_project_commitments: failed to load projects with domains", "err", err) + return + } + + k.collectGeneralPurpose(ch, flavorsByName, projects) + k.collectHana(ch, flavorsByName, projects) } // getFlavorsByName loads all flavors and returns them keyed by name. -func (k *VMwareResourceCommitmentsKPI) getFlavorsByName() (map[string]nova.Flavor, error) { +func (k *VMwareProjectCommitmentsKPI) getFlavorsByName() (map[string]nova.Flavor, error) { var flavors []nova.Flavor if _, err := k.DB.Select(&flavors, "SELECT * FROM "+nova.Flavor{}.TableName()); err != nil { return nil, err @@ -88,7 +95,7 @@ func (k *VMwareResourceCommitmentsKPI) getFlavorsByName() (map[string]nova.Flavo } // getGeneralPurposeCommitments loads confirmed/guaranteed cores and ram commitments. -func (k *VMwareResourceCommitmentsKPI) getGeneralPurposeCommitments() ([]limes.Commitment, error) { +func (k *VMwareProjectCommitmentsKPI) getGeneralPurposeCommitments() ([]limes.Commitment, error) { var commitments []limes.Commitment if _, err := k.DB.Select(&commitments, ` SELECT * FROM `+limes.Commitment{}.TableName()+` @@ -103,7 +110,7 @@ func (k *VMwareResourceCommitmentsKPI) getGeneralPurposeCommitments() ([]limes.C // getGeneralPurposeServers loads running non-HANA servers for general purpose usage accounting. // KVM-specific flavors are filtered out in Go since SQL LIKE cannot express the segment-exact pattern. -func (k *VMwareResourceCommitmentsKPI) getGeneralPurposeServers() ([]nova.Server, error) { +func (k *VMwareProjectCommitmentsKPI) getGeneralPurposeServers() ([]nova.Server, error) { var servers []nova.Server if _, err := k.DB.Select(&servers, ` SELECT * FROM `+nova.Server{}.TableName()+` @@ -122,7 +129,7 @@ func (k *VMwareResourceCommitmentsKPI) getGeneralPurposeServers() ([]nova.Server } // getHanaInstanceCommitments loads confirmed/guaranteed HANA instance commitments. -func (k *VMwareResourceCommitmentsKPI) getHanaInstanceCommitments() ([]limes.Commitment, error) { +func (k *VMwareProjectCommitmentsKPI) getHanaInstanceCommitments() ([]limes.Commitment, error) { var commitments []limes.Commitment if _, err := k.DB.Select(&commitments, ` SELECT * FROM `+limes.Commitment{}.TableName()+` @@ -136,7 +143,7 @@ func (k *VMwareResourceCommitmentsKPI) getHanaInstanceCommitments() ([]limes.Com } // getRunningHanaServers loads all running HANA VMware servers (KVM HANA flavors excluded in Go). -func (k *VMwareResourceCommitmentsKPI) getRunningHanaServers() ([]nova.Server, error) { +func (k *VMwareProjectCommitmentsKPI) getRunningHanaServers() ([]nova.Server, error) { var servers []nova.Server if _, err := k.DB.Select(&servers, ` SELECT * FROM `+nova.Server{}.TableName()+` @@ -156,15 +163,15 @@ func (k *VMwareResourceCommitmentsKPI) getRunningHanaServers() ([]nova.Server, e // collectGeneralPurpose computes and emits unused general purpose committed resources per project. // Unused = committed - in-use (clamped to zero; zero values are not emitted). -func (k *VMwareResourceCommitmentsKPI) collectGeneralPurpose(ch chan<- prometheus.Metric, flavorsByName map[string]nova.Flavor) { +func (k *VMwareProjectCommitmentsKPI) collectGeneralPurpose(ch chan<- prometheus.Metric, flavorsByName map[string]nova.Flavor, projects map[string]projectWithDomain) { commitments, err := k.getGeneralPurposeCommitments() if err != nil { - slog.Error("vmware_resource_commitments: failed to load gp commitments", "err", err) + slog.Error("vmware_project_commitments: failed to load gp commitments", "err", err) return } servers, err := k.getGeneralPurposeServers() if err != nil { - slog.Error("vmware_resource_commitments: failed to load gp servers", "err", err) + slog.Error("vmware_project_commitments: failed to load gp servers", "err", err) return } @@ -178,7 +185,7 @@ func (k *VMwareResourceCommitmentsKPI) collectGeneralPurpose(ch chan<- prometheu case "ram": bytes, err := bytesFromUnit(float64(c.Amount), c.Unit) if err != nil { - slog.Warn("vmware_resource_commitments: unknown ram unit", "unit", c.Unit, "err", err) + slog.Warn("vmware_project_commitments: unknown ram unit", "unit", c.Unit, "err", err) continue } committed[gpKey{c.ProjectID, c.AvailabilityZone, "ram"}] += bytes @@ -189,7 +196,7 @@ func (k *VMwareResourceCommitmentsKPI) collectGeneralPurpose(ch chan<- prometheu for _, s := range servers { flavor, ok := flavorsByName[s.FlavorName] if !ok { - slog.Warn("vmware_resource_commitments: gp flavor not found", "flavor", s.FlavorName) + slog.Warn("vmware_project_commitments: gp flavor not found", "flavor", s.FlavorName) continue } used[gpKey{s.TenantID, s.OSEXTAvailabilityZone, "cpu"}] += float64(flavor.VCPUs) @@ -201,11 +208,12 @@ func (k *VMwareResourceCommitmentsKPI) collectGeneralPurpose(ch chan<- prometheu if unused <= 0 { continue } + project := projects[key.projectID] ch <- prometheus.MustNewConstMetric( k.unusedGeneralPurposeCommitmentsPerProject, prometheus.GaugeValue, unused, - key.az, key.resource, key.projectID, + key.az, key.resource, key.projectID, project.ProjectName, project.DomainID, project.DomainName, ) } } @@ -213,7 +221,7 @@ func (k *VMwareResourceCommitmentsKPI) collectGeneralPurpose(ch chan<- prometheu // collectHana computes and emits unused committed HANA instance resources per project. // Each HANA instance commitment is compared against running servers; the remainder is // translated to cpu/ram/disk capacity using the flavor spec. -func (k *VMwareResourceCommitmentsKPI) collectHana(ch chan<- prometheus.Metric, flavorsByName map[string]nova.Flavor) { +func (k *VMwareProjectCommitmentsKPI) collectHana(ch chan<- prometheus.Metric, flavorsByName map[string]nova.Flavor, projects map[string]projectWithDomain) { commitments, err := k.getHanaInstanceCommitments() if err != nil { slog.Error("vmware_resource_commitments: failed to load hana commitments", "err", err) @@ -261,11 +269,36 @@ func (k *VMwareResourceCommitmentsKPI) collectHana(ch chan<- prometheus.Metric, } for key, value := range totals { + project := projects[key.projectID] ch <- prometheus.MustNewConstMetric( k.unusedHanaCommittedResourcesPerProject, prometheus.GaugeValue, value, - key.az, key.cpuArch, key.resource, key.projectID, + key.az, key.cpuArch, key.resource, key.projectID, project.ProjectName, project.DomainID, project.DomainName, ) } } + +type projectWithDomain struct { + ProjectID string `db:"project_id"` + ProjectName string `db:"project_name"` + DomainID string `db:"domain_id"` + DomainName string `db:"domain_name"` +} + +func (k *VMwareProjectCommitmentsKPI) getProjectsWithDomains() (map[string]projectWithDomain, error) { + var projects []projectWithDomain + if _, err := k.DB.Select(&projects, ` + SELECT p.id AS project_id, p.name AS project_name, COALESCE(d.id, '') AS domain_id, COALESCE(d.name, '') AS domain_name + FROM `+identity.Project{}.TableName()+` p + LEFT JOIN `+identity.Domain{}.TableName()+` d ON p.domain_id = d.id + `); err != nil { + return nil, err + } + + projectMap := make(map[string]projectWithDomain, len(projects)) + for _, p := range projects { + projectMap[p.ProjectID] = p + } + return projectMap, nil +} diff --git a/internal/knowledge/kpis/plugins/infrastructure/vmware_resource_commitments_test.go b/internal/knowledge/kpis/plugins/infrastructure/vmware_project_commitments_test.go similarity index 68% rename from internal/knowledge/kpis/plugins/infrastructure/vmware_resource_commitments_test.go rename to internal/knowledge/kpis/plugins/infrastructure/vmware_project_commitments_test.go index 6616dc558..8978f76a9 100644 --- a/internal/knowledge/kpis/plugins/infrastructure/vmware_resource_commitments_test.go +++ b/internal/knowledge/kpis/plugins/infrastructure/vmware_project_commitments_test.go @@ -6,6 +6,7 @@ package infrastructure import ( "testing" + "github.com/cobaltcore-dev/cortex/internal/knowledge/datasources/plugins/openstack/identity" "github.com/cobaltcore-dev/cortex/internal/knowledge/datasources/plugins/openstack/limes" "github.com/cobaltcore-dev/cortex/internal/knowledge/datasources/plugins/openstack/nova" "github.com/cobaltcore-dev/cortex/internal/knowledge/db" @@ -15,7 +16,7 @@ import ( prometheusgo "github.com/prometheus/client_model/go" ) -func setupResourceCommitmentsDB(t *testing.T) (testDB *db.DB, cleanup func()) { +func setupProjectCommitmentsDB(t *testing.T) (testDB *db.DB, cleanup func()) { t.Helper() dbEnv := testlibDB.SetupDBEnv(t) testDB = &db.DB{DbMap: dbEnv.DbMap} @@ -23,18 +24,20 @@ func setupResourceCommitmentsDB(t *testing.T) (testDB *db.DB, cleanup func()) { testDB.AddTable(limes.Commitment{}), testDB.AddTable(nova.Server{}), testDB.AddTable(nova.Flavor{}), + testDB.AddTable(identity.Project{}), + testDB.AddTable(identity.Domain{}), ); err != nil { t.Fatalf("failed to create tables: %v", err) } return testDB, dbEnv.Close } -// collectResourceCommitmentsMetrics runs the KPI and returns all emitted metrics keyed by -// "metricName|az|cpu_architecture|resource|project_id". GP metrics have an empty cpu_architecture -// segment since the descriptor does not include that label. -func collectResourceCommitmentsMetrics(t *testing.T, testDB *db.DB) map[string]float64 { +// collectProjectCommitmentsMetrics runs the KPI and returns all emitted metrics keyed by +// "metricName|az|cpu_architecture|resource|project_id|project_name|domain_id|domain_name". +// GP metrics have an empty cpu_architecture segment since the descriptor does not include that label. +func collectProjectCommitmentsMetrics(t *testing.T, testDB *db.DB) map[string]float64 { t.Helper() - kpi := &VMwareResourceCommitmentsKPI{} + kpi := &VMwareProjectCommitmentsKPI{} if err := kpi.Init(testDB, nil, conf.NewRawOpts("{}")); err != nil { t.Fatalf("failed to init KPI: %v", err) } @@ -53,7 +56,7 @@ func collectResourceCommitmentsMetrics(t *testing.T, testDB *db.DB) map[string]f lbls[lp.GetName()] = lp.GetValue() } name := getMetricName(m.Desc().String()) - key := name + "|" + lbls["availability_zone"] + "|" + lbls["cpu_architecture"] + "|" + lbls["resource"] + "|" + lbls["project_id"] + key := name + "|" + lbls["availability_zone"] + "|" + lbls["cpu_architecture"] + "|" + lbls["resource"] + "|" + lbls["project_id"] + "|" + lbls["project_name"] + "|" + lbls["domain_id"] + "|" + lbls["domain_name"] result[key] = pm.GetGauge().GetValue() } return result @@ -61,30 +64,40 @@ func collectResourceCommitmentsMetrics(t *testing.T, testDB *db.DB) map[string]f // gpKey builds the expected map key for a general-purpose metric. // cpu_architecture is always empty because the GP metric descriptor omits that label. -func gpKey(az, resource, projectID string) string { - return "cortex_vmware_commitments_general_purpose|" + az + "||" + resource + "|" + projectID +func gpKey(az, resource string, p projectWithDomain) string { + return "cortex_vmware_commitments_general_purpose|" + az + "||" + resource + "|" + p.ProjectID + "|" + p.ProjectName + "|" + p.DomainID + "|" + p.DomainName } // hKey builds the expected map key for a HANA metric. -func hKey(az, cpuArch, resource, projectID string) string { - return "cortex_vmware_commitments_hana_resources|" + az + "|" + cpuArch + "|" + resource + "|" + projectID +func hKey(az, cpuArch, resource string, p projectWithDomain) string { + return "cortex_vmware_commitments_hana_resources|" + az + "|" + cpuArch + "|" + resource + "|" + p.ProjectID + "|" + p.ProjectName + "|" + p.DomainID + "|" + p.DomainName } -func TestVMwareResourceCommitmentsKPI_Init(t *testing.T) { +func TestVMwareProjectCommitmentsKPI_Init(t *testing.T) { dbEnv := testlibDB.SetupDBEnv(t) testDB := db.DB{DbMap: dbEnv.DbMap} defer dbEnv.Close() - kpi := &VMwareResourceCommitmentsKPI{} + kpi := &VMwareProjectCommitmentsKPI{} if err := kpi.Init(&testDB, nil, conf.NewRawOpts("{}")); err != nil { t.Fatalf("expected no error, got %v", err) } } -func TestVMwareResourceCommitmentsKPI_Collect_GeneralPurpose(t *testing.T) { + +func TestVMwareProjectCommitmentsKPI_Collect_GeneralPurpose(t *testing.T) { + // Reusable project/domain entries for test cases that need them. + p1 := identity.Project{ID: "p1", Name: "project-one", DomainID: "d1", Enabled: true} + p2 := identity.Project{ID: "p2", Name: "project-two", DomainID: "d1", Enabled: true} + d1 := identity.Domain{ID: "d1", Name: "domain-one", Enabled: true} + pd1 := projectWithDomain{ProjectID: "p1", ProjectName: "project-one", DomainID: "d1", DomainName: "domain-one"} + pd2 := projectWithDomain{ProjectID: "p2", ProjectName: "project-two", DomainID: "d1", DomainName: "domain-one"} + tests := []struct { name string commitments []limes.Commitment servers []nova.Server flavors []nova.Flavor + projects []identity.Project + domains []identity.Domain want map[string]float64 }{ { @@ -96,8 +109,10 @@ func TestVMwareResourceCommitmentsKPI_Collect_GeneralPurpose(t *testing.T) { commitments: []limes.Commitment{ {ID: 1, UUID: "c1", ServiceType: "compute", ResourceName: "cores", AvailabilityZone: "az1", Amount: 10, Status: "confirmed", ProjectID: "p1"}, }, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, want: map[string]float64{ - gpKey("az1", "cpu", "p1"): 10, + gpKey("az1", "cpu", pd1): 10, }, }, { @@ -105,8 +120,10 @@ func TestVMwareResourceCommitmentsKPI_Collect_GeneralPurpose(t *testing.T) { commitments: []limes.Commitment{ {ID: 1, UUID: "c1", ServiceType: "compute", ResourceName: "ram", AvailabilityZone: "az1", Amount: 1024, Unit: "MiB", Status: "confirmed", ProjectID: "p1"}, }, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, want: map[string]float64{ - gpKey("az1", "ram", "p1"): 1024 * 1024 * 1024, + gpKey("az1", "ram", pd1): 1024 * 1024 * 1024, }, }, { @@ -114,8 +131,10 @@ func TestVMwareResourceCommitmentsKPI_Collect_GeneralPurpose(t *testing.T) { commitments: []limes.Commitment{ {ID: 1, UUID: "c1", ServiceType: "compute", ResourceName: "ram", AvailabilityZone: "az1", Amount: 2, Unit: "GiB", Status: "confirmed", ProjectID: "p1"}, }, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, want: map[string]float64{ - gpKey("az1", "ram", "p1"): 2 * 1024 * 1024 * 1024, + gpKey("az1", "ram", pd1): 2 * 1024 * 1024 * 1024, }, }, { @@ -130,8 +149,10 @@ func TestVMwareResourceCommitmentsKPI_Collect_GeneralPurpose(t *testing.T) { flavors: []nova.Flavor{ {ID: "f1", Name: "small", VCPUs: 3, RAM: 0, Disk: 0}, }, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, want: map[string]float64{ - gpKey("az1", "cpu", "p1"): 4, // 10 - 2×3 = 4 + gpKey("az1", "cpu", pd1): 4, // 10 - 2×3 = 4 }, }, { @@ -145,7 +166,9 @@ func TestVMwareResourceCommitmentsKPI_Collect_GeneralPurpose(t *testing.T) { flavors: []nova.Flavor{ {ID: "f1", Name: "small", VCPUs: 4, RAM: 0, Disk: 0}, }, - want: map[string]float64{}, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, + want: map[string]float64{}, }, { name: "over-used cpu produces no metric", @@ -158,7 +181,9 @@ func TestVMwareResourceCommitmentsKPI_Collect_GeneralPurpose(t *testing.T) { flavors: []nova.Flavor{ {ID: "f1", Name: "large", VCPUs: 8, RAM: 0, Disk: 0}, }, - want: map[string]float64{}, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, + want: map[string]float64{}, }, { name: "hana servers not counted against gp commitments", @@ -171,8 +196,10 @@ func TestVMwareResourceCommitmentsKPI_Collect_GeneralPurpose(t *testing.T) { flavors: []nova.Flavor{ {ID: "f1", Name: "hana_small", VCPUs: 8, RAM: 0, Disk: 0}, }, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, want: map[string]float64{ - gpKey("az1", "cpu", "p1"): 10, + gpKey("az1", "cpu", pd1): 10, }, }, { @@ -186,8 +213,10 @@ func TestVMwareResourceCommitmentsKPI_Collect_GeneralPurpose(t *testing.T) { flavors: []nova.Flavor{ {ID: "f1", Name: "m1_k_small", VCPUs: 4, RAM: 0, Disk: 0}, }, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, want: map[string]float64{ - gpKey("az1", "cpu", "p1"): 10, + gpKey("az1", "cpu", pd1): 10, }, }, { @@ -203,8 +232,10 @@ func TestVMwareResourceCommitmentsKPI_Collect_GeneralPurpose(t *testing.T) { flavors: []nova.Flavor{ {ID: "f1", Name: "small", VCPUs: 2, RAM: 0, Disk: 0}, }, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, want: map[string]float64{ - gpKey("az1", "cpu", "p1"): 8, // only 1 ACTIVE × 2 subtracted + gpKey("az1", "cpu", pd1): 8, // only 1 ACTIVE × 2 subtracted }, }, { @@ -212,8 +243,10 @@ func TestVMwareResourceCommitmentsKPI_Collect_GeneralPurpose(t *testing.T) { commitments: []limes.Commitment{ {ID: 1, UUID: "c1", ServiceType: "compute", ResourceName: "cores", AvailabilityZone: "az1", Amount: 5, Status: "guaranteed", ProjectID: "p1"}, }, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, want: map[string]float64{ - gpKey("az1", "cpu", "p1"): 5, + gpKey("az1", "cpu", pd1): 5, }, }, { @@ -221,14 +254,18 @@ func TestVMwareResourceCommitmentsKPI_Collect_GeneralPurpose(t *testing.T) { commitments: []limes.Commitment{ {ID: 1, UUID: "c1", ServiceType: "compute", ResourceName: "cores", AvailabilityZone: "az1", Amount: 100, Status: "pending", ProjectID: "p1"}, }, - want: map[string]float64{}, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, + want: map[string]float64{}, }, { name: "non-compute service type excluded", commitments: []limes.Commitment{ {ID: 1, UUID: "c1", ServiceType: "network", ResourceName: "cores", AvailabilityZone: "az1", Amount: 100, Status: "confirmed", ProjectID: "p1"}, }, - want: map[string]float64{}, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, + want: map[string]float64{}, }, { name: "multiple commitments per project and AZ summed", @@ -238,10 +275,12 @@ func TestVMwareResourceCommitmentsKPI_Collect_GeneralPurpose(t *testing.T) { {ID: 3, UUID: "c3", ServiceType: "compute", ResourceName: "cores", AvailabilityZone: "az2", Amount: 20, Status: "confirmed", ProjectID: "p1"}, {ID: 4, UUID: "c4", ServiceType: "compute", ResourceName: "cores", AvailabilityZone: "az1", Amount: 8, Status: "confirmed", ProjectID: "p2"}, }, + projects: []identity.Project{p1, p2}, + domains: []identity.Domain{d1}, want: map[string]float64{ - gpKey("az1", "cpu", "p1"): 15, - gpKey("az2", "cpu", "p1"): 20, - gpKey("az1", "cpu", "p2"): 8, + gpKey("az1", "cpu", pd1): 15, + gpKey("az2", "cpu", pd1): 20, + gpKey("az1", "cpu", pd2): 8, }, }, { @@ -256,16 +295,18 @@ func TestVMwareResourceCommitmentsKPI_Collect_GeneralPurpose(t *testing.T) { flavors: []nova.Flavor{ {ID: "f1", Name: "medium", VCPUs: 2, RAM: 256, Disk: 0}, }, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, want: map[string]float64{ - gpKey("az1", "cpu", "p1"): 6, // 8 - 1×2 - gpKey("az1", "ram", "p1"): (512 - 256) * 1024 * 1024, // 512MiB - 256MB (flavor.RAM is in MB) + gpKey("az1", "cpu", pd1): 6, // 8 - 1×2 + gpKey("az1", "ram", pd1): (512 - 256) * 1024 * 1024, // 512MiB - 256MB (flavor.RAM is in MB) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - testDB, cleanup := setupResourceCommitmentsDB(t) + testDB, cleanup := setupProjectCommitmentsDB(t) defer cleanup() var rows []any @@ -278,13 +319,19 @@ func TestVMwareResourceCommitmentsKPI_Collect_GeneralPurpose(t *testing.T) { for i := range tt.flavors { rows = append(rows, &tt.flavors[i]) } + for i := range tt.projects { + rows = append(rows, &tt.projects[i]) + } + for i := range tt.domains { + rows = append(rows, &tt.domains[i]) + } if len(rows) > 0 { if err := testDB.Insert(rows...); err != nil { t.Fatalf("failed to insert test data: %v", err) } } - got := collectResourceCommitmentsMetrics(t, testDB) + got := collectProjectCommitmentsMetrics(t, testDB) if len(got) != len(tt.want) { t.Errorf("expected %d metrics, got %d: %v", len(tt.want), len(got), got) @@ -303,12 +350,21 @@ func TestVMwareResourceCommitmentsKPI_Collect_GeneralPurpose(t *testing.T) { } } -func TestVMwareResourceCommitmentsKPI_Collect_HANA(t *testing.T) { +func TestVMwareProjectCommitmentsKPI_Collect_HANA(t *testing.T) { + // Reusable project/domain entries for test cases that need them. + p1 := identity.Project{ID: "p1", Name: "project-one", DomainID: "d1", Enabled: true} + p2 := identity.Project{ID: "p2", Name: "project-two", DomainID: "d1", Enabled: true} + d1 := identity.Domain{ID: "d1", Name: "domain-one", Enabled: true} + pd1 := projectWithDomain{ProjectID: "p1", ProjectName: "project-one", DomainID: "d1", DomainName: "domain-one"} + pd2 := projectWithDomain{ProjectID: "p2", ProjectName: "project-two", DomainID: "d1", DomainName: "domain-one"} + tests := []struct { name string commitments []limes.Commitment servers []nova.Server flavors []nova.Flavor + projects []identity.Project + domains []identity.Domain want map[string]float64 }{ { @@ -323,10 +379,12 @@ func TestVMwareResourceCommitmentsKPI_Collect_HANA(t *testing.T) { flavors: []nova.Flavor{ {ID: "f1", Name: "hana_c128_m1600", VCPUs: 128, RAM: 1638400, Disk: 100}, }, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, want: map[string]float64{ - hKey("az1", "cascade-lake", "cpu", "p1"): 2 * 128, - hKey("az1", "cascade-lake", "ram", "p1"): 2 * 1638400 * 1024 * 1024, - hKey("az1", "cascade-lake", "disk", "p1"): 2 * 100 * 1024 * 1024 * 1024, + hKey("az1", "cascade-lake", "cpu", pd1): 2 * 128, + hKey("az1", "cascade-lake", "ram", pd1): 2 * 1638400 * 1024 * 1024, + hKey("az1", "cascade-lake", "disk", pd1): 2 * 100 * 1024 * 1024 * 1024, }, }, { @@ -340,10 +398,12 @@ func TestVMwareResourceCommitmentsKPI_Collect_HANA(t *testing.T) { flavors: []nova.Flavor{ {ID: "f1", Name: "hana_c128_m1600", VCPUs: 128, RAM: 1638400, Disk: 100}, }, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, want: map[string]float64{ - hKey("az1", "cascade-lake", "cpu", "p1"): 2 * 128, - hKey("az1", "cascade-lake", "ram", "p1"): 2 * 1638400 * 1024 * 1024, - hKey("az1", "cascade-lake", "disk", "p1"): 2 * 100 * 1024 * 1024 * 1024, + hKey("az1", "cascade-lake", "cpu", pd1): 2 * 128, + hKey("az1", "cascade-lake", "ram", pd1): 2 * 1638400 * 1024 * 1024, + hKey("az1", "cascade-lake", "disk", pd1): 2 * 100 * 1024 * 1024 * 1024, }, }, { @@ -358,7 +418,9 @@ func TestVMwareResourceCommitmentsKPI_Collect_HANA(t *testing.T) { flavors: []nova.Flavor{ {ID: "f1", Name: "hana_small", VCPUs: 64, RAM: 819200, Disk: 50}, }, - want: map[string]float64{}, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, + want: map[string]float64{}, }, { name: "over-used hana produces no metric", @@ -372,7 +434,9 @@ func TestVMwareResourceCommitmentsKPI_Collect_HANA(t *testing.T) { flavors: []nova.Flavor{ {ID: "f1", Name: "hana_small", VCPUs: 64, RAM: 819200, Disk: 50}, }, - want: map[string]float64{}, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, + want: map[string]float64{}, }, { name: "sapphire-rapids arch from _v2 suffix", @@ -382,10 +446,12 @@ func TestVMwareResourceCommitmentsKPI_Collect_HANA(t *testing.T) { flavors: []nova.Flavor{ {ID: "f1", Name: "hana_c256_m3200_v2", VCPUs: 256, RAM: 3276800, Disk: 200}, }, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, want: map[string]float64{ - hKey("az1", "sapphire-rapids", "cpu", "p1"): 256, - hKey("az1", "sapphire-rapids", "ram", "p1"): 3276800 * 1024 * 1024, - hKey("az1", "sapphire-rapids", "disk", "p1"): 200 * 1024 * 1024 * 1024, + hKey("az1", "sapphire-rapids", "cpu", pd1): 256, + hKey("az1", "sapphire-rapids", "ram", pd1): 3276800 * 1024 * 1024, + hKey("az1", "sapphire-rapids", "disk", pd1): 200 * 1024 * 1024 * 1024, }, }, { @@ -398,13 +464,15 @@ func TestVMwareResourceCommitmentsKPI_Collect_HANA(t *testing.T) { {ID: "f1", Name: "hana_c128_m1600", VCPUs: 128, RAM: 1638400, Disk: 100}, {ID: "f2", Name: "hana_c128_m1600_v2", VCPUs: 128, RAM: 1638400, Disk: 100}, }, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, want: map[string]float64{ - hKey("az1", "cascade-lake", "cpu", "p1"): 2 * 128, - hKey("az1", "cascade-lake", "ram", "p1"): 2 * 1638400 * 1024 * 1024, - hKey("az1", "cascade-lake", "disk", "p1"): 2 * 100 * 1024 * 1024 * 1024, - hKey("az1", "sapphire-rapids", "cpu", "p1"): 1 * 128, - hKey("az1", "sapphire-rapids", "ram", "p1"): 1 * 1638400 * 1024 * 1024, - hKey("az1", "sapphire-rapids", "disk", "p1"): 1 * 100 * 1024 * 1024 * 1024, + hKey("az1", "cascade-lake", "cpu", pd1): 2 * 128, + hKey("az1", "cascade-lake", "ram", pd1): 2 * 1638400 * 1024 * 1024, + hKey("az1", "cascade-lake", "disk", pd1): 2 * 100 * 1024 * 1024 * 1024, + hKey("az1", "sapphire-rapids", "cpu", pd1): 1 * 128, + hKey("az1", "sapphire-rapids", "ram", pd1): 1 * 1638400 * 1024 * 1024, + hKey("az1", "sapphire-rapids", "disk", pd1): 1 * 100 * 1024 * 1024 * 1024, }, }, { @@ -416,7 +484,9 @@ func TestVMwareResourceCommitmentsKPI_Collect_HANA(t *testing.T) { flavors: []nova.Flavor{ {ID: "f1", Name: "hana_k_large", VCPUs: 64, RAM: 819200, Disk: 50}, }, - want: map[string]float64{}, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, + want: map[string]float64{}, }, { name: "DELETED and ERROR hana servers excluded from running count", @@ -431,10 +501,12 @@ func TestVMwareResourceCommitmentsKPI_Collect_HANA(t *testing.T) { flavors: []nova.Flavor{ {ID: "f1", Name: "hana_small", VCPUs: 64, RAM: 819200, Disk: 50}, }, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, want: map[string]float64{ - hKey("az1", "cascade-lake", "cpu", "p1"): 2 * 64, // 3 committed - 1 ACTIVE = 2 unused - hKey("az1", "cascade-lake", "ram", "p1"): 2 * 819200 * 1024 * 1024, - hKey("az1", "cascade-lake", "disk", "p1"): 2 * 50 * 1024 * 1024 * 1024, + hKey("az1", "cascade-lake", "cpu", pd1): 2 * 64, // 3 committed - 1 ACTIVE = 2 unused + hKey("az1", "cascade-lake", "ram", pd1): 2 * 819200 * 1024 * 1024, + hKey("az1", "cascade-lake", "disk", pd1): 2 * 50 * 1024 * 1024 * 1024, }, }, { @@ -445,10 +517,12 @@ func TestVMwareResourceCommitmentsKPI_Collect_HANA(t *testing.T) { flavors: []nova.Flavor{ {ID: "f1", Name: "hana_small", VCPUs: 64, RAM: 819200, Disk: 50}, }, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, want: map[string]float64{ - hKey("az1", "cascade-lake", "cpu", "p1"): 64, - hKey("az1", "cascade-lake", "ram", "p1"): 819200 * 1024 * 1024, - hKey("az1", "cascade-lake", "disk", "p1"): 50 * 1024 * 1024 * 1024, + hKey("az1", "cascade-lake", "cpu", pd1): 64, + hKey("az1", "cascade-lake", "ram", pd1): 819200 * 1024 * 1024, + hKey("az1", "cascade-lake", "disk", pd1): 50 * 1024 * 1024 * 1024, }, }, { @@ -456,7 +530,9 @@ func TestVMwareResourceCommitmentsKPI_Collect_HANA(t *testing.T) { commitments: []limes.Commitment{ {ID: 1, UUID: "h1", ServiceType: "compute", ResourceName: "instances_hana_nonexistent", AvailabilityZone: "az1", Amount: 2, Status: "confirmed", ProjectID: "p1"}, }, - want: map[string]float64{}, + projects: []identity.Project{p1}, + domains: []identity.Domain{d1}, + want: map[string]float64{}, }, { name: "multiple projects and AZs aggregated per bucket", @@ -468,23 +544,25 @@ func TestVMwareResourceCommitmentsKPI_Collect_HANA(t *testing.T) { flavors: []nova.Flavor{ {ID: "f1", Name: "hana_small", VCPUs: 64, RAM: 819200, Disk: 50}, }, + projects: []identity.Project{p1, p2}, + domains: []identity.Domain{d1}, want: map[string]float64{ - hKey("az1", "cascade-lake", "cpu", "p1"): 2 * 64, - hKey("az1", "cascade-lake", "ram", "p1"): 2 * 819200 * 1024 * 1024, - hKey("az1", "cascade-lake", "disk", "p1"): 2 * 50 * 1024 * 1024 * 1024, - hKey("az2", "cascade-lake", "cpu", "p1"): 3 * 64, - hKey("az2", "cascade-lake", "ram", "p1"): 3 * 819200 * 1024 * 1024, - hKey("az2", "cascade-lake", "disk", "p1"): 3 * 50 * 1024 * 1024 * 1024, - hKey("az1", "cascade-lake", "cpu", "p2"): 1 * 64, - hKey("az1", "cascade-lake", "ram", "p2"): 1 * 819200 * 1024 * 1024, - hKey("az1", "cascade-lake", "disk", "p2"): 1 * 50 * 1024 * 1024 * 1024, + hKey("az1", "cascade-lake", "cpu", pd1): 2 * 64, + hKey("az1", "cascade-lake", "ram", pd1): 2 * 819200 * 1024 * 1024, + hKey("az1", "cascade-lake", "disk", pd1): 2 * 50 * 1024 * 1024 * 1024, + hKey("az2", "cascade-lake", "cpu", pd1): 3 * 64, + hKey("az2", "cascade-lake", "ram", pd1): 3 * 819200 * 1024 * 1024, + hKey("az2", "cascade-lake", "disk", pd1): 3 * 50 * 1024 * 1024 * 1024, + hKey("az1", "cascade-lake", "cpu", pd2): 1 * 64, + hKey("az1", "cascade-lake", "ram", pd2): 1 * 819200 * 1024 * 1024, + hKey("az1", "cascade-lake", "disk", pd2): 1 * 50 * 1024 * 1024 * 1024, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - testDB, cleanup := setupResourceCommitmentsDB(t) + testDB, cleanup := setupProjectCommitmentsDB(t) defer cleanup() var rows []any @@ -497,13 +575,19 @@ func TestVMwareResourceCommitmentsKPI_Collect_HANA(t *testing.T) { for i := range tt.flavors { rows = append(rows, &tt.flavors[i]) } + for i := range tt.projects { + rows = append(rows, &tt.projects[i]) + } + for i := range tt.domains { + rows = append(rows, &tt.domains[i]) + } if len(rows) > 0 { if err := testDB.Insert(rows...); err != nil { t.Fatalf("failed to insert test data: %v", err) } } - got := collectResourceCommitmentsMetrics(t, testDB) + got := collectProjectCommitmentsMetrics(t, testDB) if len(got) != len(tt.want) { t.Errorf("expected %d metrics, got %d: %v", len(tt.want), len(got), got) diff --git a/internal/knowledge/kpis/supported_kpis.go b/internal/knowledge/kpis/supported_kpis.go index 023454fb9..64d8828cb 100644 --- a/internal/knowledge/kpis/supported_kpis.go +++ b/internal/knowledge/kpis/supported_kpis.go @@ -23,10 +23,10 @@ var supportedKPIs = map[string]plugins.KPI{ "vm_commitments_kpi": &compute.VMCommitmentsKPI{}, "vm_faults_kpi": &compute.VMFaultsKPI{}, - "kvm_project_utilization_kpi": &infrastructure.KVMProjectUtilizationKPI{}, - "vmware_project_utilization_kpi": &infrastructure.VMwareProjectUtilizationKPI{}, - "vmware_resource_commitments_kpi": &infrastructure.VMwareResourceCommitmentsKPI{}, - "vmware_host_capacity_kpi": &infrastructure.VMwareHostCapacityKPI{}, + "kvm_project_utilization_kpi": &infrastructure.KVMProjectUtilizationKPI{}, + "vmware_project_utilization_kpi": &infrastructure.VMwareProjectUtilizationKPI{}, + "vmware_project_commitments_kpi": &infrastructure.VMwareProjectCommitmentsKPI{}, + "vmware_host_capacity_kpi": &infrastructure.VMwareHostCapacityKPI{}, "netapp_storage_pool_cpu_usage_kpi": &storage.NetAppStoragePoolCPUUsageKPI{},