Skip to content

Commit

Permalink
Add liquid nova management for computated states
Browse files Browse the repository at this point in the history
  • Loading branch information
Varsius committed Dec 17, 2024
1 parent 9d47f14 commit a242be7
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 36 deletions.
76 changes: 44 additions & 32 deletions internal/liquids/nova/liquid.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.
Expand All @@ -66,16 +68,39 @@ 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.
//
// 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)
Expand All @@ -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,
Expand All @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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)
}

////////////////////////////////////////////////////////////////////////////////
Expand Down
8 changes: 4 additions & 4 deletions internal/liquids/nova/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down

0 comments on commit a242be7

Please sign in to comment.