Skip to content

Commit 3a8866a

Browse files
authored
feat(instance): server create using snapshots (#3063)
1 parent 9ec11d4 commit 3a8866a

10 files changed

+13271
-3
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 volumes from snapshots
22+
scw instance server create image=ubuntu_focal root-volume=local:<snapshot_id> additional-volumes.0=block:<snapshot_id>
23+
2124
Use an existing IP
2225
ip=$(scw instance ip create | grep id | awk '{ print $2 }')
2326
scw instance server create image=ubuntu_focal ip=$ip

docs/commands/instance.md

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

1735+
Create an instance with volumes from snapshots
1736+
```
1737+
scw instance server create image=ubuntu_focal root-volume=local:<snapshot_id> additional-volumes.0=block:<snapshot_id>
1738+
```
1739+
17351740
Use an existing IP
17361741
```
17371742
ip=$(scw instance ip create | grep id | awk '{ print $2 }')

internal/namespaces/instance/v1/custom_server_create.go

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@ func serverCreateCommand() *core.Command {
176176
Short: "Create an instance with 2 local volumes (10GB and 10GB)",
177177
ArgsJSON: `{"image":"ubuntu_focal","root_volume":"local:10GB","additional_volumes":["local:10GB"]}`,
178178
},
179+
{
180+
Short: "Create an instance with volumes from snapshots",
181+
ArgsJSON: `{"image":"ubuntu_focal","root_volume":"local:<snapshot_id>","additional_volumes":["block:<snapshot_id>"]}`,
182+
},
179183
{
180184
Short: "Use an existing IP",
181185
Raw: `ip=$(scw instance ip create | grep id | awk '{ print $2 }')
@@ -493,6 +497,7 @@ func buildVolumes(api *instance.API, zone scw.Zone, serverName, rootVolume strin
493497
//
494498
// A valid volume format is either
495499
// - a "creation" format: ^((local|l|block|b):)?\d+GB?$ (size is handled by go-humanize, so other sizes are supported)
500+
// - a "creation" format with a snapshot id: l:<uuid> b:<uuid>
496501
// - a UUID format
497502
func buildVolumeTemplate(api *instance.API, zone scw.Zone, flagV string) (*instance.VolumeServerTemplate, error) {
498503
parts := strings.Split(strings.TrimSpace(flagV), ":")
@@ -510,6 +515,10 @@ func buildVolumeTemplate(api *instance.API, zone scw.Zone, flagV string) (*insta
510515
return nil, fmt.Errorf("invalid volume type %s in %s volume", parts[0], flagV)
511516
}
512517

518+
if validation.IsUUID(parts[1]) {
519+
return buildVolumeTemplateFromSnapshot(api, zone, parts[1], vt.VolumeType)
520+
}
521+
513522
size, err := humanize.ParseBytes(parts[1])
514523
if err != nil {
515524
return nil, fmt.Errorf("invalid size format %s in %s volume", parts[1], flagV)
@@ -534,14 +543,17 @@ func buildVolumeTemplate(api *instance.API, zone scw.Zone, flagV string) (*insta
534543
// buildVolumeTemplateFromUUID validate an UUID volume and add their types and sizes.
535544
// Add volume types and sizes allow US to treat UUID volumes like the others and simplify the implementation.
536545
// The instance API refuse the type and the size for UUID volumes, therefore,
537-
// buildVolumeMap function will remove them.
546+
// sanitizeVolumeMap function will remove them.
538547
func buildVolumeTemplateFromUUID(api *instance.API, zone scw.Zone, volumeUUID string) (*instance.VolumeServerTemplate, error) {
539548
res, err := api.GetVolume(&instance.GetVolumeRequest{
540549
Zone: zone,
541550
VolumeID: volumeUUID,
542551
})
543-
if err != nil { // FIXME: isNotFoundError
544-
return nil, fmt.Errorf("volume %s does not exist", volumeUUID)
552+
if err != nil {
553+
if core.IsNotFoundError(err) {
554+
return nil, fmt.Errorf("volume %s does not exist", volumeUUID)
555+
}
556+
return nil, err
545557
}
546558

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

571+
// buildVolumeTemplateFromUUID validate a snapshot UUID and check that requested volume type is compatible.
572+
// The instance API refuse the size for Snapshot volumes, therefore,
573+
// sanitizeVolumeMap function will remove them.
574+
func buildVolumeTemplateFromSnapshot(api *instance.API, zone scw.Zone, snapshotUUID string, volumeType instance.VolumeVolumeType) (*instance.VolumeServerTemplate, error) {
575+
res, err := api.GetSnapshot(&instance.GetSnapshotRequest{
576+
Zone: zone,
577+
SnapshotID: snapshotUUID,
578+
})
579+
if err != nil {
580+
if core.IsNotFoundError(err) {
581+
return nil, fmt.Errorf("snapshot %s does not exist", snapshotUUID)
582+
}
583+
return nil, err
584+
}
585+
586+
snapshotType := res.Snapshot.VolumeType
587+
588+
if snapshotType != instance.VolumeVolumeTypeUnified && snapshotType != volumeType {
589+
return nil, fmt.Errorf("snapshot of type %s not compatible with requested volume type %s", snapshotType, volumeType)
590+
}
591+
592+
return &instance.VolumeServerTemplate{
593+
Name: res.Snapshot.Name,
594+
VolumeType: volumeType,
595+
BaseSnapshot: res.Snapshot.ID,
596+
Size: res.Snapshot.Size,
597+
}, nil
598+
}
599+
559600
func validateImageServerTypeCompatibility(image *instance.Image, serverType *instance.ServerType, CommercialType string) error {
560601
// An instance might not have any constraints on the local volume size
561602
if serverType.VolumesConstraint.MaxSize == 0 {
@@ -637,6 +678,12 @@ func sanitizeVolumeMap(serverName string, volumes map[string]*instance.VolumeSer
637678
ID: v.ID,
638679
Name: v.Name,
639680
}
681+
case v.BaseSnapshot != "":
682+
v = &instance.VolumeServerTemplate{
683+
BaseSnapshot: v.BaseSnapshot,
684+
Name: v.Name,
685+
VolumeType: v.VolumeType,
686+
}
640687
case index == "0" && v.Size != 0:
641688
v = &instance.VolumeServerTemplate{
642689
VolumeType: v.VolumeType,

internal/namespaces/instance/v1/custom_server_create_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,26 @@ func Test_CreateServer(t *testing.T) {
125125
AfterFunc: deleteServerAfterFunc(),
126126
}))
127127

128+
t.Run("valid single local snapshot", core.Test(&core.TestConfig{
129+
Commands: GetCommands(),
130+
BeforeFunc: core.BeforeFuncCombine(
131+
core.ExecStoreBeforeCmd("Server", "scw instance server create image=ubuntu_bionic root-volume=local:20GB stopped=true"),
132+
core.ExecStoreBeforeCmd("Snapshot", `scw instance snapshot create volume-id={{ (index .Server.Volumes "0").ID }}`),
133+
),
134+
Cmd: "scw instance server create image=ubuntu_bionic root-volume=local:{{ .Snapshot.Snapshot.ID }} stopped=true",
135+
Check: core.TestCheckCombine(
136+
core.TestCheckExitCode(0),
137+
func(t *testing.T, ctx *core.CheckFuncCtx) {
138+
assert.Equal(t, 20*scw.GB, ctx.Result.(*instance.Server).Volumes["0"].Size)
139+
},
140+
),
141+
AfterFunc: core.AfterFuncCombine(
142+
deleteServer("Server"),
143+
deleteServerAfterFunc(),
144+
deleteSnapshot("Snapshot"),
145+
),
146+
}))
147+
128148
t.Run("valid double local volumes", core.Test(&core.TestConfig{
129149
Commands: GetCommands(),
130150
Cmd: "scw instance server create image=ubuntu_bionic root-volume=local:10GB additional-volumes.0=l:10G stopped=true",
@@ -138,6 +158,27 @@ func Test_CreateServer(t *testing.T) {
138158
AfterFunc: deleteServerAfterFunc(),
139159
}))
140160

161+
t.Run("valid double snapshot", core.Test(&core.TestConfig{
162+
Commands: GetCommands(),
163+
BeforeFunc: core.BeforeFuncCombine(
164+
core.ExecStoreBeforeCmd("Server", "scw instance server create image=ubuntu_bionic root-volume=local:20GB stopped=true"),
165+
core.ExecStoreBeforeCmd("Snapshot", `scw instance snapshot create unified=true volume-id={{ (index .Server.Volumes "0").ID }}`),
166+
),
167+
Cmd: "scw instance server create image=ubuntu_bionic root-volume=block:{{ .Snapshot.Snapshot.ID }} additional-volumes.0=local:{{ .Snapshot.Snapshot.ID }} stopped=true",
168+
Check: core.TestCheckCombine(
169+
core.TestCheckExitCode(0),
170+
func(t *testing.T, ctx *core.CheckFuncCtx) {
171+
assert.Equal(t, 20*scw.GB, ctx.Result.(*instance.Server).Volumes["0"].Size)
172+
assert.Equal(t, 20*scw.GB, ctx.Result.(*instance.Server).Volumes["1"].Size)
173+
},
174+
),
175+
AfterFunc: core.AfterFuncCombine(
176+
deleteServer("Server"),
177+
deleteServerAfterFunc(),
178+
deleteSnapshot("Snapshot"),
179+
),
180+
}))
181+
141182
t.Run("valid additional block volumes", core.Test(&core.TestConfig{
142183
Commands: GetCommands(),
143184
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",
@@ -385,6 +426,26 @@ func Test_CreateServerErrors(t *testing.T) {
385426
DisableParallel: true,
386427
}))
387428

429+
t.Run("Error: invalid root volume snapshot ID", core.Test(&core.TestConfig{
430+
Commands: GetCommands(),
431+
Cmd: "scw instance server create image=ubuntu_bionic root-volume=local:29da9ad9-e759-4a56-82c8-f0607f93055c",
432+
Check: core.TestCheckCombine(
433+
core.TestCheckGolden(),
434+
core.TestCheckExitCode(1),
435+
),
436+
DisableParallel: true,
437+
}))
438+
439+
t.Run("Error: invalid additional volume snapshot ID", core.Test(&core.TestConfig{
440+
Commands: GetCommands(),
441+
Cmd: "scw instance server create image=ubuntu_bionic additional-volumes.0=block:29da9ad9-e759-4a56-82c8-f0607f93055c",
442+
Check: core.TestCheckCombine(
443+
core.TestCheckGolden(),
444+
core.TestCheckExitCode(1),
445+
),
446+
DisableParallel: true,
447+
}))
448+
388449
////
389450
// IP errors
390451
////

0 commit comments

Comments
 (0)