Allow ImageRef to be empty when block device is present (#97)

This commit removes the requirement for ImageRef to be set when creating
a server. This is to enable booting from a volume to work properly.

A unit test was added to verify this is possible.

Acceptance tests were also modified to handle this.
diff --git a/acceptance/openstack/compute/v2/compute.go b/acceptance/openstack/compute/v2/compute.go
index 42a2c10..9b284c3 100644
--- a/acceptance/openstack/compute/v2/compute.go
+++ b/acceptance/openstack/compute/v2/compute.go
@@ -85,7 +85,6 @@
 	serverCreateOpts := servers.CreateOpts{
 		Name:      name,
 		FlavorRef: choices.FlavorID,
-		ImageRef:  choices.ImageID,
 		Networks: []servers.Network{
 			servers.Network{UUID: networkID},
 		},
@@ -100,7 +99,13 @@
 		return server, err
 	}
 
-	return server, nil
+	if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
+		return server, err
+	}
+
+	newServer, err := servers.Get(client, server.ID).Extract()
+
+	return newServer, nil
 }
 
 // CreateDefaultRule will create a default security group rule with a
@@ -175,6 +180,53 @@
 	return keyPair, nil
 }
 
+// CreateMultiEphemeralServer works like CreateServer but is configured with
+// one or more block devices defined by passing in []bootfromvolume.BlockDevice.
+// These block devices act like block devices when booting from a volume but
+// are actually local ephemeral disks.
+// An error will be returned if a server was unable to be created.
+func CreateMultiEphemeralServer(t *testing.T, client *gophercloud.ServiceClient, blockDevices []bootfromvolume.BlockDevice, choices *clients.AcceptanceTestChoices) (*servers.Server, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	var server *servers.Server
+
+	networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName)
+	if err != nil {
+		return server, err
+	}
+
+	name := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create bootable volume server: %s", name)
+
+	serverCreateOpts := servers.CreateOpts{
+		Name:      name,
+		FlavorRef: choices.FlavorID,
+		ImageRef:  choices.ImageID,
+		Networks: []servers.Network{
+			servers.Network{UUID: networkID},
+		},
+	}
+
+	server, err = bootfromvolume.Create(client, bootfromvolume.CreateOptsExt{
+		serverCreateOpts,
+		blockDevices,
+	}).Extract()
+
+	if err != nil {
+		return server, err
+	}
+
+	if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
+		return server, err
+	}
+
+	newServer, err := servers.Get(client, server.ID).Extract()
+
+	return newServer, nil
+}
+
 // CreateSecurityGroup will create a security group with a random name.
 // An error will be returned if one was failed to be created.
 func CreateSecurityGroup(t *testing.T, client *gophercloud.ServiceClient) (secgroups.SecurityGroup, error) {
@@ -257,7 +309,54 @@
 		return server, err
 	}
 
-	if err = WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
+	if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
+		return server, err
+	}
+
+	return server, nil
+}
+
+// CreateServerWithoutImageRef creates a basic instance with a randomly generated name.
+// The flavor of the instance will be the value of the OS_FLAVOR_ID environment variable.
+// The image is intentionally missing to trigger an error.
+// The instance will be launched on the network specified in OS_NETWORK_NAME.
+// An error will be returned if the instance was unable to be created.
+func CreateServerWithoutImageRef(t *testing.T, client *gophercloud.ServiceClient, choices *clients.AcceptanceTestChoices) (*servers.Server, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	var server *servers.Server
+
+	networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName)
+	if err != nil {
+		return server, err
+	}
+
+	name := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create server: %s", name)
+
+	pwd := tools.MakeNewPassword("")
+
+	server, err = servers.Create(client, servers.CreateOpts{
+		Name:      name,
+		FlavorRef: choices.FlavorID,
+		AdminPass: pwd,
+		Networks: []servers.Network{
+			servers.Network{UUID: networkID},
+		},
+		Personality: servers.Personality{
+			&servers.File{
+				Path:     "/etc/test",
+				Contents: []byte("hello world"),
+			},
+		},
+	}).Extract()
+	if err != nil {
+		return server, err
+	}
+
+	if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
 		return server, err
 	}
 
@@ -356,7 +455,7 @@
 		return server, err
 	}
 
-	if err = WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
+	if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
 		return server, err
 	}
 
@@ -376,7 +475,7 @@
 		return volumeAttachment, err
 	}
 
-	if err = volumes.WaitForStatus(blockClient, volume.ID, "in-use", 60); err != nil {
+	if err := volumes.WaitForStatus(blockClient, volume.ID, "in-use", 60); err != nil {
 		return volumeAttachment, err
 	}
 
@@ -476,7 +575,7 @@
 		t.Fatalf("Unable to detach volume: %v", err)
 	}
 
-	if err = volumes.WaitForStatus(blockClient, volumeAttachment.ID, "available", 60); err != nil {
+	if err := volumes.WaitForStatus(blockClient, volumeAttachment.ID, "available", 60); err != nil {
 		t.Fatalf("Unable to wait for volume: %v", err)
 	}
 	t.Logf("Deleted volume: %s", volumeAttachment.VolumeID)