Skip to content

Commit

Permalink
add sapcc-ironic capacity plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
majewsky committed Feb 5, 2018
1 parent 2891520 commit bfcf602
Show file tree
Hide file tree
Showing 5 changed files with 399 additions and 44 deletions.
10 changes: 10 additions & 0 deletions docs/operators/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,16 @@ capacitors:
capacity: min(swift_cluster_storage_capacity_bytes < inf) / 3
```

## `sapcc-ironic`

```yaml
capacitors:
- id: sapcc-ironic
```

This capacity plugin reports capacity for the special `compute/instances_<flavorname>` resources that exist on SAP
Converged Cloud ([see above](#compute-nova-v2)). For each such flavor, it counts the number of Ironic nodes whose RAM
size, disk size, number of cores, and capabilities match those in the flavor.

[yaml]: http://yaml.org/
[pq-uri]: https://www.postgresql.org/docs/9.6/static/libpq-connect.html#LIBPQ-CONNSTRING
Expand Down
25 changes: 8 additions & 17 deletions pkg/plugins/capacity_nova.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@ type capacityNovaPlugin struct {
cfg limes.CapacitorConfiguration
}

type extraSpecs struct {
ExtraSpecs map[string]string `json:"extra_specs"`
}

func init() {
limes.RegisterCapacityPlugin(func(c limes.CapacitorConfiguration) limes.CapacityPlugin {
return &capacityNovaPlugin{c}
Expand Down Expand Up @@ -117,14 +113,14 @@ func (p *capacityNovaPlugin) Scrape(provider *gophercloud.ProviderClient) (map[s

//necessary to be able to ignore huge baremetal flavors
//consider only flavors as defined in extra specs
var extraSpecs = map[string]string{}
var extraSpecs map[string]string
if p.cfg.Nova.ExtraSpecs != nil {
extraSpecs = p.cfg.Nova.ExtraSpecs
}

matches := true
for key, value := range extraSpecs {
if value != extras.ExtraSpecs[key] {
if value != extras[key] {
matches = false
break
}
Expand Down Expand Up @@ -157,13 +153,6 @@ func (p *capacityNovaPlugin) Scrape(provider *gophercloud.ProviderClient) (map[s
vcpuOvercommitFactor = *p.cfg.Nova.VCPUOvercommitFactor
}

//returns something like
//"volumev2": {
// "cores": total_vcpus,
// "instances": min(10000 per Availability Zone, local_gb/max(flavor size)),
// "ram": total_memory_mb,
//}

capacity := map[string]map[string]uint64{
"compute": {
"cores": uint64(hypervisorData.HypervisorStatistics.Vcpus) * vcpuOvercommitFactor,
Expand All @@ -184,20 +173,22 @@ func (p *capacityNovaPlugin) Scrape(provider *gophercloud.ProviderClient) (map[s
//result contains
//{ "vmware:hv_enabled" : 'True' }
//which identifies a VM flavor
func getFlavorExtras(client *gophercloud.ServiceClient, flavorUUID string) (*extraSpecs, error) {
func getFlavorExtras(client *gophercloud.ServiceClient, flavorUUID string) (map[string]string, error) {
var result gophercloud.Result
var extraSpecsData extraSpecs
var extraSpecs struct {
ExtraSpecs map[string]string `json:"extra_specs"`
}

url := client.ServiceURL("flavors", flavorUUID, "os-extra_specs")
_, err := client.Get(url, &result.Body, nil)
if err != nil {
return nil, err
}

err = result.ExtractInto(&extraSpecsData)
err = result.ExtractInto(&extraSpecs)
if err != nil {
return nil, err
}

return &extraSpecsData, nil
return extraSpecs.ExtraSpecs, nil
}
237 changes: 237 additions & 0 deletions pkg/plugins/capacity_sapcc_ironic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/*******************************************************************************
*
* Copyright 2018 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 (
"strings"

"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack"
flavorsmodule "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors"
"github.com/gophercloud/gophercloud/pagination"
"github.com/sapcc/limes/pkg/limes"
"github.com/sapcc/limes/pkg/util"
)

type capacitySapccIronicPlugin struct {
cfg limes.CapacitorConfiguration
}

func init() {
limes.RegisterCapacityPlugin(func(c limes.CapacitorConfiguration) limes.CapacityPlugin {
return &capacitySapccIronicPlugin{c}
})
}

func (p *capacitySapccIronicPlugin) NovaClient(provider *gophercloud.ProviderClient) (*gophercloud.ServiceClient, error) {
return openstack.NewComputeV2(provider,
gophercloud.EndpointOpts{Availability: gophercloud.AvailabilityPublic},
)
}

//ID implements the limes.CapacityPlugin interface.
func (p *capacitySapccIronicPlugin) ID() string {
return "sapcc-ironic"
}

type ironicFlavorInfo struct {
ID string
Name string
Cores uint64
MemoryMiB uint64
DiskGiB uint64
Capabilities map[string]string
}

//Scrape implements the limes.CapacityPlugin interface.
func (p *capacitySapccIronicPlugin) Scrape(provider *gophercloud.ProviderClient) (map[string]map[string]uint64, error) {
//collect info about flavors with separate instance quota
novaClient, err := p.NovaClient(provider)
if err != nil {
return nil, err
}
flavors, err := collectIronicFlavorInfo(novaClient)
if err != nil {
return nil, err
}

//we are going to report capacity for all per-flavor instance quotas
result := make(map[string]uint64)
for _, flavor := range flavors {
result["instances_"+flavor.Name] = 0
}

//count Ironic nodes
ironicClient, err := newIronicClient(provider)
if err != nil {
return nil, err
}
nodes, err := ironicClient.GetNodes()
if err != nil {
return nil, err
}

unmatchedCounter := 0
for _, node := range nodes {
matched := false
for _, flavor := range flavors {
if node.Matches(flavor) {
util.LogDebug("Ironic node %q (%s) matches flavor %s", node.Name, node.ID, flavor.Name)
result["instances_"+flavor.Name]++
matched = true
break
}
}
if !matched {
util.LogDebug("Ironic node %q (%s) does not match any flavor", node.Name, node.ID)
unmatchedCounter++
}
}

if unmatchedCounter > 0 {
util.LogError("%d Ironic nodes do not match any baremetal flavors", unmatchedCounter)
}

return map[string]map[string]uint64{"compute": result}, nil
}

//NOTE: This method is shared with the Nova quota plugin.
func listPerFlavorInstanceResources(novaClient *gophercloud.ServiceClient) ([]string, error) {
//look at quota class "default" to determine which quotas exist
url := novaClient.ServiceURL("os-quota-class-sets", "default")
var result gophercloud.Result
_, err := novaClient.Get(url, &result.Body, nil)
if err != nil {
return nil, err
}

//At SAP Converged Cloud, we use per-flavor instance quotas for baremetal
//(Ironic) flavors, to control precisely how many baremetal machines can be
//used by each domain/project. Each such quota has the resource name
//"instances_${FLAVOR_NAME}".
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]interface{} `json:"quota_class_set"`
}
err = result.ExtractInto(&body)
if err != nil {
return nil, err
}

var resources []string
for key := range body.QuotaClassSet {
if strings.HasPrefix(key, "instances_") {
resources = append(resources, key)
}
}

return resources, nil
}

func collectIronicFlavorInfo(novaClient *gophercloud.ServiceClient) ([]ironicFlavorInfo, error) {
//which flavors have separate instance quota?
resources, err := listPerFlavorInstanceResources(novaClient)
if err != nil {
return nil, err
}
isRelevantFlavorName := make(map[string]bool, len(resources))
for _, resourceName := range resources {
flavorName := strings.TrimPrefix(resourceName, "instances_")
isRelevantFlavorName[flavorName] = true
}

//collect basic attributes for flavors
var result []ironicFlavorInfo
err = flavorsmodule.ListDetail(novaClient, nil).EachPage(func(page pagination.Page) (bool, error) {
flavors, err := flavorsmodule.ExtractFlavors(page)
if err != nil {
return false, err
}

for _, flavor := range flavors {
if isRelevantFlavorName[flavor.Name] {
result = append(result, ironicFlavorInfo{
ID: flavor.ID,
Name: flavor.Name,
Cores: uint64(flavor.VCPUs),
MemoryMiB: uint64(flavor.RAM),
DiskGiB: uint64(flavor.Disk),
Capabilities: make(map[string]string),
})
}
}
return true, nil
})
if err != nil {
return nil, err
}

//retrieve extra specs - the ones in the "capabilities" namespace are
//relevant for Ironic node selection
for _, flavor := range result {
extraSpecs, err := getFlavorExtras(novaClient, flavor.ID)
if err != nil {
return nil, err
}
for key, value := range extraSpecs {
if strings.HasPrefix(key, "capabilities:") {
capability := strings.TrimPrefix(key, "capabilities:")
flavor.Capabilities[capability] = value
}
}
}
return result, nil
}

func (n ironicNode) Matches(f ironicFlavorInfo) bool {
if uint64(n.Properties.Cores) != f.Cores {
util.LogDebug("core mismatch: %d != %d", n.Properties.Cores, f.Cores)
return false
}
if uint64(n.Properties.MemoryMiB) != f.MemoryMiB {
util.LogDebug("memory mismatch: %d != %d", n.Properties.MemoryMiB, f.MemoryMiB)
return false
}
if uint64(n.Properties.DiskGiB) != f.DiskGiB {
util.LogDebug("disk mismatch: %d != %d", n.Properties.DiskGiB, f.DiskGiB)
return false
}

nodeCaps := make(map[string]string)
if n.Properties.CPUArchitecture != "" {
nodeCaps["cpu_arch"] = n.Properties.CPUArchitecture
}
for _, field := range strings.Split(n.Properties.Capabilities, ",") {
fields := strings.SplitN(field, ":", 2)
if len(fields) == 2 {
nodeCaps[fields[0]] = fields[1]
}
}

for key, flavorValue := range f.Capabilities {
nodeValue, exists := nodeCaps[key]
if !exists || nodeValue != flavorValue {
util.LogDebug("capability %s mismatch: %q != %q", key, nodeValue, flavorValue)
return false
}
}
return true
}
Loading

0 comments on commit bfcf602

Please sign in to comment.