From a242be7a587c16351c1387fa051f3a2981df039a Mon Sep 17 00:00:00 2001 From: Varsius <42544122+Varsius@users.noreply.github.com> Date: Tue, 17 Dec 2024 14:40:45 +0100 Subject: [PATCH] Add liquid nova management for computated states --- internal/liquids/nova/liquid.go | 76 +++++++++++++++++++-------------- internal/liquids/nova/usage.go | 8 ++-- 2 files changed, 48 insertions(+), 36 deletions(-) diff --git a/internal/liquids/nova/liquid.go b/internal/liquids/nova/liquid.go index b0bc6e2f..f4d5d6be 100644 --- a/internal/liquids/nova/liquid.go +++ b/internal/liquids/nova/liquid.go @@ -33,6 +33,8 @@ import ( "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors" "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/quotasets" "github.com/sapcc/go-api-declarations/liquid" + + "github.com/sapcc/limes/internal/liquids" ) type Logic struct { @@ -43,9 +45,9 @@ type Logic struct { OSTypeProber *OSTypeProber `json:"-"` ServerGroupProber *ServerGroupProber `json:"-"` // computed state - ignoredFlavorNames []string `json:"-"` - hasPooledResource map[string]map[liquid.ResourceName]bool `json:"-"` - hwVersionResources []liquid.ResourceName `json:"-"` + ignoredFlavorNames liquids.State[[]string] `json:"-"` + hasPooledResource liquids.State[map[string]map[liquid.ResourceName]bool] `json:"-"` + hwVersionResources liquids.State[[]liquid.ResourceName] `json:"-"` } // Init implements the liquidapi.Logic interface. @@ -66,6 +68,28 @@ func (l *Logic) Init(ctx context.Context, provider *gophercloud.ProviderClient, l.OSTypeProber = NewOSTypeProber(l.NovaV2, cinderV3, glanceV2) l.ServerGroupProber = NewServerGroupProber(l.NovaV2) + return nil +} + +func getDefaultQuotaClassSet(ctx context.Context, novaV2 *gophercloud.ServiceClient) (map[string]any, error) { + url := novaV2.ServiceURL("os-quota-class-sets", "default") + var result gophercloud.Result + _, err := novaV2.Get(ctx, url, &result.Body, nil) //nolint:bodyclose + if err != nil { + return nil, err + } + + var body struct { + //NOTE: cannot use map[string]int64 here because this object contains the + // field "id": "default" (curse you, untyped JSON) + QuotaClassSet map[string]any `json:"quota_class_set"` + } + err = result.ExtractInto(&body) + return body.QuotaClassSet, err +} + +// BuildServiceInfo implements the liquidapi.Logic interface. +func (l *Logic) BuildServiceInfo(ctx context.Context) (liquid.ServiceInfo, error) { // SAPCC extension: Nova may report quotas with this name pattern in its quota sets and quota class sets. // If it does, instances with flavors that have the extra spec `quota:hw_version` set to the first match // group of this regexp will count towards those quotas instead of the regular `cores/instances/ram` quotas. @@ -73,9 +97,10 @@ func (l *Logic) Init(ctx context.Context, provider *gophercloud.ProviderClient, // This initialization enumerates which such pooled resources exist. defaultQuotaClassSet, err := getDefaultQuotaClassSet(ctx, l.NovaV2) if err != nil { - return fmt.Errorf("while enumerating default quotas: %w", err) + return liquid.ServiceInfo{}, fmt.Errorf("while enumerating default quotas: %w", err) } - l.hasPooledResource = make(map[string]map[liquid.ResourceName]bool) + hasPooledResource := make(map[string]map[liquid.ResourceName]bool) + var hwVersionResources []liquid.ResourceName hwVersionResourceRx := regexp.MustCompile(`^hw_version_(\S+)_(cores|instances|ram)$`) for resourceName := range defaultQuotaClassSet { match := hwVersionResourceRx.FindStringSubmatch(resourceName) @@ -84,41 +109,28 @@ func (l *Logic) Init(ctx context.Context, provider *gophercloud.ProviderClient, } hwVersion, baseResourceName := match[1], liquid.ResourceName(match[2]) - l.hwVersionResources = append(l.hwVersionResources, liquid.ResourceName(resourceName)) + hwVersionResources = append(hwVersionResources, liquid.ResourceName(resourceName)) - if l.hasPooledResource[hwVersion] == nil { - l.hasPooledResource[hwVersion] = make(map[liquid.ResourceName]bool) + if hasPooledResource[hwVersion] == nil { + hasPooledResource[hwVersion] = make(map[liquid.ResourceName]bool) } - l.hasPooledResource[hwVersion][baseResourceName] = true + hasPooledResource[hwVersion][baseResourceName] = true } + l.hasPooledResource.Set(hasPooledResource) + l.hwVersionResources.Set(hwVersionResources) - return FlavorSelection{}.ForeachFlavor(ctx, l.NovaV2, func(f flavors.Flavor) error { + var ignoredFlavorNames []string + err = FlavorSelection{}.ForeachFlavor(ctx, l.NovaV2, func(f flavors.Flavor) error { if IsIronicFlavor(f) { - l.ignoredFlavorNames = append(l.ignoredFlavorNames, f.Name) + ignoredFlavorNames = append(ignoredFlavorNames, f.Name) } return nil }) -} - -func getDefaultQuotaClassSet(ctx context.Context, novaV2 *gophercloud.ServiceClient) (map[string]any, error) { - url := novaV2.ServiceURL("os-quota-class-sets", "default") - var result gophercloud.Result - _, err := novaV2.Get(ctx, url, &result.Body, nil) //nolint:bodyclose if err != nil { - return nil, err - } - - var body struct { - //NOTE: cannot use map[string]int64 here because this object contains the - // field "id": "default" (curse you, untyped JSON) - QuotaClassSet map[string]any `json:"quota_class_set"` + return liquid.ServiceInfo{}, err } - err = result.ExtractInto(&body) - return body.QuotaClassSet, err -} + l.ignoredFlavorNames.Set(ignoredFlavorNames) -// BuildServiceInfo implements the liquidapi.Logic interface. -func (l *Logic) BuildServiceInfo(ctx context.Context) (liquid.ServiceInfo, error) { resources := map[liquid.ResourceName]liquid.ResourceInfo{ "cores": { Unit: liquid.UnitNone, @@ -145,7 +157,7 @@ func (l *Logic) BuildServiceInfo(ctx context.Context) (liquid.ServiceInfo, error }, } - err := FlavorSelection{}.ForeachFlavor(ctx, l.NovaV2, func(f flavors.Flavor) error { + err = FlavorSelection{}.ForeachFlavor(ctx, l.NovaV2, func(f flavors.Flavor) error { if IsIronicFlavor(f) { return nil } @@ -162,7 +174,7 @@ func (l *Logic) BuildServiceInfo(ctx context.Context) (liquid.ServiceInfo, error return liquid.ServiceInfo{}, err } - for _, resourceName := range l.hwVersionResources { + for _, resourceName := range hwVersionResources { unit := liquid.UnitNone if strings.HasSuffix(string(resourceName), "ram") { unit = liquid.UnitMebibytes @@ -194,7 +206,7 @@ func (l *Logic) SetQuota(ctx context.Context, projectUUID string, req liquid.Ser } func (l *Logic) IgnoreFlavor(flavorName string) bool { - return slices.Contains(l.ignoredFlavorNames, flavorName) + return slices.Contains(l.ignoredFlavorNames.Get(), flavorName) } //////////////////////////////////////////////////////////////////////////////// diff --git a/internal/liquids/nova/usage.go b/internal/liquids/nova/usage.go index 17102b2b..3ebc3851 100644 --- a/internal/liquids/nova/usage.go +++ b/internal/liquids/nova/usage.go @@ -49,7 +49,7 @@ func (l *Logic) pooledResourceName(hwVersion string, base liquid.ResourceName) l } // if we saw a "quota:hw_version" extra spec on the instance's flavor, use the appropriate resource if it exists - if l.hasPooledResource[hwVersion][base] { + if l.hasPooledResource.Get()[hwVersion][base] { return liquid.ResourceName(fmt.Sprintf("hw_version_%s_%s", hwVersion, base)) } return base @@ -130,19 +130,19 @@ func (l *Logic) ScanUsage(ctx context.Context, projectUUID string, req liquid.Se } } for hwVersion, limits := range limitsData.Limits.AbsolutePerHWVersion { - if l.hasPooledResource[hwVersion]["cores"] { + if l.hasPooledResource.Get()[hwVersion]["cores"] { resources[l.pooledResourceName(hwVersion, "cores")] = &liquid.ResourceUsageReport{ Quota: &limits.MaxTotalCores, PerAZ: liquid.AZResourceUsageReport{Usage: limits.TotalCoresUsed}.PrepareForBreakdownInto(req.AllAZs), } } - if l.hasPooledResource[hwVersion]["instances"] { + if l.hasPooledResource.Get()[hwVersion]["instances"] { resources[l.pooledResourceName(hwVersion, "instances")] = &liquid.ResourceUsageReport{ Quota: &limits.MaxTotalInstances, PerAZ: liquid.AZResourceUsageReport{Usage: limits.TotalInstancesUsed}.PrepareForBreakdownInto(req.AllAZs), } } - if l.hasPooledResource[hwVersion]["ram"] { + if l.hasPooledResource.Get()[hwVersion]["ram"] { resources[l.pooledResourceName(hwVersion, "ram")] = &liquid.ResourceUsageReport{ Quota: &limits.MaxTotalRAMSize, PerAZ: liquid.AZResourceUsageReport{Usage: limits.TotalRAMUsed}.PrepareForBreakdownInto(req.AllAZs),