Skip to content

Commit

Permalink
plugins/nova: split server_group_members usage measurement into a sep…
Browse files Browse the repository at this point in the history
…arate file
  • Loading branch information
majewsky committed Nov 3, 2023
1 parent f84f8d1 commit e810ef8
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 73 deletions.
78 changes: 5 additions & 73 deletions internal/plugins/nova.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,13 @@ import (
"math/big"
"sort"
"strconv"
"time"

"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/extensions/servergroups"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
"github.com/gophercloud/gophercloud/pagination"
"github.com/prometheus/client_golang/prometheus"
"github.com/sapcc/go-api-declarations/limes"
Expand All @@ -45,10 +42,6 @@ import (
"github.com/sapcc/limes/internal/core"
)

// use a name that's unique to github.com/gophercloud/gophercloud/openstack/imageservice/v2/images
// to ensure that goimports does not mistakenly replace it by .../compute/v2/images
var _ images.ImageVisibility

type novaPlugin struct {
//configuration
BigVMMinMemoryMiB uint64 `yaml:"bigvm_min_memory"`
Expand All @@ -61,14 +54,10 @@ type novaPlugin struct {
//computed state
resources []limesresources.ResourceInfo `yaml:"-"`
ftt novaFlavorTranslationTable `yaml:"-"`
//caches
serverGroups struct {
lastScrapeTime *time.Time
members map[string]uint64 // per project
} `yaml:"-"`
//connections
NovaV2 *gophercloud.ServiceClient `yaml:"-"`
OSTypeProber *novaOSTypeProber `yaml:"-"`
NovaV2 *gophercloud.ServiceClient `yaml:"-"`
OSTypeProber *novaOSTypeProber `yaml:"-"`
ServerGroupProber *novaServerGroupProber `yaml:"-"`
}

type novaSerializedMetrics struct {
Expand Down Expand Up @@ -122,6 +111,7 @@ func (p *novaPlugin) Init(provider *gophercloud.ProviderClient, eo gophercloud.E
return err
}
p.OSTypeProber = newNovaOSTypeProber(p.NovaV2, cinderV3, glanceV2)
p.ServerGroupProber = newNovaServerGroupProber(p.NovaV2)

//find per-flavor instance resources
flavorNames, err := p.ftt.ListFlavorsWithSeparateInstanceQuota(p.NovaV2)
Expand Down Expand Up @@ -253,14 +243,10 @@ func (p *novaPlugin) Scrape(project core.KeystoneProject) (result map[string]cor

var totalServerGroupMembersUsed uint64
if limitsData.Limits.Absolute.TotalServerGroupsUsed > 0 {
err := p.getServerGroups()
totalServerGroupMembersUsed, err = p.ServerGroupProber.GetMemberUsageForProject(project.UUID)
if err != nil {
return nil, nil, err
}

if v, ok := p.serverGroups.members[project.UUID]; ok {
totalServerGroupMembersUsed = v
}
}

resultPtr := map[string]*core.ResourceData{
Expand Down Expand Up @@ -539,57 +525,3 @@ type novaQuotaUpdateOpts map[string]uint64
func (opts novaQuotaUpdateOpts) ToComputeQuotaUpdateMap() (map[string]any, error) {
return map[string]any{"quota_set": opts}, nil
}

func (p *novaPlugin) getServerGroups() error {
if p.serverGroups.lastScrapeTime != nil {
if time.Since(*p.serverGroups.lastScrapeTime) < 10*time.Minute {
return nil // no need to refresh cache
}
}

//When paginating through the list of server groups, perform steps slightly
//smaller than the actual page size, in order to correctly detect insertions
//and deletions that may cause list entries to shift around while we iterate
//over them.
const pageSize int = 500
stepSize := pageSize * 9 / 10
var currentOffset int
serverGroupSeen := make(map[string]bool)
membersPerProject := make(map[string]uint64)
for {
groups, err := p.getServerGroupsPage(pageSize, currentOffset)
if err != nil {
return err
}
for _, sg := range groups {
if !serverGroupSeen[sg.ID] {
membersPerProject[sg.ProjectID] += uint64(len(sg.Members))
serverGroupSeen[sg.ID] = true
}
}

//abort after the last page
if len(groups) < pageSize {
break
}
currentOffset += stepSize
}

p.serverGroups.members = membersPerProject
t := time.Now()
p.serverGroups.lastScrapeTime = &t

return nil
}

func (p *novaPlugin) getServerGroupsPage(limit, offset int) ([]servergroups.ServerGroup, error) {
allPages, err := servergroups.List(p.NovaV2, servergroups.ListOpts{AllProjects: true, Limit: limit, Offset: offset}).AllPages()
if err != nil {
return nil, err
}
allServerGroups, err := servergroups.ExtractServerGroups(allPages)
if err != nil {
return nil, err
}
return allServerGroups, nil
}
4 changes: 4 additions & 0 deletions internal/plugins/nova_ostype_prober.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ import (
"github.com/sapcc/limes/internal/util"
)

// use a name that's unique to github.com/gophercloud/gophercloud/openstack/imageservice/v2/images
// to ensure that goimports does not mistakenly replace it by .../compute/v2/images
var _ images.ImageVisibility

type novaOSTypeProber struct {
//caches
CacheByImage map[string]string //for instances booted from images
Expand Down
99 changes: 99 additions & 0 deletions internal/plugins/nova_server_group_prober.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*******************************************************************************
*
* Copyright 2020-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 (
"time"

"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups"
)

// The reason why this type exists at all is that:
// a) Nova does not report the usage for server_group_members directly, and
// b) we cannot ask for server groups in a specific foreign project.
//
// We can only list *all* server groups globally at once. Since this is very
// expensive, we only do it once every few minutes.
type novaServerGroupProber struct {
NovaV2 *gophercloud.ServiceClient
UsageByProjectID map[string]uint64
LastScrapeTime time.Time // only initialized if .usageByProjectID != nil
}

func newNovaServerGroupProber(novaV2 *gophercloud.ServiceClient) *novaServerGroupProber {
return &novaServerGroupProber{NovaV2: novaV2}
}

func (p *novaServerGroupProber) GetMemberUsageForProject(projectID string) (uint64, error) {
//refresh cache if not initialized or outdated
var err error
if p.UsageByProjectID == nil || time.Since(p.LastScrapeTime) > 10*time.Minute {
err = p.fillCache()
}

return p.UsageByProjectID[projectID], err
}

func (p *novaServerGroupProber) fillCache() error {
//When paginating through the list of server groups, perform steps slightly
//smaller than the actual page size, in order to correctly detect insertions
//and deletions that may cause list entries to shift around while we iterate
//over them.
const pageSize int = 500
stepSize := pageSize * 9 / 10
var currentOffset int
serverGroupSeen := make(map[string]bool)
usageByProjectID := make(map[string]uint64)
for {
groups, err := p.getServerGroupsPage(pageSize, currentOffset)
if err != nil {
return err
}
for _, sg := range groups {
if !serverGroupSeen[sg.ID] {
usageByProjectID[sg.ProjectID] += uint64(len(sg.Members))
serverGroupSeen[sg.ID] = true
}
}

//abort after the last page
if len(groups) < pageSize {
break
}
currentOffset += stepSize
}

p.UsageByProjectID = usageByProjectID
p.LastScrapeTime = time.Now()
return nil
}

func (p *novaServerGroupProber) getServerGroupsPage(limit, offset int) ([]servergroups.ServerGroup, error) {
allPages, err := servergroups.List(p.NovaV2, servergroups.ListOpts{AllProjects: true, Limit: limit, Offset: offset}).AllPages()
if err != nil {
return nil, err
}
allServerGroups, err := servergroups.ExtractServerGroups(allPages)
if err != nil {
return nil, err
}
return allServerGroups, nil
}

0 comments on commit e810ef8

Please sign in to comment.