Skip to content

Commit 942297b

Browse files
kindermoumoutejerome-quere
authored andcommitted
fix: create servers volumes algo (#546)
* Leverage compute resizing feature The size of the default volume will now be resized using the following rules: 1- the user define a custom root size 2- (default) use the largest possible size ==> min(categoryMaxSize,volumeMaxSize) 3- the user specify additional volumes ==> min(50G,volumeMaxSize) * Typo * Add unit tests
1 parent 6e07351 commit 942297b

File tree

4 files changed

+174
-56
lines changed

4 files changed

+174
-56
lines changed

pkg/api/api.go

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -293,15 +293,21 @@ type ProductVolumeConstraint struct {
293293
MaxSize uint64 `json:"max_size,omitempty"`
294294
}
295295

296+
// ProductVolumeConstraint contains any per volume constraint that the offer has
297+
type ProductPerVolumeConstraint struct {
298+
LSsdConstraint ProductVolumeConstraint `json:"l_ssd,omitempty"`
299+
}
300+
296301
// ProductServerOffer represents a specific offer
297302
type ProductServer struct {
298-
Arch string `json:"arch,omitempty"`
299-
Ncpus uint64 `json:"ncpus,omitempty"`
300-
Ram uint64 `json:"ram,omitempty"`
301-
Baremetal bool `json:"baremetal,omitempty"`
302-
VolumesConstraint ProductVolumeConstraint `json:"volumes_constraint,omitempty"`
303-
AltNames []string `json:"alt_names,omitempty"`
304-
Network ProductNetwork `json:"network,omitempty"`
303+
Arch string `json:"arch,omitempty"`
304+
Ncpus uint64 `json:"ncpus,omitempty"`
305+
Ram uint64 `json:"ram,omitempty"`
306+
Baremetal bool `json:"baremetal,omitempty"`
307+
VolumesConstraint ProductVolumeConstraint `json:"volumes_constraint,omitempty"`
308+
PerVolumesConstraint ProductPerVolumeConstraint `json:"per_volume_constraint,omitempty"`
309+
AltNames []string `json:"alt_names,omitempty"`
310+
Network ProductNetwork `json:"network,omitempty"`
305311
}
306312

307313
// Products holds a map of all Scaleway servers
@@ -634,6 +640,32 @@ type ScalewayServerPatchDefinition struct {
634640
BootType *string `json:"boot_type,omitempty"`
635641
}
636642

643+
type ScalewayServerVolumeDefinition interface {
644+
isScalewayServerVolumeDefinition()
645+
}
646+
647+
type ScalewayServerVolumeDefinitionNew struct {
648+
Name string `json:"name"`
649+
OrganizationId string `json:"organization"`
650+
Size uint64 `json:"size"`
651+
VolumeType string `json:"volume_type"`
652+
}
653+
654+
func (*ScalewayServerVolumeDefinitionNew) isScalewayServerVolumeDefinition() {
655+
}
656+
657+
type ScalewayServerVolumeDefinitionResize struct {
658+
Size uint64 `json:"size"`
659+
}
660+
661+
func (*ScalewayServerVolumeDefinitionResize) isScalewayServerVolumeDefinition() {
662+
}
663+
664+
type ScalewayServerVolumeDefinitionFromId string
665+
666+
func (ScalewayServerVolumeDefinitionFromId) isScalewayServerVolumeDefinition() {
667+
}
668+
637669
// ScalewayServerDefinition represents a Scaleway server with image definition
638670
type ScalewayServerDefinition struct {
639671
// Name is the user-defined name of the server
@@ -643,7 +675,7 @@ type ScalewayServerDefinition struct {
643675
Image *string `json:"image,omitempty"`
644676

645677
// Volumes are the attached volumes
646-
Volumes map[string]string `json:"volumes,omitempty"`
678+
Volumes map[string]ScalewayServerVolumeDefinition `json:"volumes,omitempty"`
647679

648680
// DynamicIPRequired is a flag that defines a server with a dynamic ip address attached
649681
DynamicIPRequired *bool `json:"dynamic_ip_required,omitempty"`

pkg/api/helpers.go

Lines changed: 68 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ package api
77
import (
88
"errors"
99
"fmt"
10-
"math"
1110
"os"
1211
"sort"
1312
"strings"
@@ -95,34 +94,29 @@ func CreateVolumeFromHumanSize(api *ScalewayAPI, size string) (*string, error) {
9594
return &volumeID, nil
9695
}
9796

98-
// VolumesFromSize returns a string of standard sized volumes from a given size
99-
func VolumesFromSize(size uint64) string {
100-
const DefaultVolumeSize float64 = 50000000000
101-
StdVolumeSizes := []struct {
102-
kind string
103-
capacity float64
104-
}{
105-
{"150G", 150000000000},
106-
{"100G", 100000000000},
107-
{"50G", 50000000000},
108-
}
109-
110-
RequiredSize := float64(size) - DefaultVolumeSize
111-
Volumes := ""
112-
for _, v := range StdVolumeSizes {
113-
q := RequiredSize / v.capacity
114-
r := math.Mod(RequiredSize, v.capacity)
115-
RequiredSize = r
116-
117-
if q > 0 {
118-
Volumes += strings.Repeat(v.kind+" ", int(q))
119-
}
120-
if r == 0 {
121-
break
122-
}
97+
func min(a, b uint64) uint64 {
98+
if a > b {
99+
return b
123100
}
101+
return a
102+
}
103+
104+
const Giga = 1000000000
124105

125-
return strings.TrimSpace(Volumes)
106+
// VolumesFromSize returns a string of standard sized volumes from a given size
107+
func VolumesFromSize(rootVolumeSize, targetSize, perVolumeMaxSize uint64) string {
108+
if targetSize <= rootVolumeSize {
109+
return ""
110+
}
111+
targetSize -= rootVolumeSize
112+
q := targetSize / perVolumeMaxSize
113+
r := targetSize % perVolumeMaxSize
114+
humanSize := fmt.Sprintf("%dG", perVolumeMaxSize/Giga)
115+
volumes := strings.Repeat(humanSize+" ", int(q))
116+
if r != 0 {
117+
volumes += fmt.Sprintf("%dG", r/Giga)
118+
}
119+
return strings.TrimSpace(volumes)
126120
}
127121

128122
// fillIdentifierCache fills the cache by fetching from the API
@@ -396,7 +390,7 @@ func CreateServer(api *ScalewayAPI, c *ConfigCreateServer) (string, error) {
396390
var server ScalewayServerDefinition
397391

398392
server.CommercialType = commercialType
399-
server.Volumes = make(map[string]string)
393+
server.Volumes = make(map[string]ScalewayServerVolumeDefinition)
400394
server.DynamicIPRequired = &c.DynamicIPRequired
401395
server.EnableIPV6 = c.EnableIPV6
402396
server.BootType = c.BootType
@@ -435,21 +429,59 @@ func CreateServer(api *ScalewayAPI, c *ConfigCreateServer) (string, error) {
435429
if err != nil {
436430
return "", fmt.Errorf("Unknow commercial type %v: %v", server.CommercialType, err)
437431
}
432+
//
433+
// Find the correct root size
434+
//
435+
// 1- the user define a custom root size
436+
// 2- (default) use the largest possible size ==> min(categoryMaxSize,volumeMaxSize)
437+
// 3- the user specify additional volumes ==> min(50G,volumeMaxSize)
438+
//
439+
isUserDefinedRootSize := true
440+
rootVolumeSize, err := humanize.ParseBytes(c.ImageName)
441+
if err != nil {
442+
isUserDefinedRootSize = false
443+
rootVolumeSize = min(offer.PerVolumesConstraint.LSsdConstraint.MaxSize, offer.VolumesConstraint.MaxSize)
444+
if c.AdditionalVolumes != "" {
445+
rootVolumeSize = min(50*Giga, offer.VolumesConstraint.MaxSize) // create a volume up to 50GB
446+
}
447+
}
448+
449+
if isUserDefinedRootSize {
450+
// create a new volume from scratch
451+
server.Volumes["0"] = &ScalewayServerVolumeDefinitionNew{
452+
OrganizationId: api.Organization,
453+
VolumeType: "l_ssd",
454+
Name: "Volume-0",
455+
Size: rootVolumeSize,
456+
}
457+
} else {
458+
// leverage compute image resizing
459+
server.Volumes["0"] = &ScalewayServerVolumeDefinitionResize{
460+
Size: rootVolumeSize,
461+
}
462+
}
463+
438464
if offer.VolumesConstraint.MinSize > 0 && c.AdditionalVolumes == "" {
439-
c.AdditionalVolumes = VolumesFromSize(offer.VolumesConstraint.MinSize)
440-
log.Debugf("%s needs at least %s. Automatically creates the following volumes: %s",
441-
server.CommercialType, humanize.Bytes(offer.VolumesConstraint.MinSize), c.AdditionalVolumes)
465+
c.AdditionalVolumes = VolumesFromSize(rootVolumeSize, offer.VolumesConstraint.MinSize, offer.PerVolumesConstraint.LSsdConstraint.MaxSize)
466+
log.Debugf("%s needs at least %s. Automatically creates the following volumes: %dG %s",
467+
server.CommercialType, humanize.Bytes(offer.VolumesConstraint.MinSize), rootVolumeSize/Giga, c.AdditionalVolumes)
442468
}
469+
443470
if c.AdditionalVolumes != "" {
444471
volumes := strings.Split(c.AdditionalVolumes, " ")
445472
for i := range volumes {
446-
volumeID, err := CreateVolumeFromHumanSize(api, volumes[i])
473+
rootSize, err := humanize.ParseBytes(volumes[i])
447474
if err != nil {
448475
return "", err
449476
}
450477

451478
volumeIDx := fmt.Sprintf("%d", i+1)
452-
server.Volumes[volumeIDx] = *volumeID
479+
server.Volumes[volumeIDx] = &ScalewayServerVolumeDefinitionNew{
480+
OrganizationId: api.Organization,
481+
VolumeType: "l_ssd",
482+
Name: "Volume-" + volumeIDx,
483+
Size: rootSize,
484+
}
453485
}
454486
}
455487

@@ -462,15 +494,8 @@ func CreateServer(api *ScalewayAPI, c *ConfigCreateServer) (string, error) {
462494
}
463495
server.Name = c.Name
464496
inheritingVolume := false
465-
_, err = humanize.ParseBytes(c.ImageName)
466-
if err == nil {
467-
// Create a new root volume
468-
volumeID, errCreateVol := CreateVolumeFromHumanSize(api, c.ImageName)
469-
if errCreateVol != nil {
470-
return "", errCreateVol
471-
}
472-
server.Volumes["0"] = *volumeID
473-
} else {
497+
498+
if !isUserDefinedRootSize {
474499
// Use an existing image
475500
inheritingVolume = true
476501
if anonuuid.IsUUID(c.ImageName) == nil {
@@ -494,7 +519,7 @@ func CreateServer(api *ScalewayAPI, c *ConfigCreateServer) (string, error) {
494519
if snapshot.BaseVolume.Identifier == "" {
495520
return "", fmt.Errorf("snapshot %v does not have base volume", snapshot.Name)
496521
}
497-
server.Volumes["0"] = snapshot.BaseVolume.Identifier
522+
server.Volumes["0"] = ScalewayServerVolumeDefinitionFromId(snapshot.BaseVolume.Identifier)
498523
}
499524
}
500525
}

pkg/api/helpers_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package api
2+
3+
import (
4+
. "github.com/smartystreets/goconvey/convey"
5+
"testing"
6+
)
7+
8+
type VolumesFromSizeCase struct {
9+
name string
10+
input struct {
11+
rootVolumeSize, targeSize, perVolumeMaxSize uint64
12+
}
13+
output string
14+
}
15+
16+
func TestVolumesFromSize(t *testing.T) {
17+
tests := []VolumesFromSizeCase{
18+
{
19+
name: "200G 200G 200G",
20+
input: struct{ rootVolumeSize, targeSize, perVolumeMaxSize uint64 }{
21+
200 * Giga, 600 * Giga, 200 * Giga,
22+
},
23+
output: "200G 200G",
24+
},
25+
{
26+
name: "200G 200G 100G",
27+
input: struct{ rootVolumeSize, targeSize, perVolumeMaxSize uint64 }{
28+
200 * Giga, 500 * Giga, 200 * Giga,
29+
},
30+
output: "200G 100G",
31+
},
32+
{
33+
name: "25G",
34+
input: struct{ rootVolumeSize, targeSize, perVolumeMaxSize uint64 }{
35+
25 * Giga, 25 * Giga, 200 * Giga,
36+
},
37+
output: "",
38+
},
39+
{
40+
name: "100G 150G",
41+
input: struct{ rootVolumeSize, targeSize, perVolumeMaxSize uint64 }{
42+
100 * Giga, 250 * Giga, 200 * Giga,
43+
},
44+
output: "150G",
45+
},
46+
{
47+
name: "200G 50G 50G 50G 50G",
48+
input: struct{ rootVolumeSize, targeSize, perVolumeMaxSize uint64 }{
49+
200 * Giga, 400 * Giga, 50 * Giga,
50+
},
51+
output: "50G 50G 50G 50G",
52+
},
53+
}
54+
for _, test := range tests {
55+
Convey("Testing VolumesFromSize with expected "+test.name, t, func(c C) {
56+
output := VolumesFromSize(test.input.rootVolumeSize, test.input.targeSize, test.input.perVolumeMaxSize)
57+
c.So(output, ShouldEqual, test.output)
58+
})
59+
}
60+
61+
}

pkg/api/logger.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,20 @@ func (l *defaultLogger) LogHTTP(r *http.Request) {
3636
}
3737

3838
func (l *defaultLogger) Fatalf(format string, v ...interface{}) {
39-
l.Printf("[FATAL] %s\n", fmt.Sprintf(format, v))
39+
l.Printf("[FATAL] %s\n", fmt.Sprintf(format, v...))
4040
os.Exit(1)
4141
}
4242

4343
func (l *defaultLogger) Debugf(format string, v ...interface{}) {
44-
l.Printf("[DEBUG] %s\n", fmt.Sprintf(format, v))
44+
l.Printf("[DEBUG] %s\n", fmt.Sprintf(format, v...))
4545
}
4646

4747
func (l *defaultLogger) Infof(format string, v ...interface{}) {
48-
l.Printf("[INFO ] %s\n", fmt.Sprintf(format, v))
48+
l.Printf("[INFO ] %s\n", fmt.Sprintf(format, v...))
4949
}
5050

5151
func (l *defaultLogger) Warnf(format string, v ...interface{}) {
52-
l.Printf("[WARN ] %s\n", fmt.Sprintf(format, v))
52+
l.Printf("[WARN ] %s\n", fmt.Sprintf(format, v...))
5353
}
5454

5555
type disableLogger struct {
@@ -64,7 +64,7 @@ func (d *disableLogger) LogHTTP(r *http.Request) {
6464
}
6565

6666
func (d *disableLogger) Fatalf(format string, v ...interface{}) {
67-
panic(fmt.Sprintf(format, v))
67+
panic(fmt.Sprintf(format, v...))
6868
}
6969

7070
func (d *disableLogger) Debugf(format string, v ...interface{}) {

0 commit comments

Comments
 (0)