Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ EXAMPLES:
Create an instance with 2 local volumes (10GB and 10GB)
scw instance server create image=ubuntu_focal root-volume=local:10GB additional-volumes.0=local:10GB

Create an instance with volumes from snapshots
scw instance server create image=ubuntu_focal root-volume=local:<snapshot_id> additional-volumes.0=block:<snapshot_id>

Use an existing IP
ip=$(scw instance ip create | grep id | awk '{ print $2 }')
scw instance server create image=ubuntu_focal ip=$ip
Expand Down
5 changes: 5 additions & 0 deletions docs/commands/instance.md
Original file line number Diff line number Diff line change
Expand Up @@ -1731,6 +1731,11 @@ Create an instance with 2 local volumes (10GB and 10GB)
scw instance server create image=ubuntu_focal root-volume=local:10GB additional-volumes.0=local:10GB
```

Create an instance with volumes from snapshots
```
scw instance server create image=ubuntu_focal root-volume=local:<snapshot_id> additional-volumes.0=block:<snapshot_id>
```

Use an existing IP
```
ip=$(scw instance ip create | grep id | awk '{ print $2 }')
Expand Down
53 changes: 50 additions & 3 deletions internal/namespaces/instance/v1/custom_server_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ func serverCreateCommand() *core.Command {
Short: "Create an instance with 2 local volumes (10GB and 10GB)",
ArgsJSON: `{"image":"ubuntu_focal","root_volume":"local:10GB","additional_volumes":["local:10GB"]}`,
},
{
Short: "Create an instance with volumes from snapshots",
ArgsJSON: `{"image":"ubuntu_focal","root_volume":"local:<snapshot_id>","additional_volumes":["block:<snapshot_id>"]}`,
},
{
Short: "Use an existing IP",
Raw: `ip=$(scw instance ip create | grep id | awk '{ print $2 }')
Expand Down Expand Up @@ -493,6 +497,7 @@ func buildVolumes(api *instance.API, zone scw.Zone, serverName, rootVolume strin
//
// A valid volume format is either
// - a "creation" format: ^((local|l|block|b):)?\d+GB?$ (size is handled by go-humanize, so other sizes are supported)
// - a "creation" format with a snapshot id: l:<uuid> b:<uuid>
// - a UUID format
func buildVolumeTemplate(api *instance.API, zone scw.Zone, flagV string) (*instance.VolumeServerTemplate, error) {
parts := strings.Split(strings.TrimSpace(flagV), ":")
Expand All @@ -510,6 +515,10 @@ func buildVolumeTemplate(api *instance.API, zone scw.Zone, flagV string) (*insta
return nil, fmt.Errorf("invalid volume type %s in %s volume", parts[0], flagV)
}

if validation.IsUUID(parts[1]) {
return buildVolumeTemplateFromSnapshot(api, zone, parts[1], vt.VolumeType)
}

size, err := humanize.ParseBytes(parts[1])
if err != nil {
return nil, fmt.Errorf("invalid size format %s in %s volume", parts[1], flagV)
Expand All @@ -534,14 +543,17 @@ func buildVolumeTemplate(api *instance.API, zone scw.Zone, flagV string) (*insta
// buildVolumeTemplateFromUUID validate an UUID volume and add their types and sizes.
// Add volume types and sizes allow US to treat UUID volumes like the others and simplify the implementation.
// The instance API refuse the type and the size for UUID volumes, therefore,
// buildVolumeMap function will remove them.
// sanitizeVolumeMap function will remove them.
func buildVolumeTemplateFromUUID(api *instance.API, zone scw.Zone, volumeUUID string) (*instance.VolumeServerTemplate, error) {
res, err := api.GetVolume(&instance.GetVolumeRequest{
Zone: zone,
VolumeID: volumeUUID,
})
if err != nil { // FIXME: isNotFoundError
return nil, fmt.Errorf("volume %s does not exist", volumeUUID)
if err != nil {
if core.IsNotFoundError(err) {
return nil, fmt.Errorf("volume %s does not exist", volumeUUID)
}
return nil, err
}

// Check that volume is not already attached to a server.
Expand All @@ -556,6 +568,35 @@ func buildVolumeTemplateFromUUID(api *instance.API, zone scw.Zone, volumeUUID st
}, nil
}

// buildVolumeTemplateFromUUID validate a snapshot UUID and check that requested volume type is compatible.
// The instance API refuse the size for Snapshot volumes, therefore,
// sanitizeVolumeMap function will remove them.
func buildVolumeTemplateFromSnapshot(api *instance.API, zone scw.Zone, snapshotUUID string, volumeType instance.VolumeVolumeType) (*instance.VolumeServerTemplate, error) {
res, err := api.GetSnapshot(&instance.GetSnapshotRequest{
Zone: zone,
SnapshotID: snapshotUUID,
})
if err != nil {
if core.IsNotFoundError(err) {
return nil, fmt.Errorf("snapshot %s does not exist", snapshotUUID)
}
return nil, err
}

snapshotType := res.Snapshot.VolumeType

if snapshotType != instance.VolumeVolumeTypeUnified && snapshotType != volumeType {
return nil, fmt.Errorf("snapshot of type %s not compatible with requested volume type %s", snapshotType, volumeType)
}

return &instance.VolumeServerTemplate{
Name: res.Snapshot.Name,
VolumeType: volumeType,
BaseSnapshot: res.Snapshot.ID,
Size: res.Snapshot.Size,
}, nil
}

func validateImageServerTypeCompatibility(image *instance.Image, serverType *instance.ServerType, CommercialType string) error {
// An instance might not have any constraints on the local volume size
if serverType.VolumesConstraint.MaxSize == 0 {
Expand Down Expand Up @@ -637,6 +678,12 @@ func sanitizeVolumeMap(serverName string, volumes map[string]*instance.VolumeSer
ID: v.ID,
Name: v.Name,
}
case v.BaseSnapshot != "":
v = &instance.VolumeServerTemplate{
BaseSnapshot: v.BaseSnapshot,
Name: v.Name,
VolumeType: v.VolumeType,
}
case index == "0" && v.Size != 0:
v = &instance.VolumeServerTemplate{
VolumeType: v.VolumeType,
Expand Down
61 changes: 61 additions & 0 deletions internal/namespaces/instance/v1/custom_server_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,26 @@ func Test_CreateServer(t *testing.T) {
AfterFunc: deleteServerAfterFunc(),
}))

t.Run("valid single local snapshot", core.Test(&core.TestConfig{
Commands: GetCommands(),
BeforeFunc: core.BeforeFuncCombine(
core.ExecStoreBeforeCmd("Server", "scw instance server create image=ubuntu_bionic root-volume=local:20GB stopped=true"),
core.ExecStoreBeforeCmd("Snapshot", `scw instance snapshot create volume-id={{ (index .Server.Volumes "0").ID }}`),
),
Cmd: "scw instance server create image=ubuntu_bionic root-volume=local:{{ .Snapshot.Snapshot.ID }} stopped=true",
Check: core.TestCheckCombine(
core.TestCheckExitCode(0),
func(t *testing.T, ctx *core.CheckFuncCtx) {
assert.Equal(t, 20*scw.GB, ctx.Result.(*instance.Server).Volumes["0"].Size)
},
),
AfterFunc: core.AfterFuncCombine(
deleteServer("Server"),
deleteServerAfterFunc(),
deleteSnapshot("Snapshot"),
),
}))

t.Run("valid double local volumes", core.Test(&core.TestConfig{
Commands: GetCommands(),
Cmd: "scw instance server create image=ubuntu_bionic root-volume=local:10GB additional-volumes.0=l:10G stopped=true",
Expand All @@ -138,6 +158,27 @@ func Test_CreateServer(t *testing.T) {
AfterFunc: deleteServerAfterFunc(),
}))

t.Run("valid double snapshot", core.Test(&core.TestConfig{
Commands: GetCommands(),
BeforeFunc: core.BeforeFuncCombine(
core.ExecStoreBeforeCmd("Server", "scw instance server create image=ubuntu_bionic root-volume=local:20GB stopped=true"),
core.ExecStoreBeforeCmd("Snapshot", `scw instance snapshot create unified=true volume-id={{ (index .Server.Volumes "0").ID }}`),
),
Cmd: "scw instance server create image=ubuntu_bionic root-volume=block:{{ .Snapshot.Snapshot.ID }} additional-volumes.0=local:{{ .Snapshot.Snapshot.ID }} stopped=true",
Check: core.TestCheckCombine(
core.TestCheckExitCode(0),
func(t *testing.T, ctx *core.CheckFuncCtx) {
assert.Equal(t, 20*scw.GB, ctx.Result.(*instance.Server).Volumes["0"].Size)
assert.Equal(t, 20*scw.GB, ctx.Result.(*instance.Server).Volumes["1"].Size)
},
),
AfterFunc: core.AfterFuncCombine(
deleteServer("Server"),
deleteServerAfterFunc(),
deleteSnapshot("Snapshot"),
),
}))

t.Run("valid additional block volumes", core.Test(&core.TestConfig{
Commands: GetCommands(),
Cmd: "scw instance server create image=ubuntu_bionic additional-volumes.0=b:1G additional-volumes.1=b:5G additional-volumes.2=b:10G stopped=true",
Expand Down Expand Up @@ -385,6 +426,26 @@ func Test_CreateServerErrors(t *testing.T) {
DisableParallel: true,
}))

t.Run("Error: invalid root volume snapshot ID", core.Test(&core.TestConfig{
Commands: GetCommands(),
Cmd: "scw instance server create image=ubuntu_bionic root-volume=local:29da9ad9-e759-4a56-82c8-f0607f93055c",
Check: core.TestCheckCombine(
core.TestCheckGolden(),
core.TestCheckExitCode(1),
),
DisableParallel: true,
}))

t.Run("Error: invalid additional volume snapshot ID", core.Test(&core.TestConfig{
Commands: GetCommands(),
Cmd: "scw instance server create image=ubuntu_bionic additional-volumes.0=block:29da9ad9-e759-4a56-82c8-f0607f93055c",
Check: core.TestCheckCombine(
core.TestCheckGolden(),
core.TestCheckExitCode(1),
),
DisableParallel: true,
}))

////
// IP errors
////
Expand Down
Loading