diff --git a/pkg/api/api.go b/pkg/api/api.go index 2b3d0288c0..dcf78528cb 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -293,15 +293,21 @@ type ProductVolumeConstraint struct { MaxSize uint64 `json:"max_size,omitempty"` } +// ProductVolumeConstraint contains any per volume constraint that the offer has +type ProductPerVolumeConstraint struct { + LSsdConstraint ProductVolumeConstraint `json:"l_ssd,omitempty"` +} + // ProductServerOffer represents a specific offer type ProductServer struct { - Arch string `json:"arch,omitempty"` - Ncpus uint64 `json:"ncpus,omitempty"` - Ram uint64 `json:"ram,omitempty"` - Baremetal bool `json:"baremetal,omitempty"` - VolumesConstraint ProductVolumeConstraint `json:"volumes_constraint,omitempty"` - AltNames []string `json:"alt_names,omitempty"` - Network ProductNetwork `json:"network,omitempty"` + Arch string `json:"arch,omitempty"` + Ncpus uint64 `json:"ncpus,omitempty"` + Ram uint64 `json:"ram,omitempty"` + Baremetal bool `json:"baremetal,omitempty"` + VolumesConstraint ProductVolumeConstraint `json:"volumes_constraint,omitempty"` + PerVolumesConstraint ProductPerVolumeConstraint `json:"per_volume_constraint,omitempty"` + AltNames []string `json:"alt_names,omitempty"` + Network ProductNetwork `json:"network,omitempty"` } // Products holds a map of all Scaleway servers @@ -634,6 +640,32 @@ type ScalewayServerPatchDefinition struct { BootType *string `json:"boot_type,omitempty"` } +type ScalewayServerVolumeDefinition interface { + isScalewayServerVolumeDefinition() +} + +type ScalewayServerVolumeDefinitionNew struct { + Name string `json:"name"` + OrganizationId string `json:"organization"` + Size uint64 `json:"size"` + VolumeType string `json:"volume_type"` +} + +func (*ScalewayServerVolumeDefinitionNew) isScalewayServerVolumeDefinition() { +} + +type ScalewayServerVolumeDefinitionResize struct { + Size uint64 `json:"size"` +} + +func (*ScalewayServerVolumeDefinitionResize) isScalewayServerVolumeDefinition() { +} + +type ScalewayServerVolumeDefinitionFromId string + +func (ScalewayServerVolumeDefinitionFromId) isScalewayServerVolumeDefinition() { +} + // ScalewayServerDefinition represents a Scaleway server with image definition type ScalewayServerDefinition struct { // Name is the user-defined name of the server @@ -643,7 +675,7 @@ type ScalewayServerDefinition struct { Image *string `json:"image,omitempty"` // Volumes are the attached volumes - Volumes map[string]string `json:"volumes,omitempty"` + Volumes map[string]ScalewayServerVolumeDefinition `json:"volumes,omitempty"` // DynamicIPRequired is a flag that defines a server with a dynamic ip address attached DynamicIPRequired *bool `json:"dynamic_ip_required,omitempty"` diff --git a/pkg/api/helpers.go b/pkg/api/helpers.go index 5b5224ba65..86f7e9789a 100644 --- a/pkg/api/helpers.go +++ b/pkg/api/helpers.go @@ -7,7 +7,6 @@ package api import ( "errors" "fmt" - "math" "os" "sort" "strings" @@ -95,34 +94,29 @@ func CreateVolumeFromHumanSize(api *ScalewayAPI, size string) (*string, error) { return &volumeID, nil } -// VolumesFromSize returns a string of standard sized volumes from a given size -func VolumesFromSize(size uint64) string { - const DefaultVolumeSize float64 = 50000000000 - StdVolumeSizes := []struct { - kind string - capacity float64 - }{ - {"150G", 150000000000}, - {"100G", 100000000000}, - {"50G", 50000000000}, - } - - RequiredSize := float64(size) - DefaultVolumeSize - Volumes := "" - for _, v := range StdVolumeSizes { - q := RequiredSize / v.capacity - r := math.Mod(RequiredSize, v.capacity) - RequiredSize = r - - if q > 0 { - Volumes += strings.Repeat(v.kind+" ", int(q)) - } - if r == 0 { - break - } +func min(a, b uint64) uint64 { + if a > b { + return b } + return a +} + +const Giga = 1000000000 - return strings.TrimSpace(Volumes) +// VolumesFromSize returns a string of standard sized volumes from a given size +func VolumesFromSize(rootVolumeSize, targetSize, perVolumeMaxSize uint64) string { + if targetSize <= rootVolumeSize { + return "" + } + targetSize -= rootVolumeSize + q := targetSize / perVolumeMaxSize + r := targetSize % perVolumeMaxSize + humanSize := fmt.Sprintf("%dG", perVolumeMaxSize/Giga) + volumes := strings.Repeat(humanSize+" ", int(q)) + if r != 0 { + volumes += fmt.Sprintf("%dG", r/Giga) + } + return strings.TrimSpace(volumes) } // fillIdentifierCache fills the cache by fetching from the API @@ -396,7 +390,7 @@ func CreateServer(api *ScalewayAPI, c *ConfigCreateServer) (string, error) { var server ScalewayServerDefinition server.CommercialType = commercialType - server.Volumes = make(map[string]string) + server.Volumes = make(map[string]ScalewayServerVolumeDefinition) server.DynamicIPRequired = &c.DynamicIPRequired server.EnableIPV6 = c.EnableIPV6 server.BootType = c.BootType @@ -435,21 +429,59 @@ func CreateServer(api *ScalewayAPI, c *ConfigCreateServer) (string, error) { if err != nil { return "", fmt.Errorf("Unknow commercial type %v: %v", server.CommercialType, err) } + // + // Find the correct root size + // + // 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) + // + isUserDefinedRootSize := true + rootVolumeSize, err := humanize.ParseBytes(c.ImageName) + if err != nil { + isUserDefinedRootSize = false + rootVolumeSize = min(offer.PerVolumesConstraint.LSsdConstraint.MaxSize, offer.VolumesConstraint.MaxSize) + if c.AdditionalVolumes != "" { + rootVolumeSize = min(50*Giga, offer.VolumesConstraint.MaxSize) // create a volume up to 50GB + } + } + + if isUserDefinedRootSize { + // create a new volume from scratch + server.Volumes["0"] = &ScalewayServerVolumeDefinitionNew{ + OrganizationId: api.Organization, + VolumeType: "l_ssd", + Name: "Volume-0", + Size: rootVolumeSize, + } + } else { + // leverage compute image resizing + server.Volumes["0"] = &ScalewayServerVolumeDefinitionResize{ + Size: rootVolumeSize, + } + } + if offer.VolumesConstraint.MinSize > 0 && c.AdditionalVolumes == "" { - c.AdditionalVolumes = VolumesFromSize(offer.VolumesConstraint.MinSize) - log.Debugf("%s needs at least %s. Automatically creates the following volumes: %s", - server.CommercialType, humanize.Bytes(offer.VolumesConstraint.MinSize), c.AdditionalVolumes) + c.AdditionalVolumes = VolumesFromSize(rootVolumeSize, offer.VolumesConstraint.MinSize, offer.PerVolumesConstraint.LSsdConstraint.MaxSize) + log.Debugf("%s needs at least %s. Automatically creates the following volumes: %dG %s", + server.CommercialType, humanize.Bytes(offer.VolumesConstraint.MinSize), rootVolumeSize/Giga, c.AdditionalVolumes) } + if c.AdditionalVolumes != "" { volumes := strings.Split(c.AdditionalVolumes, " ") for i := range volumes { - volumeID, err := CreateVolumeFromHumanSize(api, volumes[i]) + rootSize, err := humanize.ParseBytes(volumes[i]) if err != nil { return "", err } volumeIDx := fmt.Sprintf("%d", i+1) - server.Volumes[volumeIDx] = *volumeID + server.Volumes[volumeIDx] = &ScalewayServerVolumeDefinitionNew{ + OrganizationId: api.Organization, + VolumeType: "l_ssd", + Name: "Volume-" + volumeIDx, + Size: rootSize, + } } } @@ -462,15 +494,8 @@ func CreateServer(api *ScalewayAPI, c *ConfigCreateServer) (string, error) { } server.Name = c.Name inheritingVolume := false - _, err = humanize.ParseBytes(c.ImageName) - if err == nil { - // Create a new root volume - volumeID, errCreateVol := CreateVolumeFromHumanSize(api, c.ImageName) - if errCreateVol != nil { - return "", errCreateVol - } - server.Volumes["0"] = *volumeID - } else { + + if !isUserDefinedRootSize { // Use an existing image inheritingVolume = true if anonuuid.IsUUID(c.ImageName) == nil { @@ -494,7 +519,7 @@ func CreateServer(api *ScalewayAPI, c *ConfigCreateServer) (string, error) { if snapshot.BaseVolume.Identifier == "" { return "", fmt.Errorf("snapshot %v does not have base volume", snapshot.Name) } - server.Volumes["0"] = snapshot.BaseVolume.Identifier + server.Volumes["0"] = ScalewayServerVolumeDefinitionFromId(snapshot.BaseVolume.Identifier) } } } diff --git a/pkg/api/helpers_test.go b/pkg/api/helpers_test.go new file mode 100644 index 0000000000..28225ec356 --- /dev/null +++ b/pkg/api/helpers_test.go @@ -0,0 +1,61 @@ +package api + +import ( + . "github.com/smartystreets/goconvey/convey" + "testing" +) + +type VolumesFromSizeCase struct { + name string + input struct { + rootVolumeSize, targeSize, perVolumeMaxSize uint64 + } + output string +} + +func TestVolumesFromSize(t *testing.T) { + tests := []VolumesFromSizeCase{ + { + name: "200G 200G 200G", + input: struct{ rootVolumeSize, targeSize, perVolumeMaxSize uint64 }{ + 200 * Giga, 600 * Giga, 200 * Giga, + }, + output: "200G 200G", + }, + { + name: "200G 200G 100G", + input: struct{ rootVolumeSize, targeSize, perVolumeMaxSize uint64 }{ + 200 * Giga, 500 * Giga, 200 * Giga, + }, + output: "200G 100G", + }, + { + name: "25G", + input: struct{ rootVolumeSize, targeSize, perVolumeMaxSize uint64 }{ + 25 * Giga, 25 * Giga, 200 * Giga, + }, + output: "", + }, + { + name: "100G 150G", + input: struct{ rootVolumeSize, targeSize, perVolumeMaxSize uint64 }{ + 100 * Giga, 250 * Giga, 200 * Giga, + }, + output: "150G", + }, + { + name: "200G 50G 50G 50G 50G", + input: struct{ rootVolumeSize, targeSize, perVolumeMaxSize uint64 }{ + 200 * Giga, 400 * Giga, 50 * Giga, + }, + output: "50G 50G 50G 50G", + }, + } + for _, test := range tests { + Convey("Testing VolumesFromSize with expected "+test.name, t, func(c C) { + output := VolumesFromSize(test.input.rootVolumeSize, test.input.targeSize, test.input.perVolumeMaxSize) + c.So(output, ShouldEqual, test.output) + }) + } + +} diff --git a/pkg/api/logger.go b/pkg/api/logger.go index 58ad93716a..d6222d4498 100644 --- a/pkg/api/logger.go +++ b/pkg/api/logger.go @@ -36,20 +36,20 @@ func (l *defaultLogger) LogHTTP(r *http.Request) { } func (l *defaultLogger) Fatalf(format string, v ...interface{}) { - l.Printf("[FATAL] %s\n", fmt.Sprintf(format, v)) + l.Printf("[FATAL] %s\n", fmt.Sprintf(format, v...)) os.Exit(1) } func (l *defaultLogger) Debugf(format string, v ...interface{}) { - l.Printf("[DEBUG] %s\n", fmt.Sprintf(format, v)) + l.Printf("[DEBUG] %s\n", fmt.Sprintf(format, v...)) } func (l *defaultLogger) Infof(format string, v ...interface{}) { - l.Printf("[INFO ] %s\n", fmt.Sprintf(format, v)) + l.Printf("[INFO ] %s\n", fmt.Sprintf(format, v...)) } func (l *defaultLogger) Warnf(format string, v ...interface{}) { - l.Printf("[WARN ] %s\n", fmt.Sprintf(format, v)) + l.Printf("[WARN ] %s\n", fmt.Sprintf(format, v...)) } type disableLogger struct { @@ -64,7 +64,7 @@ func (d *disableLogger) LogHTTP(r *http.Request) { } func (d *disableLogger) Fatalf(format string, v ...interface{}) { - panic(fmt.Sprintf(format, v)) + panic(fmt.Sprintf(format, v...)) } func (d *disableLogger) Debugf(format string, v ...interface{}) {