Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(hatchery): openstack max CPUs count and flavor weigth #5190

Merged
merged 5 commits into from
Jun 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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