bfv updates (#100)
This commit creates a DestinationType for the possible block device
destination types.
It allows VolumeSize to be omitted.
Finally, it adds both unit and acceptance tests for all possible ways
that the bootfromvolume extension can be used.
* Renaming and reordering source and destination types
* Erroneous rename
diff --git a/acceptance/openstack/blockstorage/v2/blockstorage.go b/acceptance/openstack/blockstorage/v2/blockstorage.go
index e007c2a..555bdcd 100644
--- a/acceptance/openstack/blockstorage/v2/blockstorage.go
+++ b/acceptance/openstack/blockstorage/v2/blockstorage.go
@@ -7,6 +7,7 @@
"testing"
"github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes"
)
@@ -39,6 +40,35 @@
return volume, nil
}
+// CreateVolumeFromImage will create a volume from with a random name and size of
+// 1GB. An error will be returned if the volume was unable to be created.
+func CreateVolumeFromImage(t *testing.T, client *gophercloud.ServiceClient, choices *clients.AcceptanceTestChoices) (*volumes.Volume, error) {
+ if testing.Short() {
+ t.Skip("Skipping test that requires volume creation in short mode.")
+ }
+
+ volumeName := tools.RandomString("ACPTTEST", 16)
+ t.Logf("Attempting to create volume: %s", volumeName)
+
+ createOpts := volumes.CreateOpts{
+ Size: 1,
+ Name: volumeName,
+ ImageID: choices.ImageID,
+ }
+
+ volume, err := volumes.Create(client, createOpts).Extract()
+ if err != nil {
+ return volume, err
+ }
+
+ err = volumes.WaitForStatus(client, volume.ID, "available", 60)
+ if err != nil {
+ return volume, err
+ }
+
+ return volume, nil
+}
+
// DeleteVolume will delete a volume. A fatal error will occur if the volume
// failed to be deleted. This works best when used as a deferred function.
func DeleteVolume(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) {
diff --git a/acceptance/openstack/compute/v2/bootfromvolume_test.go b/acceptance/openstack/compute/v2/bootfromvolume_test.go
index 844a7cb..54719d0 100644
--- a/acceptance/openstack/compute/v2/bootfromvolume_test.go
+++ b/acceptance/openstack/compute/v2/bootfromvolume_test.go
@@ -6,10 +6,11 @@
"testing"
"github.com/gophercloud/gophercloud/acceptance/clients"
+ blockstorage "github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v2"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
)
-func TestBootFromVolumeSingleVolume(t *testing.T) {
+func TestBootFromImage(t *testing.T) {
if testing.Short() {
t.Skip("Skipping test that requires server creation in short mode.")
}
@@ -26,10 +27,44 @@
blockDevices := []bootfromvolume.BlockDevice{
bootfromvolume.BlockDevice{
- UUID: choices.ImageID,
- SourceType: bootfromvolume.Image,
+ BootIndex: 0,
DeleteOnTermination: true,
- DestinationType: "volume",
+ DestinationType: bootfromvolume.DestinationLocal,
+ SourceType: bootfromvolume.SourceImage,
+ UUID: choices.ImageID,
+ },
+ }
+
+ server, err := CreateBootableVolumeServer(t, client, blockDevices, choices)
+ if err != nil {
+ t.Fatalf("Unable to create server: %v", err)
+ }
+ defer DeleteServer(t, client, server)
+
+ PrintServer(t, server)
+}
+
+func TestBootFromNewVolume(t *testing.T) {
+ if testing.Short() {
+ t.Skip("Skipping test that requires server creation in short mode.")
+ }
+
+ client, err := clients.NewComputeV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ choices, err := clients.AcceptanceTestChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ blockDevices := []bootfromvolume.BlockDevice{
+ bootfromvolume.BlockDevice{
+ DeleteOnTermination: true,
+ DestinationType: bootfromvolume.DestinationVolume,
+ SourceType: bootfromvolume.SourceImage,
+ UUID: choices.ImageID,
VolumeSize: 2,
},
}
@@ -43,6 +78,49 @@
PrintServer(t, server)
}
+func TestBootFromExistingVolume(t *testing.T) {
+ if testing.Short() {
+ t.Skip("Skipping test that requires server creation in short mode.")
+ }
+
+ computeClient, err := clients.NewComputeV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ blockStorageClient, err := clients.NewBlockStorageV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a block storage client: %v", err)
+ }
+
+ choices, err := clients.AcceptanceTestChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ volume, err := blockstorage.CreateVolumeFromImage(t, blockStorageClient, choices)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ blockDevices := []bootfromvolume.BlockDevice{
+ bootfromvolume.BlockDevice{
+ DeleteOnTermination: true,
+ DestinationType: bootfromvolume.DestinationVolume,
+ SourceType: bootfromvolume.SourceVolume,
+ UUID: volume.ID,
+ },
+ }
+
+ server, err := CreateBootableVolumeServer(t, computeClient, blockDevices, choices)
+ if err != nil {
+ t.Fatalf("Unable to create server: %v", err)
+ }
+ defer DeleteServer(t, computeClient, server)
+
+ PrintServer(t, server)
+}
+
func TestBootFromMultiEphemeralServer(t *testing.T) {
if testing.Short() {
t.Skip("Skipping test that requires server creation in short mode.")
@@ -61,26 +139,26 @@
blockDevices := []bootfromvolume.BlockDevice{
bootfromvolume.BlockDevice{
BootIndex: 0,
- UUID: choices.ImageID,
- SourceType: bootfromvolume.Image,
- DestinationType: "local",
+ DestinationType: bootfromvolume.DestinationLocal,
DeleteOnTermination: true,
+ SourceType: bootfromvolume.SourceImage,
+ UUID: choices.ImageID,
VolumeSize: 5,
},
bootfromvolume.BlockDevice{
BootIndex: -1,
- SourceType: bootfromvolume.Blank,
- DestinationType: "local",
+ DestinationType: bootfromvolume.DestinationLocal,
DeleteOnTermination: true,
GuestFormat: "ext4",
+ SourceType: bootfromvolume.SourceBlank,
VolumeSize: 1,
},
bootfromvolume.BlockDevice{
BootIndex: -1,
- SourceType: bootfromvolume.Blank,
- DestinationType: "local",
+ DestinationType: bootfromvolume.DestinationLocal,
DeleteOnTermination: true,
GuestFormat: "ext4",
+ SourceType: bootfromvolume.SourceBlank,
VolumeSize: 1,
},
}
@@ -93,3 +171,95 @@
PrintServer(t, server)
}
+
+func TestAttachNewVolume(t *testing.T) {
+ if testing.Short() {
+ t.Skip("Skipping test that requires server creation in short mode.")
+ }
+
+ client, err := clients.NewComputeV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ choices, err := clients.AcceptanceTestChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ blockDevices := []bootfromvolume.BlockDevice{
+ bootfromvolume.BlockDevice{
+ BootIndex: 0,
+ DeleteOnTermination: true,
+ DestinationType: bootfromvolume.DestinationLocal,
+ SourceType: bootfromvolume.SourceImage,
+ UUID: choices.ImageID,
+ },
+ bootfromvolume.BlockDevice{
+ BootIndex: 1,
+ DeleteOnTermination: true,
+ DestinationType: bootfromvolume.DestinationVolume,
+ SourceType: bootfromvolume.SourceBlank,
+ VolumeSize: 2,
+ },
+ }
+
+ server, err := CreateBootableVolumeServer(t, client, blockDevices, choices)
+ if err != nil {
+ t.Fatalf("Unable to create server: %v", err)
+ }
+ defer DeleteServer(t, client, server)
+
+ PrintServer(t, server)
+}
+
+func TestAttachExistingVolume(t *testing.T) {
+ if testing.Short() {
+ t.Skip("Skipping test that requires server creation in short mode.")
+ }
+
+ computeClient, err := clients.NewComputeV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ blockStorageClient, err := clients.NewBlockStorageV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a block storage client: %v", err)
+ }
+
+ choices, err := clients.AcceptanceTestChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ volume, err := blockstorage.CreateVolume(t, blockStorageClient)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ blockDevices := []bootfromvolume.BlockDevice{
+ bootfromvolume.BlockDevice{
+ BootIndex: 0,
+ DeleteOnTermination: true,
+ DestinationType: bootfromvolume.DestinationLocal,
+ SourceType: bootfromvolume.SourceImage,
+ UUID: choices.ImageID,
+ },
+ bootfromvolume.BlockDevice{
+ BootIndex: 1,
+ DeleteOnTermination: true,
+ DestinationType: bootfromvolume.DestinationVolume,
+ SourceType: bootfromvolume.SourceVolume,
+ UUID: volume.ID,
+ },
+ }
+
+ server, err := CreateBootableVolumeServer(t, computeClient, blockDevices, choices)
+ if err != nil {
+ t.Fatalf("Unable to create server: %v", err)
+ }
+ defer DeleteServer(t, computeClient, server)
+
+ PrintServer(t, server)
+}
diff --git a/acceptance/openstack/compute/v2/compute.go b/acceptance/openstack/compute/v2/compute.go
index 9b284c3..3392c2e 100644
--- a/acceptance/openstack/compute/v2/compute.go
+++ b/acceptance/openstack/compute/v2/compute.go
@@ -90,6 +90,10 @@
},
}
+ if blockDevices[0].SourceType == bootfromvolume.SourceImage && blockDevices[0].DestinationType == bootfromvolume.DestinationLocal {
+ serverCreateOpts.ImageRef = blockDevices[0].UUID
+ }
+
server, err = bootfromvolume.Create(client, bootfromvolume.CreateOptsExt{
serverCreateOpts,
blockDevices,
diff --git a/openstack/compute/v2/extensions/bootfromvolume/requests.go b/openstack/compute/v2/extensions/bootfromvolume/requests.go
index 28fef94..9dae14c 100644
--- a/openstack/compute/v2/extensions/bootfromvolume/requests.go
+++ b/openstack/compute/v2/extensions/bootfromvolume/requests.go
@@ -5,40 +5,68 @@
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
)
-// SourceType represents the type of medium being used to create the volume.
-type SourceType string
+type (
+ // DestinationType represents the type of medium being used as the
+ // destination of the bootable device.
+ DestinationType string
-const (
- // Volume SourceType
- Volume SourceType = "volume"
- // Snapshot SourceType
- Snapshot SourceType = "snapshot"
- // Image SourceType
- Image SourceType = "image"
- // Blank SourceType
- Blank SourceType = "blank"
+ // SourceType represents the type of medium being used as the source of the
+ // bootable device.
+ SourceType string
)
-// BlockDevice is a structure with options for booting a server instance
-// from a volume. The volume may be created from an image, snapshot, or another
-// volume.
+const (
+ // DestinationLocal DestinationType is for using an ephemeral disk as the
+ // destination.
+ DestinationLocal DestinationType = "local"
+
+ // DestinationVolume DestinationType is for using a volume as the destination.
+ DestinationVolume DestinationType = "volume"
+
+ // SourceBlank SourceType is for a "blank" or empty source.
+ SourceBlank SourceType = "blank"
+
+ // SourceImage SourceType is for using images as the source of a block device.
+ SourceImage SourceType = "image"
+
+ // SourceSnapshot SourceType is for using a volume snapshot as the source of
+ // a block device.
+ SourceSnapshot SourceType = "snapshot"
+
+ // SourceVolume SourceType is for using a volume as the source of block
+ // device.
+ SourceVolume SourceType = "volume"
+)
+
+// BlockDevice is a structure with options for creating block devices in a
+// server. The block device may be created from an image, snapshot, new volume,
+// or existing volume. The destination may be a new volume, existing volume
+// which will be attached to the instance, ephemeral disk, or boot device.
type BlockDevice struct {
- // SourceType must be one of: "volume", "snapshot", "image".
+ // SourceType must be one of: "volume", "snapshot", "image", or "blank".
SourceType SourceType `json:"source_type" required:"true"`
- // UUID is the unique identifier for the volume, snapshot, or image (see above)
+
+ // UUID is the unique identifier for the existing volume, snapshot, or
+ // image (see above).
UUID string `json:"uuid,omitempty"`
+
// BootIndex is the boot index. It defaults to 0.
BootIndex int `json:"boot_index"`
+
// DeleteOnTermination specifies whether or not to delete the attached volume
// when the server is deleted. Defaults to `false`.
DeleteOnTermination bool `json:"delete_on_termination"`
+
// DestinationType is the type that gets created. Possible values are "volume"
// and "local".
- DestinationType string `json:"destination_type,omitempty"`
+ DestinationType DestinationType `json:"destination_type,omitempty"`
+
// GuestFormat specifies the format of the block device.
GuestFormat string `json:"guest_format,omitempty"`
- // VolumeSize is the size of the volume to create (in gigabytes).
- VolumeSize int `json:"volume_size"`
+
+ // VolumeSize is the size of the volume to create (in gigabytes). This can be
+ // omitted for existing volumes.
+ VolumeSize int `json:"volume_size,omitempty"`
}
// CreateOptsExt is a structure that extends the server `CreateOpts` structure
diff --git a/openstack/compute/v2/extensions/bootfromvolume/testing/requests_test.go b/openstack/compute/v2/extensions/bootfromvolume/testing/requests_test.go
index 6724dab..7fd3e7d 100644
--- a/openstack/compute/v2/extensions/bootfromvolume/testing/requests_test.go
+++ b/openstack/compute/v2/extensions/bootfromvolume/testing/requests_test.go
@@ -8,7 +8,91 @@
th "github.com/gophercloud/gophercloud/testhelper"
)
-func TestCreateOpts(t *testing.T) {
+func TestBootFromNewVolume(t *testing.T) {
+ base := servers.CreateOpts{
+ Name: "createdserver",
+ FlavorRef: "performance1-1",
+ }
+
+ ext := bootfromvolume.CreateOptsExt{
+ CreateOptsBuilder: base,
+ BlockDevice: []bootfromvolume.BlockDevice{
+ {
+ UUID: "123456",
+ SourceType: bootfromvolume.SourceImage,
+ DestinationType: bootfromvolume.DestinationVolume,
+ VolumeSize: 10,
+ DeleteOnTermination: true,
+ },
+ },
+ }
+
+ expected := `
+ {
+ "server": {
+ "name":"createdserver",
+ "flavorRef":"performance1-1",
+ "imageRef":"",
+ "block_device_mapping_v2":[
+ {
+ "uuid":"123456",
+ "source_type":"image",
+ "destination_type":"volume",
+ "boot_index": 0,
+ "delete_on_termination": true,
+ "volume_size": 10
+ }
+ ]
+ }
+ }
+ `
+ actual, err := ext.ToServerCreateMap()
+ th.AssertNoErr(t, err)
+ th.CheckJSONEquals(t, expected, actual)
+}
+
+func TestBootFromExistingVolume(t *testing.T) {
+ base := servers.CreateOpts{
+ Name: "createdserver",
+ FlavorRef: "performance1-1",
+ }
+
+ ext := bootfromvolume.CreateOptsExt{
+ CreateOptsBuilder: base,
+ BlockDevice: []bootfromvolume.BlockDevice{
+ {
+ UUID: "123456",
+ SourceType: bootfromvolume.SourceVolume,
+ DestinationType: bootfromvolume.DestinationVolume,
+ DeleteOnTermination: true,
+ },
+ },
+ }
+
+ expected := `
+ {
+ "server": {
+ "name":"createdserver",
+ "flavorRef":"performance1-1",
+ "imageRef":"",
+ "block_device_mapping_v2":[
+ {
+ "uuid":"123456",
+ "source_type":"volume",
+ "destination_type":"volume",
+ "boot_index": 0,
+ "delete_on_termination": true
+ }
+ ]
+ }
+ }
+ `
+ actual, err := ext.ToServerCreateMap()
+ th.AssertNoErr(t, err)
+ th.CheckJSONEquals(t, expected, actual)
+}
+
+func TestBootFromImage(t *testing.T) {
base := servers.CreateOpts{
Name: "createdserver",
ImageRef: "asdfasdfasdf",
@@ -19,11 +103,11 @@
CreateOptsBuilder: base,
BlockDevice: []bootfromvolume.BlockDevice{
{
- UUID: "123456",
- SourceType: bootfromvolume.Image,
- DestinationType: "volume",
- VolumeSize: 10,
- DeleteOnTermination: false,
+ BootIndex: 0,
+ DeleteOnTermination: true,
+ DestinationType: bootfromvolume.DestinationLocal,
+ SourceType: bootfromvolume.SourceImage,
+ UUID: "asdfasdfasdf",
},
},
}
@@ -36,55 +120,11 @@
"flavorRef": "performance1-1",
"block_device_mapping_v2":[
{
- "uuid":"123456",
- "source_type":"image",
- "destination_type":"volume",
"boot_index": 0,
- "delete_on_termination": false,
- "volume_size": 10
- }
- ]
- }
- }
- `
- actual, err := ext.ToServerCreateMap()
- th.AssertNoErr(t, err)
- th.CheckJSONEquals(t, expected, actual)
-}
-
-func TestCreateOptsWithoutImageRef(t *testing.T) {
- base := servers.CreateOpts{
- Name: "createdserver",
- FlavorRef: "performance1-1",
- }
-
- ext := bootfromvolume.CreateOptsExt{
- CreateOptsBuilder: base,
- BlockDevice: []bootfromvolume.BlockDevice{
- {
- UUID: "123456",
- SourceType: bootfromvolume.Image,
- DestinationType: "volume",
- VolumeSize: 10,
- DeleteOnTermination: false,
- },
- },
- }
-
- expected := `
- {
- "server": {
- "name": "createdserver",
- "imageRef": "",
- "flavorRef": "performance1-1",
- "block_device_mapping_v2":[
- {
- "uuid":"123456",
+ "delete_on_termination": true,
+ "destination_type":"local",
"source_type":"image",
- "destination_type":"volume",
- "boot_index": 0,
- "delete_on_termination": false,
- "volume_size": 10
+ "uuid":"asdfasdfasdf"
}
]
}
@@ -108,24 +148,24 @@
{
BootIndex: 0,
DeleteOnTermination: true,
- DestinationType: "local",
- SourceType: bootfromvolume.Image,
- UUID: "123456",
+ DestinationType: bootfromvolume.DestinationLocal,
+ SourceType: bootfromvolume.SourceImage,
+ UUID: "asdfasdfasdf",
},
{
BootIndex: -1,
DeleteOnTermination: true,
- DestinationType: "local",
+ DestinationType: bootfromvolume.DestinationLocal,
GuestFormat: "ext4",
- SourceType: bootfromvolume.Blank,
+ SourceType: bootfromvolume.SourceBlank,
VolumeSize: 1,
},
{
BootIndex: -1,
DeleteOnTermination: true,
- DestinationType: "local",
+ DestinationType: bootfromvolume.DestinationLocal,
GuestFormat: "ext4",
- SourceType: bootfromvolume.Blank,
+ SourceType: bootfromvolume.SourceBlank,
VolumeSize: 1,
},
},
@@ -143,8 +183,7 @@
"delete_on_termination": true,
"destination_type":"local",
"source_type":"image",
- "uuid":"123456",
- "volume_size": 0
+ "uuid":"asdfasdfasdf"
},
{
"boot_index": -1,
@@ -170,3 +209,119 @@
th.AssertNoErr(t, err)
th.CheckJSONEquals(t, expected, actual)
}
+
+func TestAttachNewVolume(t *testing.T) {
+ base := servers.CreateOpts{
+ Name: "createdserver",
+ ImageRef: "asdfasdfasdf",
+ FlavorRef: "performance1-1",
+ }
+
+ ext := bootfromvolume.CreateOptsExt{
+ CreateOptsBuilder: base,
+ BlockDevice: []bootfromvolume.BlockDevice{
+ {
+ BootIndex: 0,
+ DeleteOnTermination: true,
+ DestinationType: bootfromvolume.DestinationLocal,
+ SourceType: bootfromvolume.SourceImage,
+ UUID: "asdfasdfasdf",
+ },
+ {
+ BootIndex: 1,
+ DeleteOnTermination: true,
+ DestinationType: bootfromvolume.DestinationVolume,
+ SourceType: bootfromvolume.SourceBlank,
+ VolumeSize: 1,
+ },
+ },
+ }
+
+ expected := `
+ {
+ "server": {
+ "name": "createdserver",
+ "imageRef": "asdfasdfasdf",
+ "flavorRef": "performance1-1",
+ "block_device_mapping_v2":[
+ {
+ "boot_index": 0,
+ "delete_on_termination": true,
+ "destination_type":"local",
+ "source_type":"image",
+ "uuid":"asdfasdfasdf"
+ },
+ {
+ "boot_index": 1,
+ "delete_on_termination": true,
+ "destination_type":"volume",
+ "source_type":"blank",
+ "volume_size": 1
+ }
+ ]
+ }
+ }
+ `
+ actual, err := ext.ToServerCreateMap()
+ th.AssertNoErr(t, err)
+ th.CheckJSONEquals(t, expected, actual)
+}
+
+func TestAttachExistingVolume(t *testing.T) {
+ base := servers.CreateOpts{
+ Name: "createdserver",
+ ImageRef: "asdfasdfasdf",
+ FlavorRef: "performance1-1",
+ }
+
+ ext := bootfromvolume.CreateOptsExt{
+ CreateOptsBuilder: base,
+ BlockDevice: []bootfromvolume.BlockDevice{
+ {
+ BootIndex: 0,
+ DeleteOnTermination: true,
+ DestinationType: bootfromvolume.DestinationLocal,
+ SourceType: bootfromvolume.SourceImage,
+ UUID: "asdfasdfasdf",
+ },
+ {
+ BootIndex: 1,
+ DeleteOnTermination: true,
+ DestinationType: bootfromvolume.DestinationVolume,
+ SourceType: bootfromvolume.SourceVolume,
+ UUID: "123456",
+ VolumeSize: 1,
+ },
+ },
+ }
+
+ expected := `
+ {
+ "server": {
+ "name": "createdserver",
+ "imageRef": "asdfasdfasdf",
+ "flavorRef": "performance1-1",
+ "block_device_mapping_v2":[
+ {
+ "boot_index": 0,
+ "delete_on_termination": true,
+ "destination_type":"local",
+ "source_type":"image",
+ "uuid":"asdfasdfasdf"
+ },
+ {
+ "boot_index": 1,
+ "delete_on_termination": true,
+ "destination_type":"volume",
+ "source_type":"volume",
+ "uuid":"123456",
+ "volume_size": 1
+ }
+ ]
+ }
+ }
+ `
+ actual, err := ext.ToServerCreateMap()
+ th.AssertNoErr(t, err)
+ th.CheckJSONEquals(t, expected, actual)
+}