Skip to content

Commit

Permalink
feat(instance): server create using snapshots (#3063)
Browse files Browse the repository at this point in the history
  • Loading branch information
Codelax committed Apr 24, 2023
1 parent 9ec11d4 commit 3a8866a
Show file tree
Hide file tree
Showing 10 changed files with 13,271 additions and 3 deletions.
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 @@ -1732,6 +1732,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

0 comments on commit 3a8866a

Please sign in to comment.