Skip to content

Commit

Permalink
feat(hatchery): openstack max CPUs count and flavor weigth (#5190)
Browse files Browse the repository at this point in the history
* feat(hatchery): openstask max CPUs count and flavor weigth

* feat: sort and filter models enabled, check max CPUs count

* feat: impl CountSmallerFlavorToKeep check

* test: unit test func
  • Loading branch information
richardlt authored Jun 10, 2020
1 parent e8d6edc commit 80140d7
Show file tree
Hide file tree
Showing 9 changed files with 415 additions and 65 deletions.
28 changes: 22 additions & 6 deletions engine/hatchery/openstack/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"sync"
"time"

"github.com/gophercloud/gophercloud/openstack/compute/v2/flavors"
"github.com/gophercloud/gophercloud/openstack/compute/v2/images"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"

Expand All @@ -20,17 +21,32 @@ func (h *HatcheryOpenstack) imageID(ctx context.Context, img string) (string, er
return i.ID, nil
}
}
return "", fmt.Errorf("imageID> image '%s' not found", img)
return "", sdk.WithStack(fmt.Errorf("image '%s' not found", img))
}

// Find flavor ID from flavor name
func (h *HatcheryOpenstack) flavorID(flavor string) (string, error) {
for _, f := range h.flavors {
if f.Name == flavor {
return f.ID, nil
func (h *HatcheryOpenstack) flavor(flavor string) (flavors.Flavor, error) {
for i := range h.flavors {
if h.flavors[i].Name == flavor {
return h.flavors[i], nil
}
}
return "", fmt.Errorf("flavorID> flavor '%s' not found", flavor)
return flavors.Flavor{}, sdk.WithStack(fmt.Errorf("flavor '%s' not found", flavor))
}

// Find flavor ID from flavor name
func (h *HatcheryOpenstack) getSmallerFlavorThan(flavor flavors.Flavor) flavors.Flavor {
var smaller *flavors.Flavor
for i := range h.flavors {
// If the flavor is not the given one and need less CPUs its
if h.flavors[i].ID != flavor.ID && h.flavors[i].VCPUs < flavor.VCPUs && (smaller == nil || smaller.VCPUs < h.flavors[i].VCPUs) {
smaller = &h.flavors[i]
}
}
if smaller == nil {
return flavor
}
return *smaller
}

//This a embedded cache for images list
Expand Down
29 changes: 28 additions & 1 deletion engine/hatchery/openstack/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,41 @@ func (h *HatcheryOpenstack) initFlavors() error {
if err != nil {
return sdk.WithStack(fmt.Errorf("initFlavors> error on flavors.ListDetail: %v", err))
}

lflavors, err := flavors.ExtractFlavors(all)
if err != nil {
return sdk.WithStack(fmt.Errorf("initFlavors> error on flavors.ExtractFlavors: %v", err))
}
h.flavors = lflavors

h.flavors = h.filterAllowedFlavors(lflavors)

return nil
}

func (h HatcheryOpenstack) filterAllowedFlavors(allFlavors []flavors.Flavor) []flavors.Flavor {
// If allowed flavors are given in configuration we should check that given flavor is part of the list.
if len(h.Config.AllowedFlavors) == 0 {
return allFlavors
}

filteredFlavors := make([]flavors.Flavor, 0, len(allFlavors))
for i := range allFlavors {
var allowed bool
for j := range h.Config.AllowedFlavors {
if h.Config.AllowedFlavors[j] == allFlavors[i].Name {
allowed = true
break
}
}
if !allowed {
log.Debug("initFlavors> flavor '%s' is not allowed", allFlavors[i].Name)
continue
}
filteredFlavors = append(filteredFlavors, allFlavors[i])
}
return filteredFlavors
}

func (h *HatcheryOpenstack) initNetworks() error {
all, err := tenantnetworks.List(h.openstackClient).AllPages()
if err != nil {
Expand Down
41 changes: 41 additions & 0 deletions engine/hatchery/openstack/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package openstack

import (
"testing"

"github.com/gophercloud/gophercloud/openstack/compute/v2/flavors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/ovh/cds/sdk/log"
)

func TestHatcheryOpenstack_initFlavors(t *testing.T) {
log.SetLogger(t)

h := &HatcheryOpenstack{}

allFlavors := []flavors.Flavor{
{Name: "b2-7", VCPUs: 2},
{Name: "b2-15", VCPUs: 4},
{Name: "b2-30", VCPUs: 8},
{Name: "b2-60", VCPUs: 16},
{Name: "b2-120", VCPUs: 32},
}

filteredFlavors := h.filterAllowedFlavors(allFlavors)
require.Len(t, filteredFlavors, 5, "no filter as allowed flavor list is empty in config")

h.Config.AllowedFlavors = []string{"b2-15", "b2-60"}

filteredFlavors = h.filterAllowedFlavors(allFlavors)
require.Len(t, filteredFlavors, 2)
assert.Equal(t, "b2-15", filteredFlavors[0].Name)
assert.Equal(t, "b2-60", filteredFlavors[1].Name)

h.Config.AllowedFlavors = []string{"s1-4", "b2-15"}

filteredFlavors = h.filterAllowedFlavors(allFlavors)
require.Len(t, filteredFlavors, 1)
assert.Equal(t, "b2-15", filteredFlavors[0].Name)
}
39 changes: 29 additions & 10 deletions engine/hatchery/openstack/openstack.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package openstack
import (
"context"
"fmt"
"sort"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -154,7 +155,34 @@ func (*HatcheryOpenstack) ModelType() string {

// WorkerModelsEnabled returns Worker model enabled.
func (h *HatcheryOpenstack) WorkerModelsEnabled() ([]sdk.Model, error) {
return h.CDSClient().WorkerModelEnabledList()
allModels, err := h.CDSClient().WorkerModelEnabledList()
if err != nil {
return nil, err
}

filteredModels := make([]sdk.Model, 0, len(allModels))
for i := range allModels {
if allModels[i].Type != sdk.Openstack {
continue
}

// Required flavor should be available on target OpenStack project
if _, err := h.flavor(allModels[i].ModelVirtualMachine.Flavor); err != nil {
log.Debug("WorkerModelsEnabled> model %s/%s is not usable because flavor '%s' not found", allModels[i].Group.Name, allModels[i].Name, allModels[i].ModelVirtualMachine.Flavor)
continue
}

filteredModels = append(filteredModels, allModels[i])
}

// Sort models by required CPUs, this will allows to starts job without defined model on the smallest flavor.
sort.Slice(filteredModels, func(i, j int) bool {
flavorI, _ := h.flavor(filteredModels[i].ModelVirtualMachine.Flavor)
flavorJ, _ := h.flavor(filteredModels[j].ModelVirtualMachine.Flavor)
return flavorI.VCPUs < flavorJ.VCPUs
})

return filteredModels, nil
}

// WorkerModelSecretList returns secret for given model.
Expand All @@ -165,15 +193,6 @@ func (h *HatcheryOpenstack) WorkerModelSecretList(m sdk.Model) (sdk.WorkerModelS
// CanSpawn return wether or not hatchery can spawn model
// requirements are not supported
func (h *HatcheryOpenstack) CanSpawn(ctx context.Context, model *sdk.Model, jobID int64, requirements []sdk.Requirement) bool {
// if there is a model, we have to check if the flavor attached to model is knowned by this hatchery
if model != nil {
if _, err := h.flavorID(model.ModelVirtualMachine.Flavor); err != nil {
log.Debug("CanSpawn> h.flavorID on %s err:%v", model.ModelVirtualMachine.Flavor, err)
return false
}
log.Debug("CanSpawn> flavor '%s' found", model.ModelVirtualMachine.Flavor)
}

for _, r := range requirements {
if r.Type == sdk.ServiceRequirement || r.Type == sdk.MemoryRequirement || r.Type == sdk.HostnameRequirement {
return false
Expand Down
85 changes: 54 additions & 31 deletions engine/hatchery/openstack/openstack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import (
"context"
"testing"

"github.com/golang/mock/gomock"
"github.com/gophercloud/gophercloud/openstack/compute/v2/flavors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/ovh/cds/sdk"
"github.com/ovh/cds/sdk/cdsclient/mock_cdsclient"
"github.com/ovh/cds/sdk/log"
)

func TestHatcheryOpenstack_CanSpawn(t *testing.T) {
Expand All @@ -28,41 +32,60 @@ func TestHatcheryOpenstack_CanSpawn(t *testing.T) {
// no model, hostname prerequisite, canSpawn must be false: hostname can't be managed by openstack hatchery
canSpawn = h.CanSpawn(context.TODO(), nil, 1, []sdk.Requirement{{Type: sdk.HostnameRequirement, Value: "localhost"}})
require.False(t, canSpawn)
}

flavors := []flavors.Flavor{
{Name: "b2-7"},
}
h.flavors = flavors
func TestHatcheryOpenstack_WorkerModelsEnabled(t *testing.T) {
log.SetLogger(t)

m := &sdk.Model{
ID: 1,
Name: "my-model",
Group: &sdk.Group{
ID: 1,
Name: "mygroup",
},
ModelVirtualMachine: sdk.ModelVirtualMachine{
Flavor: "vps-ssd-3",
},
}
h := &HatcheryOpenstack{}

// model with a unknowned flavor
canSpawn = h.CanSpawn(context.TODO(), m, 1, nil)
require.False(t, canSpawn)
ctrl := gomock.NewController(t)
mockClient := mock_cdsclient.NewMockInterface(ctrl)
h.Client = mockClient
t.Cleanup(func() { ctrl.Finish() })

mockClient.EXPECT().WorkerModelEnabledList().DoAndReturn(func() ([]sdk.Model, error) {
return []sdk.Model{
{
ID: 1,
Type: sdk.Docker,
Name: "my-model-1",
Group: &sdk.Group{ID: 1, Name: "mygroup"},
},
{
ID: 2,
Type: sdk.Openstack,
Name: "my-model-2",
Group: &sdk.Group{ID: 1, Name: "mygroup"},
ModelVirtualMachine: sdk.ModelVirtualMachine{Flavor: "b2-120"},
},
{
ID: 3,
Type: sdk.Openstack,
Name: "my-model-3",
Group: &sdk.Group{ID: 1, Name: "mygroup"},
ModelVirtualMachine: sdk.ModelVirtualMachine{Flavor: "b2-7"},
},
{
ID: 4,
Type: sdk.Openstack,
Name: "my-model-4",
Group: &sdk.Group{ID: 1, Name: "mygroup"},
ModelVirtualMachine: sdk.ModelVirtualMachine{Flavor: "unknown"},
},
}, nil
})

m = &sdk.Model{
ID: 1,
Name: "my-model",
Group: &sdk.Group{
ID: 1,
Name: "mygroup",
},
ModelVirtualMachine: sdk.ModelVirtualMachine{
Flavor: "b2-7",
},
h.flavors = []flavors.Flavor{
{Name: "b2-7", VCPUs: 2},
{Name: "b2-30", VCPUs: 16},
{Name: "b2-120", VCPUs: 32},
}

// model with a knowned flavor
canSpawn = h.CanSpawn(context.TODO(), m, 1, nil)
require.True(t, canSpawn)
// Only model that match a known flavor should be returned and sorted by CPUs asc
ms, err := h.WorkerModelsEnabled()
require.NoError(t, err)
require.Len(t, ms, 2)
assert.Equal(t, "my-model-3", ms[0].Name)
assert.Equal(t, "my-model-2", ms[1].Name)
}
Loading

0 comments on commit 80140d7

Please sign in to comment.