Skip to content

Commit 3a9d72b

Browse files
authored
feat(instance): server create with custom iops volumes (scaleway#4140)
1 parent 8606d82 commit 3a9d72b

12 files changed

+4217
-182
lines changed

cmd/scw/testdata/test-all-usage-instance-server-create-usage.golden

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ EXAMPLES:
1818
Create an instance with 2 local volumes (10GB and 10GB)
1919
scw instance server create image=ubuntu_focal root-volume=local:10GB additional-volumes.0=local:10GB
2020

21+
Create an instance with a SBS root volume (100GB and 15000 iops)
22+
scw instance server create image=ubuntu_focal root-volume=sbs:100GB:15000
23+
2124
Create an instance with volumes from snapshots
2225
scw instance server create image=ubuntu_focal root-volume=local:<snapshot_id> additional-volumes.0=block:<snapshot_id>
2326

docs/commands/instance.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1752,6 +1752,11 @@ Create an instance with 2 local volumes (10GB and 10GB)
17521752
scw instance server create image=ubuntu_focal root-volume=local:10GB additional-volumes.0=local:10GB
17531753
```
17541754

1755+
Create an instance with a SBS root volume (100GB and 15000 iops)
1756+
```
1757+
scw instance server create image=ubuntu_focal root-volume=sbs:100GB:15000
1758+
```
1759+
17551760
Create an instance with volumes from snapshots
17561761
```
17571762
scw instance server create image=ubuntu_focal root-volume=local:<snapshot_id> additional-volumes.0=block:<snapshot_id>

internal/namespaces/instance/v1/custom_server_create.go

Lines changed: 12 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,10 @@ import (
1111

1212
"github.com/dustin/go-humanize"
1313
"github.com/scaleway/scaleway-cli/v2/core"
14-
block "github.com/scaleway/scaleway-sdk-go/api/block/v1alpha1"
1514
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
1615
"github.com/scaleway/scaleway-sdk-go/api/marketplace/v2"
1716
"github.com/scaleway/scaleway-sdk-go/logger"
1817
"github.com/scaleway/scaleway-sdk-go/scw"
19-
"github.com/scaleway/scaleway-sdk-go/validation"
2018
)
2119

2220
type instanceCreateServerRequest struct {
@@ -161,6 +159,10 @@ func serverCreateCommand() *core.Command {
161159
Short: "Create an instance with 2 local volumes (10GB and 10GB)",
162160
ArgsJSON: `{"image":"ubuntu_focal","root_volume":"local:10GB","additional_volumes":["local:10GB"]}`,
163161
},
162+
{
163+
Short: "Create an instance with a SBS root volume (100GB and 15000 iops)",
164+
ArgsJSON: `{"image":"ubuntu_focal","root_volume":"sbs:100GB:15000"}`,
165+
},
164166
{
165167
Short: "Create an instance with volumes from snapshots",
166168
ArgsJSON: `{"image":"ubuntu_focal","root_volume":"local:<snapshot_id>","additional_volumes":["block:<snapshot_id>"]}`,
@@ -239,6 +241,7 @@ func instanceServerCreateRun(ctx context.Context, argsI interface{}) (i interfac
239241
}
240242

241243
createReq, createIPReq := serverBuilder.Build()
244+
postCreationSetup := serverBuilder.BuildPostCreationSetup()
242245
needIPCreation := createIPReq != nil
243246

244247
//
@@ -280,6 +283,13 @@ func instanceServerCreateRun(ctx context.Context, argsI interface{}) (i interfac
280283
server := serverRes.Server
281284
logger.Debugf("server created %s", server.ID)
282285

286+
// Post server creation setup
287+
/// Setup SBS volumes IOPS
288+
err = postCreationSetup(ctx, server)
289+
if err != nil {
290+
logger.Warningf("error while setting up server after creation: %s", err.Error())
291+
}
292+
283293
//
284294
// Cloud-init
285295
//
@@ -357,162 +367,6 @@ func addDefaultVolumes(serverType *instance.ServerType, volumes map[string]*inst
357367
return volumes
358368
}
359369

360-
// buildVolumes creates the initial volume map.
361-
// It is not the definitive one, it will be mutated all along the process.
362-
func buildVolumes(api *instance.API, blockAPI *block.API, zone scw.Zone, serverName, rootVolume string, additionalVolumes []string) (map[string]*instance.VolumeServerTemplate, error) {
363-
volumes := make(map[string]*instance.VolumeServerTemplate)
364-
if rootVolume != "" {
365-
rootVolumeTemplate, err := buildVolumeTemplate(api, blockAPI, zone, rootVolume)
366-
if err != nil {
367-
return nil, err
368-
}
369-
370-
volumes["0"] = rootVolumeTemplate
371-
}
372-
373-
for i, v := range additionalVolumes {
374-
volumeTemplate, err := buildVolumeTemplate(api, blockAPI, zone, v)
375-
if err != nil {
376-
return nil, err
377-
}
378-
index := strconv.Itoa(i + 1)
379-
volumeTemplate.Name = scw.StringPtr(serverName + "-" + index)
380-
381-
volumes[index] = volumeTemplate
382-
}
383-
384-
return volumes, nil
385-
}
386-
387-
// buildVolumeTemplate creates a instance.VolumeTemplate from a 'volumes' argument item.
388-
//
389-
// Volumes definition must be through multiple arguments (eg: volumes.0="l:20GB" volumes.1="b:100GB")
390-
//
391-
// A valid volume format is either
392-
// - a "creation" format: ^((local|l|block|b|scratch|s):)?\d+GB?$ (size is handled by go-humanize, so other sizes are supported)
393-
// - a "creation" format with a snapshot id: l:<uuid> b:<uuid>
394-
// - a UUID format
395-
func buildVolumeTemplate(api *instance.API, blockAPI *block.API, zone scw.Zone, flagV string) (*instance.VolumeServerTemplate, error) {
396-
parts := strings.Split(strings.TrimSpace(flagV), ":")
397-
398-
// Create volume.
399-
if len(parts) == 2 {
400-
vt := &instance.VolumeServerTemplate{}
401-
402-
switch parts[0] {
403-
case "l", "local":
404-
vt.VolumeType = instance.VolumeVolumeTypeLSSD
405-
case "b", "block":
406-
vt.VolumeType = instance.VolumeVolumeTypeBSSD
407-
case "s", "scratch":
408-
vt.VolumeType = instance.VolumeVolumeTypeScratch
409-
case "sbs":
410-
vt.VolumeType = instance.VolumeVolumeTypeSbsVolume
411-
default:
412-
return nil, fmt.Errorf("invalid volume type %s in %s volume", parts[0], flagV)
413-
}
414-
415-
if validation.IsUUID(parts[1]) {
416-
return buildVolumeTemplateFromSnapshot(api, zone, parts[1], vt.VolumeType)
417-
}
418-
419-
size, err := humanize.ParseBytes(parts[1])
420-
if err != nil {
421-
return nil, fmt.Errorf("invalid size format %s in %s volume", parts[1], flagV)
422-
}
423-
vt.Size = scw.SizePtr(scw.Size(size))
424-
425-
return vt, nil
426-
}
427-
428-
// UUID format.
429-
if len(parts) == 1 && validation.IsUUID(parts[0]) {
430-
return buildVolumeTemplateFromUUID(api, blockAPI, zone, parts[0])
431-
}
432-
433-
return nil, &core.CliError{
434-
Err: fmt.Errorf("invalid volume format '%s'", flagV),
435-
Details: "",
436-
Hint: `You must provide either a UUID ("11111111-1111-1111-1111-111111111111"), a local volume size ("local:100G" or "l:100G") or a block volume size ("block:100G" or "b:100G").`,
437-
}
438-
}
439-
440-
// buildVolumeTemplateFromUUID validate an UUID volume and add their types and sizes.
441-
// Add volume types and sizes allow US to treat UUID volumes like the others and simplify the implementation.
442-
// The instance API refuse the type and the size for UUID volumes, therefore,
443-
// sanitizeVolumeMap function will remove them.
444-
func buildVolumeTemplateFromUUID(api *instance.API, blockAPI *block.API, zone scw.Zone, volumeUUID string) (*instance.VolumeServerTemplate, error) {
445-
res, err := api.GetVolume(&instance.GetVolumeRequest{
446-
Zone: zone,
447-
VolumeID: volumeUUID,
448-
})
449-
if err != nil && !core.IsNotFoundError(err) {
450-
return nil, err
451-
}
452-
453-
if res != nil {
454-
// Check that volume is not already attached to a server.
455-
if res.Volume.Server != nil {
456-
return nil, fmt.Errorf("volume %s is already attached to %s server", res.Volume.ID, res.Volume.Server.ID)
457-
}
458-
459-
return &instance.VolumeServerTemplate{
460-
ID: &res.Volume.ID,
461-
VolumeType: res.Volume.VolumeType,
462-
Size: &res.Volume.Size,
463-
}, nil
464-
}
465-
466-
blockRes, err := blockAPI.GetVolume(&block.GetVolumeRequest{
467-
Zone: zone,
468-
VolumeID: volumeUUID,
469-
})
470-
if err != nil {
471-
if core.IsNotFoundError(err) {
472-
return nil, fmt.Errorf("volume %s does not exist", volumeUUID)
473-
}
474-
return nil, err
475-
}
476-
477-
if len(blockRes.References) > 0 {
478-
return nil, fmt.Errorf("volume %s is already attached to %s %s", blockRes.ID, blockRes.References[0].ProductResourceID, blockRes.References[0].ProductResourceType)
479-
}
480-
481-
return &instance.VolumeServerTemplate{
482-
ID: &blockRes.ID,
483-
VolumeType: instance.VolumeVolumeTypeSbsVolume, // TODO: support snapshot
484-
}, nil
485-
}
486-
487-
// buildVolumeTemplateFromUUID validate a snapshot UUID and check that requested volume type is compatible.
488-
// The instance API refuse the size for Snapshot volumes, therefore,
489-
// sanitizeVolumeMap function will remove them.
490-
func buildVolumeTemplateFromSnapshot(api *instance.API, zone scw.Zone, snapshotUUID string, volumeType instance.VolumeVolumeType) (*instance.VolumeServerTemplate, error) {
491-
res, err := api.GetSnapshot(&instance.GetSnapshotRequest{
492-
Zone: zone,
493-
SnapshotID: snapshotUUID,
494-
})
495-
if err != nil {
496-
if core.IsNotFoundError(err) {
497-
return nil, fmt.Errorf("snapshot %s does not exist", snapshotUUID)
498-
}
499-
return nil, err
500-
}
501-
502-
snapshotType := res.Snapshot.VolumeType
503-
504-
if snapshotType != instance.VolumeVolumeTypeUnified && snapshotType != volumeType {
505-
return nil, fmt.Errorf("snapshot of type %s not compatible with requested volume type %s", snapshotType, volumeType)
506-
}
507-
508-
return &instance.VolumeServerTemplate{
509-
Name: &res.Snapshot.Name,
510-
VolumeType: volumeType,
511-
BaseSnapshot: &res.Snapshot.ID,
512-
Size: &res.Snapshot.Size,
513-
}, nil
514-
}
515-
516370
func validateImageServerTypeCompatibility(image *instance.Image, serverType *instance.ServerType, commercialType string) error {
517371
// An instance might not have any constraints on the local volume size
518372
if serverType.VolumesConstraint.MaxSize == 0 {

0 commit comments

Comments
 (0)