From 544d323154fc2c1119e9b5f4c31d1a98879ad7ae Mon Sep 17 00:00:00 2001 From: Stefan Majewsky Date: Fri, 15 Sep 2023 14:15:14 +0200 Subject: [PATCH] add full AZ awareness to capacity plugins This is the first half of a refactor of the scrape methods towards full AZ awareness. Scraping can return data either for the entire region, or broken down by AZs. This distinction is represented by the enum type Topological. For capacity scraping, this does not present a huge change, because several capacitors already report capacity by AZ. What changes is that: - subcapacities are now reported on the AZ (instead of regional) level - the region-wide capacity is computed outside of the plugin by summing the individual AZ capacities The frontend does not change in any way at this point. These changes will come later, once the user-visible changes have been agreed upon. This changeset is about preparing the things that we will definitely need, and making data available to the collector that we will need to surface in the frontend later on. --- internal/collector/capacity_scrape.go | 18 ++--- internal/collector/capacity_scrape_test.go | 4 +- internal/core/data.go | 94 ++++++++++++++++++++++ internal/core/plugin.go | 20 +---- internal/plugins/capacity_cinder.go | 34 +++----- internal/plugins/capacity_manila.go | 85 ++++++++----------- internal/plugins/capacity_manual.go | 8 +- internal/plugins/capacity_nova.go | 43 +++++----- internal/plugins/capacity_prometheus.go | 8 +- internal/plugins/capacity_sapcc_ironic.go | 37 +++------ internal/test/plugins/capacity_static.go | 48 ++++++----- 11 files changed, 218 insertions(+), 181 deletions(-) create mode 100644 internal/core/data.go 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 }