diff --git a/internal/collector/capacity_scrape.go b/internal/collector/capacity_scrape.go index 5e8f1f23b..cbf2991f6 100644 --- a/internal/collector/capacity_scrape.go +++ b/internal/collector/capacity_scrape.go @@ -220,7 +220,7 @@ func (c *Collector) processCapacityScrapeTask(_ context.Context, task capacitySc continue } - for resourceName, resourceData := range serviceData { + for resourceName, topologicalResourceData := range serviceData { if !c.Cluster.HasResource(serviceType, resourceName) { logg.Info("discarding capacity reported by %s for unknown resource name: %s/%s", capacitor.CapacitorID, serviceType, resourceName) continue @@ -230,25 +230,26 @@ func (c *Collector) processCapacityScrapeTask(_ context.Context, task capacitySc return fmt.Errorf("no cluster_services entry for service type %s (check if CheckConsistencyJob runs correctly)", serviceType) } + summedResourceData := topologicalResourceData.Sum() res := db.ClusterResource{ ServiceID: serviceID, Name: resourceName, - RawCapacity: resourceData.Capacity, + RawCapacity: summedResourceData.Capacity, CapacityPerAZJSON: "", //see below SubcapacitiesJSON: "", //see below CapacitorID: capacitor.CapacitorID, } - if len(resourceData.CapacityPerAZ) > 0 { - buf, err := json.Marshal(convertAZReport(resourceData.CapacityPerAZ)) + if topologicalResourceData.PerAZ != nil { + buf, err := json.Marshal(convertAZReport(topologicalResourceData.PerAZ)) if err != nil { return fmt.Errorf("could not convert capacities per AZ to JSON: %w", err) } res.CapacityPerAZJSON = string(buf) } - if len(resourceData.Subcapacities) > 0 { - buf, err := json.Marshal(resourceData.Subcapacities) + if len(summedResourceData.Subcapacities) > 0 { + buf, err := json.Marshal(summedResourceData.Subcapacities) if err != nil { return fmt.Errorf("could not convert subcapacities to JSON: %w", err) } @@ -297,10 +298,7 @@ func (c *Collector) processCapacityScrapeTask(_ context.Context, task capacitySc return tx.Commit() } -func convertAZReport(capacityPerAZ map[string]*core.CapacityDataForAZ) limesresources.ClusterAvailabilityZoneReports { - //The initial implementation wrote limesresources.ClusterAvailabilityZoneReports into the CapacityPerAZJSON database field, - //even though map[string]*core.CapacityDataForAZ would have been more appropriate. - //Now we stick with it for compatibility's sake. +func convertAZReport(capacityPerAZ map[string]*core.CapacityData) limesresources.ClusterAvailabilityZoneReports { report := make(limesresources.ClusterAvailabilityZoneReports, len(capacityPerAZ)) for azName, azData := range capacityPerAZ { report[azName] = &limesresources.ClusterAvailabilityZoneReport{ diff --git a/internal/collector/capacity_scrape_test.go b/internal/collector/capacity_scrape_test.go index 91728e528..bef368904 100644 --- a/internal/collector/capacity_scrape_test.go +++ b/internal/collector/capacity_scrape_test.go @@ -161,7 +161,7 @@ func Test_ScanCapacity(t *testing.T) { UPDATE cluster_capacitors SET scraped_at = %d, next_scrape_at = %d WHERE capacitor_id = 'unittest'; UPDATE cluster_capacitors SET scraped_at = %d, next_scrape_at = %d WHERE capacitor_id = 'unittest2'; INSERT INTO cluster_capacitors (capacitor_id, scraped_at, scrape_duration_secs, serialized_metrics, next_scrape_at) VALUES ('unittest4', %d, 5, '{"smaller_half":14,"larger_half":28}', %d); - INSERT INTO cluster_resources (service_id, name, capacity, subcapacities, capacitor_id) VALUES (2, 'things', 42, '[{"smaller_half":14},{"larger_half":28}]', 'unittest4'); + INSERT INTO cluster_resources (service_id, name, capacity, subcapacities, capacitor_id) VALUES (2, 'things', 42, '[{"az":"az-one","smaller_half":7},{"az":"az-one","larger_half":14},{"az":"az-two","smaller_half":7},{"az":"az-two","larger_half":14}]', 'unittest4'); `, scrapedAt1.Unix(), scrapedAt1.Add(15*time.Minute).Unix(), scrapedAt2.Unix(), scrapedAt2.Add(15*time.Minute).Unix(), @@ -180,7 +180,7 @@ func Test_ScanCapacity(t *testing.T) { UPDATE cluster_capacitors SET scraped_at = %d, next_scrape_at = %d WHERE capacitor_id = 'unittest'; UPDATE cluster_capacitors SET scraped_at = %d, next_scrape_at = %d WHERE capacitor_id = 'unittest2'; UPDATE cluster_capacitors SET scraped_at = %d, serialized_metrics = '{"smaller_half":3,"larger_half":7}', next_scrape_at = %d WHERE capacitor_id = 'unittest4'; - UPDATE cluster_resources SET capacity = 10, subcapacities = '[{"smaller_half":3},{"larger_half":7}]' WHERE service_id = 2 AND name = 'things'; + UPDATE cluster_resources SET capacity = 10, subcapacities = '[{"az":"az-one","smaller_half":1},{"az":"az-one","larger_half":4},{"az":"az-two","smaller_half":1},{"az":"az-two","larger_half":4}]' WHERE service_id = 2 AND name = 'things'; `, scrapedAt1.Unix(), scrapedAt1.Add(15*time.Minute).Unix(), scrapedAt2.Unix(), scrapedAt2.Add(15*time.Minute).Unix(), diff --git a/internal/core/data.go b/internal/core/data.go new file mode 100644 index 000000000..0a2cb6c23 --- /dev/null +++ b/internal/core/data.go @@ -0,0 +1,94 @@ +/******************************************************************************* +* +* Copyright 2023 SAP SE +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You should have received a copy of the License along with this +* program. If not, you may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*******************************************************************************/ + +package core + +import ( + "slices" + "sort" +) + +// Topological is a container for data that can either be reported for +// the entire region at once, or separately by AZ. +// Exactly one field shall be non-nil. +type Topological[D TopologicalData[D]] struct { + Regional *D + PerAZ map[string]*D +} + +// Regional is a shorthand to construct a Topological instance with the Regional member filled. +func Regional[D TopologicalData[D]](data D) Topological[D] { + return Topological[D]{Regional: &data} +} + +// PerAZ is a shorthand to construct a Topological instance with the PerAZ member filled. +func PerAZ[D TopologicalData[D]](data map[string]*D) Topological[D] { + return Topological[D]{PerAZ: data} +} + +// Sum returns a sum of all data in this container. +// If the Regional field is filled, that data is returned directly. +// Otherwise, all entries in the PerAZ field are summed together. +func (t Topological[D]) Sum() D { + if t.PerAZ == nil { + return *t.Regional + } + + //fold AZ data in a well-defined order for deterministic test result + azNames := make([]string, 0, len(t.PerAZ)) + for az := range t.PerAZ { + azNames = append(azNames, az) + } + sort.Strings(azNames) + + var result D + for _, az := range azNames { + result = result.add(*t.PerAZ[az]) + } + return result +} + +// TopologicalData is an interfaces for types that can be put into the Topological container. +type TopologicalData[Self any] interface { + // List of permitted types. This is required for type inference, as explained here: + // + CapacityData + + // Computes the sum of this structure and `other`. + // This is used to implement Topological.Sum(). + add(other Self) Self +} + +// CapacityData contains capacity data for a single project resource. +type CapacityData struct { + Capacity uint64 + Usage uint64 //NOTE: currently only relevant on AZ level, regional level uses the aggregation of project usages + Subcapacities []any //only if supported by plugin and enabled in config +} + +// add implements the TopologicalData interface. +// +//nolint:unused // looks like a linter bug +func (d CapacityData) add(other CapacityData) CapacityData { + return CapacityData{ + Capacity: d.Capacity + other.Capacity, + Usage: d.Usage + other.Usage, + Subcapacities: append(slices.Clone(d.Subcapacities), other.Subcapacities...), + } +} diff --git a/internal/core/plugin.go b/internal/core/plugin.go index 2c14197d1..c40d9e4e8 100644 --- a/internal/core/plugin.go +++ b/internal/core/plugin.go @@ -182,24 +182,6 @@ type QuotaPlugin interface { CollectMetrics(ch chan<- prometheus.Metric, project KeystoneProject, serializedMetrics string) error } -// CapacityData contains the total and per-availability-zone capacity data for a -// single resource. -// -// The Subcapacities field may optionally be populated with subcapacities, if the -// capacity plugin providing this CapacityData instance has been instructed to (and -// is able to) scrape subcapacities for this resource. -type CapacityData struct { - Capacity uint64 - CapacityPerAZ map[string]*CapacityDataForAZ - Subcapacities []any -} - -// CapacityDataForAZ is the capacity data for a single resource in a single AZ. -type CapacityDataForAZ struct { - Capacity uint64 - Usage uint64 -} - // CapacityPlugin is the interface that all capacity collector plugins must // implement. // @@ -231,7 +213,7 @@ type CapacityPlugin interface { // //The serializedMetrics return value is persisted in the Limes DB and //supplied to all subsequent RenderMetrics calls. - Scrape() (result map[string]map[string]CapacityData, serializedMetrics string, err error) + Scrape() (result map[string]map[string]Topological[CapacityData], serializedMetrics string, err error) //DescribeMetrics is called when Prometheus is scraping metrics from //limes-collect, to provide an opportunity to the plugin to emit its own diff --git a/internal/plugins/capacity_cinder.go b/internal/plugins/capacity_cinder.go index 8afa1c6f2..48ce53c04 100644 --- a/internal/plugins/capacity_cinder.go +++ b/internal/plugins/capacity_cinder.go @@ -82,7 +82,7 @@ func (p *capacityCinderPlugin) makeResourceName(volumeType string) string { } // Scrape implements the core.CapacityPlugin interface. -func (p *capacityCinderPlugin) Scrape() (result map[string]map[string]core.CapacityData, serializedMetrics string, err error) { +func (p *capacityCinderPlugin) Scrape() (result map[string]map[string]core.Topological[core.CapacityData], serializedMetrics string, err error) { //list storage pools var poolData struct { StoragePools []struct { @@ -126,14 +126,11 @@ func (p *capacityCinderPlugin) Scrape() (result map[string]map[string]core.Capac } } - capaData := make(map[string]*core.CapacityData) + capaData := make(map[string]core.Topological[core.CapacityData]) volumeTypesByBackendName := make(map[string]string) for volumeType, cfg := range p.VolumeTypes { volumeTypesByBackendName[cfg.VolumeBackendName] = volumeType - capaData[p.makeResourceName(volumeType)] = &core.CapacityData{ - Capacity: 0, - CapacityPerAZ: make(map[string]*core.CapacityDataForAZ), - } + capaData[p.makeResourceName(volumeType)] = core.PerAZ(make(map[string]*core.CapacityData)) } //add results from scheduler-stats @@ -153,12 +150,8 @@ func (p *capacityCinderPlugin) Scrape() (result map[string]map[string]core.Capac logg.Info("Cinder capacity plugin: skipping pool %q with unknown volume_backend_name %q", pool.Name, pool.Capabilities.VolumeBackendName) continue } - logg.Debug("Cinder capacity plugin: considering pool %q with volume_backend_name %q for volume type %q", pool.Name, pool.Capabilities.VolumeBackendName, volumeType) - resourceName := p.makeResourceName(volumeType) - capaData[resourceName].Capacity += uint64(pool.Capabilities.TotalCapacityGB) - var poolAZ string for az, hosts := range serviceHostsPerAZ { for _, v := range hosts { @@ -173,16 +166,19 @@ func (p *capacityCinderPlugin) Scrape() (result map[string]map[string]core.Capac logg.Info("Cinder storage pool %q does not match any service host", pool.Name) poolAZ = "unknown" } - if _, ok := capaData[resourceName].CapacityPerAZ[poolAZ]; !ok { - capaData[resourceName].CapacityPerAZ[poolAZ] = &core.CapacityDataForAZ{} + + resourceName := p.makeResourceName(volumeType) + capa := capaData[resourceName].PerAZ[poolAZ] + if capa == nil { + capa = &core.CapacityData{} + capaData[resourceName].PerAZ[poolAZ] = capa } - azCapaData := capaData[resourceName].CapacityPerAZ[poolAZ] - azCapaData.Capacity += uint64(pool.Capabilities.TotalCapacityGB) - azCapaData.Usage += uint64(pool.Capabilities.AllocatedCapacityGB) + capa.Capacity += uint64(pool.Capabilities.TotalCapacityGB) + capa.Usage += uint64(pool.Capabilities.AllocatedCapacityGB) if p.reportSubcapacities["capacity"] { - capaData[resourceName].Subcapacities = append(capaData[resourceName].Subcapacities, storagePoolSubcapacity{ + capa.Subcapacities = append(capa.Subcapacities, storagePoolSubcapacity{ PoolName: pool.Name, AvailabilityZone: poolAZ, CapacityGiB: uint64(pool.Capabilities.TotalCapacityGB), @@ -191,11 +187,7 @@ func (p *capacityCinderPlugin) Scrape() (result map[string]map[string]core.Capac } } - capaDataFinal := make(map[string]core.CapacityData) - for k, v := range capaData { - capaDataFinal[k] = *v - } - return map[string]map[string]core.CapacityData{"volumev2": capaDataFinal}, "", nil + return map[string]map[string]core.Topological[core.CapacityData]{"volumev2": capaData}, "", nil } // DescribeMetrics implements the core.CapacityPlugin interface. diff --git a/internal/plugins/capacity_manila.go b/internal/plugins/capacity_manila.go index 374187f30..372175438 100644 --- a/internal/plugins/capacity_manila.go +++ b/internal/plugins/capacity_manila.go @@ -101,7 +101,7 @@ func (p *capacityManilaPlugin) makeResourceName(kind string, shareType ManilaSha } // Scrape implements the core.CapacityPlugin interface. -func (p *capacityManilaPlugin) Scrape() (result map[string]map[string]core.CapacityData, _ string, err error) { +func (p *capacityManilaPlugin) Scrape() (result map[string]map[string]core.Topological[core.CapacityData], _ string, err error) { allPages, err := services.List(p.ManilaV2, nil).AllPages() if err != nil { return nil, "", err @@ -124,8 +124,8 @@ func (p *capacityManilaPlugin) Scrape() (result map[string]map[string]core.Capac } } - caps := map[string]core.CapacityData{ - "share_networks": {Capacity: p.ShareNetworks}, + caps := map[string]core.Topological[core.CapacityData]{ + "share_networks": core.Regional(core.CapacityData{Capacity: p.ShareNetworks}), } for _, shareType := range p.ShareTypes { capForType, err := p.scrapeForShareType(shareType, azForServiceHost) @@ -137,7 +137,7 @@ func (p *capacityManilaPlugin) Scrape() (result map[string]map[string]core.Capac caps[p.makeResourceName("share_capacity", shareType)] = capForType.ShareGigabytes caps[p.makeResourceName("snapshot_capacity", shareType)] = capForType.SnapshotGigabytes } - return map[string]map[string]core.CapacityData{"sharev2": caps}, "", nil + return map[string]map[string]core.Topological[core.CapacityData]{"sharev2": caps}, "", nil } // DescribeMetrics implements the core.CapacityPlugin interface. @@ -152,10 +152,10 @@ func (p *capacityManilaPlugin) CollectMetrics(ch chan<- prometheus.Metric, seria } type capacityForShareType struct { - Shares core.CapacityData - Snapshots core.CapacityData - ShareGigabytes core.CapacityData - SnapshotGigabytes core.CapacityData + Shares core.Topological[core.CapacityData] + Snapshots core.Topological[core.CapacityData] + ShareGigabytes core.Topological[core.CapacityData] + SnapshotGigabytes core.Topological[core.CapacityData] } func (p *capacityManilaPlugin) scrapeForShareType(shareType ManilaShareTypeSpec, azForServiceHost map[string]string) (capacityForShareType, error) { @@ -181,15 +181,12 @@ func (p *capacityManilaPlugin) scrapeForShareType(shareType ManilaShareTypeSpec, //count pools and their capacities var ( - totalPoolCount uint64 - totalCapacityGb = float64(0) - shareSubcapacities []any - snapshotSubcapacities []any - - availabilityZones = make(map[string]bool) - poolCountPerAZ = make(map[string]uint64) - totalCapacityGbPerAZ = make(map[string]float64) - allocatedCapacityGbPerAZ = make(map[string]float64) + availabilityZones = make(map[string]bool) + poolCountPerAZ = make(map[string]uint64) + totalCapacityGbPerAZ = make(map[string]float64) + allocatedCapacityGbPerAZ = make(map[string]float64) + shareSubcapacitiesPerAZ = make(map[string][]any) + snapshotSubcapacitiesPerAZ = make(map[string][]any) ) for _, pool := range allPools { isIncluded := true @@ -214,9 +211,6 @@ func (p *capacityManilaPlugin) scrapeForShareType(shareType ManilaShareTypeSpec, } if isIncluded { - totalCapacityGb += pool.Capabilities.TotalCapacityGB - totalPoolCount++ - availabilityZones[poolAZ] = true poolCountPerAZ[poolAZ]++ totalCapacityGbPerAZ[poolAZ] += pool.Capabilities.TotalCapacityGB @@ -233,7 +227,7 @@ func (p *capacityManilaPlugin) scrapeForShareType(shareType ManilaShareTypeSpec, if !isIncluded { subcapa.ExclusionReason = "hardware_state = " + pool.Capabilities.HardwareState } - shareSubcapacities = append(shareSubcapacities, subcapa) + shareSubcapacitiesPerAZ[poolAZ] = append(shareSubcapacitiesPerAZ[poolAZ], subcapa) } if p.reportSubcapacities["snapshot_capacity"] { subcapa := storagePoolSubcapacity{ @@ -245,48 +239,37 @@ func (p *capacityManilaPlugin) scrapeForShareType(shareType ManilaShareTypeSpec, if !isIncluded { subcapa.ExclusionReason = "hardware_state = " + pool.Capabilities.HardwareState } - snapshotSubcapacities = append(snapshotSubcapacities, subcapa) + snapshotSubcapacitiesPerAZ[poolAZ] = append(snapshotSubcapacitiesPerAZ[poolAZ], subcapa) } } //derive availability zone usage and capacities - var ( - shareCountPerAZ = make(map[string]*core.CapacityDataForAZ) - shareSnapshotsPerAZ = make(map[string]*core.CapacityDataForAZ) - shareCapacityPerAZ = make(map[string]*core.CapacityDataForAZ) - snapshotCapacityPerAZ = make(map[string]*core.CapacityDataForAZ) - ) + result := capacityForShareType{ + Shares: core.PerAZ(map[string]*core.CapacityData{}), + Snapshots: core.PerAZ(map[string]*core.CapacityData{}), + ShareGigabytes: core.PerAZ(map[string]*core.CapacityData{}), + SnapshotGigabytes: core.PerAZ(map[string]*core.CapacityData{}), + } for az := range availabilityZones { - shareCountPerAZ[az] = &core.CapacityDataForAZ{ + result.Shares.PerAZ[az] = &core.CapacityData{ Capacity: getShareCount(poolCountPerAZ[az], p.SharesPerPool, (p.ShareNetworks / uint64(len(availabilityZones)))), } - shareSnapshotsPerAZ[az] = &core.CapacityDataForAZ{ - Capacity: getShareSnapshots(shareCountPerAZ[az].Capacity, p.SnapshotsPerShare), + result.Snapshots.PerAZ[az] = &core.CapacityData{ + Capacity: getShareSnapshots(result.Shares.PerAZ[az].Capacity, p.SnapshotsPerShare), } - shareCapacityPerAZ[az] = &core.CapacityDataForAZ{ - Capacity: getShareCapacity(totalCapacityGbPerAZ[az], capBalance), - Usage: getShareCapacity(allocatedCapacityGbPerAZ[az], capBalance), + result.ShareGigabytes.PerAZ[az] = &core.CapacityData{ + Capacity: getShareCapacity(totalCapacityGbPerAZ[az], capBalance), + Usage: getShareCapacity(allocatedCapacityGbPerAZ[az], capBalance), + Subcapacities: shareSubcapacitiesPerAZ[az], } - snapshotCapacityPerAZ[az] = &core.CapacityDataForAZ{ - Capacity: getSnapshotCapacity(totalCapacityGbPerAZ[az], capBalance), - Usage: getSnapshotCapacity(allocatedCapacityGbPerAZ[az], capBalance), + result.SnapshotGigabytes.PerAZ[az] = &core.CapacityData{ + Capacity: getSnapshotCapacity(totalCapacityGbPerAZ[az], capBalance), + Usage: getSnapshotCapacity(allocatedCapacityGbPerAZ[az], capBalance), + Subcapacities: snapshotSubcapacitiesPerAZ[az], } } - //derive cluster level capacities - var ( - totalShareCount = getShareCount(totalPoolCount, p.SharesPerPool, p.ShareNetworks) - totalShareSnapshots = getShareSnapshots(totalShareCount, p.SnapshotsPerShare) - totalShareCapacity = getShareCapacity(totalCapacityGb, capBalance) - totalSnapshotCapacity = getSnapshotCapacity(totalCapacityGb, capBalance) - ) - - return capacityForShareType{ - Shares: core.CapacityData{Capacity: totalShareCount, CapacityPerAZ: shareCountPerAZ}, - Snapshots: core.CapacityData{Capacity: totalShareSnapshots, CapacityPerAZ: shareSnapshotsPerAZ}, - ShareGigabytes: core.CapacityData{Capacity: totalShareCapacity, CapacityPerAZ: shareCapacityPerAZ, Subcapacities: shareSubcapacities}, - SnapshotGigabytes: core.CapacityData{Capacity: totalSnapshotCapacity, CapacityPerAZ: snapshotCapacityPerAZ, Subcapacities: snapshotSubcapacities}, - }, nil + return result, nil } func getShareCount(poolCount, sharesPerPool, shareNetworks uint64) uint64 { diff --git a/internal/plugins/capacity_manual.go b/internal/plugins/capacity_manual.go index 751e5cee8..ea5c04e84 100644 --- a/internal/plugins/capacity_manual.go +++ b/internal/plugins/capacity_manual.go @@ -49,16 +49,16 @@ func (p *capacityManualPlugin) PluginTypeID() string { var errNoManualData = errors.New(`missing values for capacitor plugin "manual"`) // Scrape implements the core.CapacityPlugin interface. -func (p *capacityManualPlugin) Scrape() (result map[string]map[string]core.CapacityData, _ string, err error) { +func (p *capacityManualPlugin) Scrape() (result map[string]map[string]core.Topological[core.CapacityData], _ string, err error) { if p.Values == nil { return nil, "", errNoManualData } - result = make(map[string]map[string]core.CapacityData) + result = make(map[string]map[string]core.Topological[core.CapacityData]) for serviceType, serviceData := range p.Values { - serviceResult := make(map[string]core.CapacityData) + serviceResult := make(map[string]core.Topological[core.CapacityData]) for resourceName, capacity := range serviceData { - serviceResult[resourceName] = core.CapacityData{Capacity: capacity} + serviceResult[resourceName] = core.Regional(core.CapacityData{Capacity: capacity}) } result[serviceType] = serviceResult } diff --git a/internal/plugins/capacity_nova.go b/internal/plugins/capacity_nova.go index 0bc5cd3ae..d42e7f21d 100644 --- a/internal/plugins/capacity_nova.go +++ b/internal/plugins/capacity_nova.go @@ -99,7 +99,7 @@ func (p *capacityNovaPlugin) PluginTypeID() string { } // Scrape implements the core.CapacityPlugin interface. -func (p *capacityNovaPlugin) Scrape() (result map[string]map[string]core.CapacityData, serializedMetrics string, err error) { +func (p *capacityNovaPlugin) Scrape() (result map[string]map[string]core.Topological[core.CapacityData], serializedMetrics string, err error) { //enumerate aggregates which establish the hypervisor <-> AZ mapping page, err := aggregates.List(p.NovaV2).AllPages() if err != nil { @@ -143,10 +143,8 @@ func (p *capacityNovaPlugin) Scrape() (result map[string]map[string]core.Capacit //we need to prepare several aggregations in the big loop below var ( - resourceNames = []string{"cores", "instances", "ram"} - totalCapacity partialNovaCapacity - azCapacities = make(map[string]*partialNovaCapacity) - hvSubcapacities = make(map[string][]any) + resourceNames = []string{"cores", "instances", "ram"} + azCapacities = make(map[string]*partialNovaCapacity) ) //foreach hypervisor... @@ -252,9 +250,7 @@ func (p *capacityNovaPlugin) Scrape() (result map[string]map[string]core.Capacit } hvCapacity.MatchingAggregates = map[string]bool{matchingAggregateName: true} - //count this hypervisor's capacity towards the totals for the whole cloud... - totalCapacity.Add(hvCapacity) - //...and the AZ level + //count this hypervisor's capacity towards the totals for the AZ level azCapacity := azCapacities[matchingAvailabilityZone] if azCapacity == nil { azCapacity = &partialNovaCapacity{} @@ -266,7 +262,7 @@ func (p *capacityNovaPlugin) Scrape() (result map[string]map[string]core.Capacit for _, resName := range resourceNames { if p.reportSubcapacities[resName] { resCapa := hvCapacity.GetCapacity(resName, maxRootDiskSize) - hvSubcapacities[resName] = append(hvSubcapacities[resName], novaHypervisorSubcapacity{ + azCapacity.Subcapacities = append(azCapacity.Subcapacities, novaHypervisorSubcapacity{ ServiceHost: hypervisor.Service.Host, AggregateName: matchingAggregateName, AvailabilityZone: matchingAvailabilityZone, @@ -279,18 +275,14 @@ func (p *capacityNovaPlugin) Scrape() (result map[string]map[string]core.Capacit } //build final report - capacities := make(map[string]core.CapacityData, len(resourceNames)) + capacities := make(map[string]core.Topological[core.CapacityData], len(resourceNames)) for _, resName := range resourceNames { - resCapa := totalCapacity.GetCapacity(resName, maxRootDiskSize) - capacities[resName] = core.CapacityData{ - Capacity: resCapa.Capacity, - CapacityPerAZ: make(map[string]*core.CapacityDataForAZ, len(azCapacities)), - Subcapacities: hvSubcapacities[resName], - } + resCapaPerAZ := make(map[string]*core.CapacityData, len(azCapacities)) for az, azCapacity := range azCapacities { resCapa := azCapacity.GetCapacity(resName, maxRootDiskSize) - capacities[resName].CapacityPerAZ[az] = &resCapa + resCapaPerAZ[az] = &resCapa } + capacities[resName] = core.PerAZ(resCapaPerAZ) } if maxRootDiskSize == 0 { @@ -299,7 +291,7 @@ func (p *capacityNovaPlugin) Scrape() (result map[string]map[string]core.Capacit } serializedMetricsBytes, err := json.Marshal(metrics) - return map[string]map[string]core.CapacityData{"compute": capacities}, string(serializedMetricsBytes), err + return map[string]map[string]core.Topological[core.CapacityData]{"compute": capacities}, string(serializedMetricsBytes), err } var novaHypervisorWellformedGauge = prometheus.NewGaugeVec( @@ -359,13 +351,14 @@ func nameListToLabelValue(names []string) string { return strings.Join(names, ",") } -// The capacity of any level of the Nova superstructure (hypervisor, aggregate, AZ, cluster). +// The capacity of any level of the Nova superstructure (hypervisor, aggregate, AZ). type partialNovaCapacity struct { - VCPUs core.CapacityDataForAZ - MemoryMB core.CapacityDataForAZ + VCPUs core.CapacityData + MemoryMB core.CapacityData LocalGB uint64 RunningVMs uint64 MatchingAggregates map[string]bool + Subcapacities []any // only filled on AZ level } func (c partialNovaCapacity) IsEmpty() bool { @@ -390,7 +383,7 @@ func (c *partialNovaCapacity) Add(other partialNovaCapacity) { } } -func (c partialNovaCapacity) GetCapacity(resourceName string, maxRootDiskSize float64) core.CapacityDataForAZ { +func (c partialNovaCapacity) GetCapacity(resourceName string, maxRootDiskSize float64) core.CapacityData { switch resourceName { case "cores": return c.VCPUs @@ -404,7 +397,7 @@ func (c partialNovaCapacity) GetCapacity(resourceName string, maxRootDiskSize fl amount = maxAmount } } - return core.CapacityDataForAZ{ + return core.CapacityData{ Capacity: amount, Usage: c.RunningVMs, } @@ -464,11 +457,11 @@ func (h novaHypervisor) getCapacity(placementClient *gophercloud.ServiceClient, } return partialNovaCapacity{ - VCPUs: core.CapacityDataForAZ{ + VCPUs: core.CapacityData{ Capacity: uint64(inventory.Inventories["VCPU"].Total - inventory.Inventories["VCPU"].Reserved), Usage: uint64(usages.Usages["VCPU"]), }, - MemoryMB: core.CapacityDataForAZ{ + MemoryMB: core.CapacityData{ Capacity: uint64(inventory.Inventories["MEMORY_MB"].Total - inventory.Inventories["MEMORY_MB"].Reserved), Usage: uint64(usages.Usages["MEMORY_MB"]), }, diff --git a/internal/plugins/capacity_prometheus.go b/internal/plugins/capacity_prometheus.go index 4048f60ab..471f425ad 100644 --- a/internal/plugins/capacity_prometheus.go +++ b/internal/plugins/capacity_prometheus.go @@ -47,21 +47,21 @@ func (p *capacityPrometheusPlugin) PluginTypeID() string { } // Scrape implements the core.CapacityPlugin interface. -func (p *capacityPrometheusPlugin) Scrape() (result map[string]map[string]core.CapacityData, _ string, err error) { +func (p *capacityPrometheusPlugin) Scrape() (result map[string]map[string]core.Topological[core.CapacityData], _ string, err error) { client, err := p.APIConfig.Connect() if err != nil { return nil, "", err } - result = make(map[string]map[string]core.CapacityData) + result = make(map[string]map[string]core.Topological[core.CapacityData]) for serviceType, queries := range p.Queries { - serviceResult := make(map[string]core.CapacityData) + serviceResult := make(map[string]core.Topological[core.CapacityData]) for resourceName, query := range queries { value, err := client.GetSingleValue(query, nil) if err != nil { return nil, "", err } - serviceResult[resourceName] = core.CapacityData{Capacity: uint64(value)} + serviceResult[resourceName] = core.Regional(core.CapacityData{Capacity: uint64(value)}) } result[serviceType] = serviceResult } diff --git a/internal/plugins/capacity_sapcc_ironic.go b/internal/plugins/capacity_sapcc_ironic.go index a7c752067..7ba34afa1 100644 --- a/internal/plugins/capacity_sapcc_ironic.go +++ b/internal/plugins/capacity_sapcc_ironic.go @@ -157,7 +157,7 @@ var nodeNameRx = regexp.MustCompile(`^node(?:swift)?\d+-((?:b[bm]|ap|md|st|swf)\ var cpNodeNameRx = regexp.MustCompile(`^node(?:swift)?\d+-(cp\d+)$`) // Scrape implements the core.CapacityPlugin interface. -func (p *capacitySapccIronicPlugin) Scrape() (result map[string]map[string]core.CapacityData, serializedMetrics string, err error) { +func (p *capacitySapccIronicPlugin) Scrape() (result map[string]map[string]core.Topological[core.CapacityData], serializedMetrics string, err error) { //collect info about flavors with separate instance quota flavorNames, err := p.ftt.ListFlavorsWithSeparateInstanceQuota(p.NovaV2) if err != nil { @@ -165,14 +165,12 @@ func (p *capacitySapccIronicPlugin) Scrape() (result map[string]map[string]core. } //we are going to report capacity for all per-flavor instance quotas - resultCompute := make(map[string]*core.CapacityData) + resultCompute := make(map[string]core.Topological[core.CapacityData]) for _, flavorName := range flavorNames { //NOTE: If `flavor_name_pattern` is empty, then FlavorNameRx will match any input. if p.FlavorNameRx.MatchString(flavorName) { - resultCompute[p.ftt.LimesResourceNameForFlavor(flavorName)] = &core.CapacityData{ - Capacity: 0, - CapacityPerAZ: map[string]*core.CapacityDataForAZ{}, - } + resName := p.ftt.LimesResourceNameForFlavor(flavorName) + resultCompute[resName] = core.PerAZ(make(map[string]*core.CapacityData)) } } @@ -247,9 +245,6 @@ func (p *capacitySapccIronicPlugin) Scrape() (result map[string]map[string]core. if node.Matches(flavorName) { logg.Debug("Ironic node %q (%s) matches flavor %s", node.Name, node.ID, flavorName) - data := resultCompute[p.ftt.LimesResourceNameForFlavor(flavorName)] - data.Capacity++ - var nodeAZ string if match := cpNodeNameRx.FindStringSubmatch(node.Name); match != nil { //special case as explained above (near definition of `cpNodeNameRx`) @@ -264,12 +259,16 @@ func (p *capacitySapccIronicPlugin) Scrape() (result map[string]map[string]core. logg.Error(`Ironic node %q (%s) does not match the "nodeXXX-{bm,bb,ap,md,st,swf}YYY" naming convention`, node.Name, node.ID) } - if _, ok := data.CapacityPerAZ[nodeAZ]; !ok { - data.CapacityPerAZ[nodeAZ] = &core.CapacityDataForAZ{} + resName := p.ftt.LimesResourceNameForFlavor(flavorName) + data := resultCompute[resName].PerAZ[nodeAZ] + if data == nil { + data = &core.CapacityData{} + resultCompute[resName].PerAZ[nodeAZ] = data } - data.CapacityPerAZ[nodeAZ].Capacity++ + + data.Capacity++ if node.StableProvisionState() == "active" { - data.CapacityPerAZ[nodeAZ].Usage++ + data.Usage++ } if p.reportSubcapacities { @@ -312,18 +311,8 @@ func (p *capacitySapccIronicPlugin) Scrape() (result map[string]map[string]core. } } - //remove pointers from `result` - result2 := make(map[string]core.CapacityData, len(resultCompute)) - for resourceName, data := range resultCompute { - result2[resourceName] = *data - } - serializedMetricsBytes, err := json.Marshal(metrics) - if err != nil { - return nil, "", err - } - - return map[string]map[string]core.CapacityData{"compute": result2}, string(serializedMetricsBytes), nil + return map[string]map[string]core.Topological[core.CapacityData]{"compute": resultCompute}, string(serializedMetricsBytes), err } var ( diff --git a/internal/test/plugins/capacity_static.go b/internal/test/plugins/capacity_static.go index 943ff0e24..aa2a26b67 100644 --- a/internal/test/plugins/capacity_static.go +++ b/internal/test/plugins/capacity_static.go @@ -53,41 +53,47 @@ func (p *StaticCapacityPlugin) PluginTypeID() string { } // Scrape implements the core.CapacityPlugin interface. -func (p *StaticCapacityPlugin) Scrape() (result map[string]map[string]core.CapacityData, serializedMetrics string, err error) { - var capacityPerAZ map[string]*core.CapacityDataForAZ - if p.WithAZCapData { - capacityPerAZ = map[string]*core.CapacityDataForAZ{ - "az-one": { - Capacity: p.Capacity / 2, - Usage: uint64(float64(p.Capacity) * 0.1), - }, - "az-two": { - Capacity: p.Capacity / 2, - Usage: uint64(float64(p.Capacity) * 0.1), - }, +func (p *StaticCapacityPlugin) Scrape() (result map[string]map[string]core.Topological[core.CapacityData], serializedMetrics string, err error) { + makeAZCapa := func(az string, capacity, usage uint64) *core.CapacityData { + var subcapacities []any + if p.WithSubcapacities { + smallerHalf := capacity / 3 + largerHalf := capacity - smallerHalf + subcapacities = []any{ + map[string]any{"az": az, "smaller_half": smallerHalf}, + map[string]any{"az": az, "larger_half": largerHalf}, + } } + return &core.CapacityData{ + Capacity: capacity, + Usage: usage, + Subcapacities: subcapacities, + } + } + + fullCapa := core.PerAZ(map[string]*core.CapacityData{ + "az-one": makeAZCapa("az-one", p.Capacity/2, p.Capacity/10), + "az-two": makeAZCapa("az-two", p.Capacity-p.Capacity/2, p.Capacity/10), + }) + if !p.WithAZCapData { + fullCapa = core.Regional(fullCapa.Sum()) } - var subcapacities []any if p.WithSubcapacities { + //for historical reasons, serialized metrics are tested at the same time as subcapacities smallerHalf := p.Capacity / 3 largerHalf := p.Capacity - smallerHalf - subcapacities = []any{ - map[string]uint64{"smaller_half": smallerHalf}, - map[string]uint64{"larger_half": largerHalf}, - } - //this is also an opportunity to test serialized metrics serializedMetrics = fmt.Sprintf(`{"smaller_half":%d,"larger_half":%d}`, smallerHalf, largerHalf) } - result = make(map[string]map[string]core.CapacityData) + result = make(map[string]map[string]core.Topological[core.CapacityData]) for _, str := range p.Resources { parts := strings.SplitN(str, "/", 2) _, exists := result[parts[0]] if !exists { - result[parts[0]] = make(map[string]core.CapacityData) + result[parts[0]] = make(map[string]core.Topological[core.CapacityData]) } - result[parts[0]][parts[1]] = core.CapacityData{Capacity: p.Capacity, CapacityPerAZ: capacityPerAZ, Subcapacities: subcapacities} + result[parts[0]][parts[1]] = fullCapa } return result, serializedMetrics, nil }