Skip to content

Commit

Permalink
plugins/nova: split subresource rendering into a separate file
Browse files Browse the repository at this point in the history
  • Loading branch information
majewsky committed Nov 6, 2023
1 parent 2501dba commit 58ded66
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 127 deletions.
147 changes: 20 additions & 127 deletions internal/plugins/nova.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,18 @@ package plugins

import (
"encoding/json"
"fmt"
"math/big"
"sort"
"strconv"

"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/gophercloud/gophercloud/pagination"
"github.com/prometheus/client_golang/prometheus"
"github.com/sapcc/go-api-declarations/limes"
limesrates "github.com/sapcc/go-api-declarations/limes/rates"
limesresources "github.com/sapcc/go-api-declarations/limes/resources"
"github.com/sapcc/go-bits/logg"
"github.com/sapcc/go-bits/regexpext"

"github.com/sapcc/limes/internal/core"
Expand Down Expand Up @@ -312,9 +308,9 @@ func (p *novaPlugin) Scrape(project core.KeystoneProject) (result map[string]cor
var metrics novaSerializedMetrics

if p.scrapeInstances {
listOpts := novaServerListOpts{
AllTenants: true,
TenantID: project.UUID,
allSubresources, err := p.buildInstanceSubresources(project)
if err != nil {
return nil, nil, fmt.Errorf("while collecting instance info: %w", err)
}

metrics.InstanceCountsByHypervisor = map[string]uint64{
Expand All @@ -323,91 +319,27 @@ func (p *novaPlugin) Scrape(project core.KeystoneProject) (result map[string]cor
"unknown": 0,
}

err := servers.List(p.NovaV2, listOpts).EachPage(func(page pagination.Page) (bool, error) {
var instances []struct {
servers.Server
availabilityzones.ServerAvailabilityZoneExt
}
err := servers.ExtractServersInto(page, &instances)
if err != nil {
return false, err
for _, res := range allSubresources {
limesResourceName := p.ftt.LimesResourceNameForFlavor(res.FlavorName)
if res.HypervisorType != "" {
metrics.InstanceCountsByHypervisor[res.HypervisorType]++
}

for _, instance := range instances {
subResource := map[string]any{
"id": instance.ID,
"name": instance.Name,
"status": instance.Status,
"availability_zone": instance.AvailabilityZone,
"metadata": instance.Metadata,
"tags": derefSlicePtrOrEmpty(instance.Tags),
}

var flavorName string
flavor, err := unpackFlavorData(instance.Flavor)
if err != nil {
logg.Error("error while trying to parse flavor data for instance %s: %s", instance.ID, err.Error())
} else {
flavorName = flavor.OriginalName
subResource["flavor"] = flavor.OriginalName
subResource["vcpu"] = flavor.VCPUs
subResource["ram"] = limes.ValueWithUnit{
Value: flavor.MemoryMiB,
Unit: limes.UnitMebibytes,
}
subResource["disk"] = limes.ValueWithUnit{
Value: flavor.DiskGiB,
Unit: limes.UnitGibibytes,
}

if videoRAMStr, exists := flavor.ExtraSpecs["hw_video:ram_max_mb"]; exists {
videoRAMVal, err := strconv.ParseUint(videoRAMStr, 10, 64)
if err == nil {
subResource["video_ram"] = limes.ValueWithUnit{
Value: videoRAMVal,
Unit: limes.UnitMebibytes,
}
}
}

if len(p.HypervisorTypeRules) > 0 {
hypervisorType := p.HypervisorTypeRules.Evaluate(flavor)
subResource["hypervisor"] = hypervisorType
metrics.InstanceCountsByHypervisor[hypervisorType]++
}

if p.BigVMMinMemoryMiB > 0 {
class := "regular"
if flavor.MemoryMiB >= p.BigVMMinMemoryMiB {
class = "bigvm"
}
subResource["class"] = class

//do not count baremetal instances into `{cores,instances,ram}_{bigvm,regular}`
if _, exists := resultPtr[p.ftt.LimesResourceNameForFlavor(flavorName)]; !exists {
resultPtr["cores_"+class].UsageData[limes.AvailabilityZoneAny].Usage += flavor.VCPUs
resultPtr["instances_"+class].UsageData[limes.AvailabilityZoneAny].Usage++
resultPtr["ram_"+class].UsageData[limes.AvailabilityZoneAny].Usage += flavor.MemoryMiB
}
}
}

if instance.Image == nil {
subResource["os_type"] = p.OSTypeProber.GetFromBootVolume(instance.ID)
} else {
subResource["os_type"] = p.OSTypeProber.GetFromImage(instance.Image["id"])
if p.BigVMMinMemoryMiB > 0 {
//do not count baremetal instances into `{cores,instances,ram}_{bigvm,regular}`
if _, exists := resultPtr[limesResourceName]; !exists {
class := res.Class
resultPtr["cores_"+class].UsageData[limes.AvailabilityZoneAny].Usage += res.VCPUs
resultPtr["instances_"+class].UsageData[limes.AvailabilityZoneAny].Usage++
resultPtr["ram_"+class].UsageData[limes.AvailabilityZoneAny].Usage += res.MemoryMiB.Value
}
}

resource, exists := resultPtr[p.ftt.LimesResourceNameForFlavor(flavorName)]
if !exists {
resource = resultPtr["instances"]
}
resource.UsageData[limes.AvailabilityZoneAny].Subresources = append(resource.UsageData[limes.AvailabilityZoneAny].Subresources, subResource)
resource, exists := resultPtr[limesResourceName]
if !exists {
resource = resultPtr["instances"]
}
return true, nil
})
if err != nil {
return nil, nil, err
resource.UsageData[limes.AvailabilityZoneAny].Subresources = append(resource.UsageData[limes.AvailabilityZoneAny].Subresources, res)
}
}

Expand All @@ -420,13 +352,6 @@ func (p *novaPlugin) Scrape(project core.KeystoneProject) (result map[string]cor
return result2, serializedMetrics, err
}

func derefSlicePtrOrEmpty(val *[]string) []string {
if val == nil {
return nil
}
return *val
}

// IsQuotaAcceptableForProject implements the core.QuotaPlugin interface.
func (p *novaPlugin) IsQuotaAcceptableForProject(project core.KeystoneProject, fullQuotas map[string]map[string]uint64, allServiceInfos []limes.ServiceInfo) error {
//not required for this plugin
Expand Down Expand Up @@ -488,38 +413,6 @@ func (p *novaPlugin) CollectMetrics(ch chan<- prometheus.Metric, project core.Ke
return nil
}

// Information about a flavor, as it appears in GET /servers/:id in the "flavor"
// key with newer Nova microversions.
type novaFlavorInfo struct {
DiskGiB uint64 `json:"disk"`
EphemeralGiB uint64 `json:"ephemeral"`
ExtraSpecs map[string]string `json:"extra_specs"`
OriginalName string `json:"original_name"`
MemoryMiB uint64 `json:"ram"`
SwapMiB uint64 `json:"swap"`
VCPUs uint64 `json:"vcpus"`
}

func unpackFlavorData(input map[string]any) (novaFlavorInfo, error) {
buf, err := json.Marshal(input)
if err != nil {
return novaFlavorInfo{}, err
}
var result novaFlavorInfo
err = json.Unmarshal(buf, &result)
return result, err
}

type novaServerListOpts struct {
AllTenants bool `q:"all_tenants"`
TenantID string `q:"tenant_id"`
}

func (opts novaServerListOpts) ToServerListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
return q.String(), err
}

type novaQuotaUpdateOpts map[string]uint64

func (opts novaQuotaUpdateOpts) ToComputeQuotaUpdateMap() (map[string]any, error) {
Expand Down
176 changes: 176 additions & 0 deletions internal/plugins/nova_subresources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*******************************************************************************
*
* Copyright 2017-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 plugins

import (
"encoding/json"
"fmt"
"strconv"

"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/gophercloud/gophercloud/pagination"
"github.com/sapcc/go-api-declarations/limes"

Check failure on line 31 in internal/plugins/nova_subresources.go

View workflow job for this annotation

GitHub Actions / Build & Lint

File is not `goimports`-ed with -local github.com/sapcc/limes (goimports)
"github.com/sapcc/limes/internal/core"
)

// A compute instance as shown on the Nova API.
// This includes some API extensions that we need.
type novaInstance struct {
servers.Server
availabilityzones.ServerAvailabilityZoneExt
}

// A compute instance as shown in our compute/instances subresources.
type novaInstanceSubresource struct {
// instance identity
ID string `json:"id"`
Name string `json:"name"`
// base metadata
Status string `json:"status"`
Metadata map[string]string `json:"metadata"`
Tags []string `json:"tags"`
// placement information
AZ string `json:"availability_zone"`
HypervisorType string `json:"hypervisor,omitempty"`
Class string `json:"class,omitempty"` //either "bigvm" or "regular", see below
// information from flavor
FlavorName string `json:"flavor"`
VCPUs uint64 `json:"vcpu"`
MemoryMiB limes.ValueWithUnit `json:"ram"`
DiskGiB limes.ValueWithUnit `json:"disk"`
VideoMemoryMiB *limes.ValueWithUnit `json:"video_ram,omitempty"`
// information from image
OSType string `json:"os_type"`
}

// Information about a flavor, as it appears in Nova's GET /servers/:id
// in the "flavor" key with newer Nova microversions.
type novaFlavorInfo struct {
DiskGiB uint64 `json:"disk"`
EphemeralGiB uint64 `json:"ephemeral"`
ExtraSpecs map[string]string `json:"extra_specs"`
OriginalName string `json:"original_name"`
MemoryMiB uint64 `json:"ram"`
SwapMiB uint64 `json:"swap"`
VCPUs uint64 `json:"vcpus"`
}

func (p *novaPlugin) buildInstanceSubresource(instance novaInstance) (res novaInstanceSubresource, err error) {
//copy base attributes
res.ID = instance.ID
res.Name = instance.Name
res.Status = instance.Status
res.AZ = instance.AvailabilityZone
res.Metadata = instance.Metadata
if instance.Tags != nil {
res.Tags = *instance.Tags
}

//flavor data is given to us as a map[string]any, but we want something more structured
buf, err := json.Marshal(instance.Flavor)
if err != nil {
return res, fmt.Errorf("could not reserialize flavor data for instance %s: %w", instance.ID, err)
}
var flavorInfo novaFlavorInfo
err = json.Unmarshal(buf, &flavorInfo)
if err != nil {
return res, fmt.Errorf("could not parse flavor data for instance %s: %w", instance.ID, err)
}

//copy attributes from flavor data
res.FlavorName = flavorInfo.OriginalName
res.VCPUs = flavorInfo.VCPUs
res.MemoryMiB = limes.ValueWithUnit{
Value: flavorInfo.MemoryMiB,
Unit: limes.UnitMebibytes,
}
res.DiskGiB = limes.ValueWithUnit{
Value: flavorInfo.DiskGiB,
Unit: limes.UnitGibibytes,
}
if videoRAMStr, exists := flavorInfo.ExtraSpecs["hw_video:ram_max_mb"]; exists {
videoRAMVal, err := strconv.ParseUint(videoRAMStr, 10, 64)
if err == nil {
res.VideoMemoryMiB = &limes.ValueWithUnit{
Value: videoRAMVal,
Unit: limes.UnitMebibytes,
}
}
}

//calculate classifications based on flavor data
if len(p.HypervisorTypeRules) > 0 {
res.HypervisorType = p.HypervisorTypeRules.Evaluate(flavorInfo)
}
if p.BigVMMinMemoryMiB > 0 {
if flavorInfo.MemoryMiB >= p.BigVMMinMemoryMiB {
res.Class = "bigvm"
} else {
res.Class = "regular"
}
}

//calculate classifications based on image data
if instance.Image == nil {
res.OSType = p.OSTypeProber.GetFromBootVolume(instance.ID)
} else {
res.OSType = p.OSTypeProber.GetFromImage(instance.Image["id"])
}

return res, nil
}

func (p *novaPlugin) buildInstanceSubresources(project core.KeystoneProject) ([]novaInstanceSubresource, error) {
opts := novaServerListOpts{
AllTenants: true,
TenantID: project.UUID,
}

var result []novaInstanceSubresource
err := servers.List(p.NovaV2, opts).EachPage(func(page pagination.Page) (bool, error) {
var instances []novaInstance
err := servers.ExtractServersInto(page, &instances)
if err != nil {
return false, err
}

for _, instance := range instances {
res, err := p.buildInstanceSubresource(instance)
if err != nil {
return false, err
}
result = append(result, res)
}
return true, nil
})
return result, err
}

type novaServerListOpts struct {
AllTenants bool `q:"all_tenants"`
TenantID string `q:"tenant_id"`
}

func (opts novaServerListOpts) ToServerListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
return q.String(), err
}

0 comments on commit 58ded66

Please sign in to comment.