diff --git a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go index 28803da70b30..862b428f9686 100644 --- a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go +++ b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go @@ -191,17 +191,17 @@ func resourceComputeInstanceV2() *schema.Resource { ForceNew: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "uuid": &schema.Schema{ + "source_type": &schema.Schema{ Type: schema.TypeString, Required: true, }, - "source_type": &schema.Schema{ + "uuid": &schema.Schema{ Type: schema.TypeString, - Required: true, + Optional: true, }, "volume_size": &schema.Schema{ Type: schema.TypeInt, - Required: true, + Optional: true, }, "destination_type": &schema.Schema{ Type: schema.TypeString, @@ -216,6 +216,10 @@ func resourceComputeInstanceV2() *schema.Resource { Optional: true, Default: false, }, + "guest_format": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, }, }, }, @@ -330,13 +334,18 @@ func resourceComputeInstanceV2Create(d *schema.ResourceData, meta interface{}) e return err } - // determine if volume/block_device configuration is correct + // determine if volume configuration is correct // this includes ensuring volume_ids are set - // and if only one block_device was specified. if err := checkVolumeConfig(d); err != nil { return err } + // determine if block_device configuration is correct + // this includes valid combinations and required attributes + if err := checkBlockDeviceConfig(d); err != nil { + return err + } + // check if floating IP configuration is correct if err := checkInstanceFloatingIPs(d); err != nil { return err @@ -380,14 +389,10 @@ func resourceComputeInstanceV2Create(d *schema.ResourceData, meta interface{}) e } if vL, ok := d.GetOk("block_device"); ok { - for _, v := range vL.([]interface{}) { - blockDeviceRaw := v.(map[string]interface{}) - blockDevice := resourceInstanceBlockDeviceV2(d, blockDeviceRaw) - createOpts = &bootfromvolume.CreateOptsExt{ - CreateOptsBuilder: createOpts, - BlockDevice: blockDevice, - } - log.Printf("[DEBUG] Create BFV Options: %+v", createOpts) + blockDevices := resourceInstanceBlockDevicesV2(d, vL.([]interface{})) + createOpts = &bootfromvolume.CreateOptsExt{ + createOpts, + blockDevices, } } @@ -1091,20 +1096,24 @@ func resourceInstanceMetadataV2(d *schema.ResourceData) map[string]string { return m } -func resourceInstanceBlockDeviceV2(d *schema.ResourceData, bd map[string]interface{}) []bootfromvolume.BlockDevice { - sourceType := bootfromvolume.SourceType(bd["source_type"].(string)) - bfvOpts := []bootfromvolume.BlockDevice{ - bootfromvolume.BlockDevice{ - UUID: bd["uuid"].(string), +func resourceInstanceBlockDevicesV2(d *schema.ResourceData, bds []interface{}) []bootfromvolume.BlockDevice { + blockDeviceOpts := make([]bootfromvolume.BlockDevice, len(bds)) + for i, bd := range bds { + bdM := bd.(map[string]interface{}) + sourceType := bootfromvolume.SourceType(bdM["source_type"].(string)) + blockDeviceOpts[i] = bootfromvolume.BlockDevice{ + UUID: bdM["uuid"].(string), SourceType: sourceType, - VolumeSize: bd["volume_size"].(int), - DestinationType: bd["destination_type"].(string), - BootIndex: bd["boot_index"].(int), - DeleteOnTermination: bd["delete_on_termination"].(bool), - }, + VolumeSize: bdM["volume_size"].(int), + DestinationType: bdM["destination_type"].(string), + BootIndex: bdM["boot_index"].(int), + DeleteOnTermination: bdM["delete_on_termination"].(bool), + GuestFormat: bdM["guest_format"].(string), + } } - return bfvOpts + log.Printf("[DEBUG] Block Device Options: %+v", blockDeviceOpts) + return blockDeviceOpts } func resourceInstanceSchedulerHintsV2(d *schema.ResourceData, schedulerHintsRaw map[string]interface{}) schedulerhints.SchedulerHints { @@ -1142,10 +1151,19 @@ func resourceInstanceSchedulerHintsV2(d *schema.ResourceData, schedulerHintsRaw } func getImageIDFromConfig(computeClient *gophercloud.ServiceClient, d *schema.ResourceData) (string, error) { - // If block_device was used, an Image does not need to be specified. - // If an Image was specified, ignore it - if _, ok := d.GetOk("block_device"); ok { - return "", nil + // If block_device was used, an Image does not need to be specified, unless an image/local + // combination was used. This emulates normal boot behavior. Otherwise, ignore the image altogether. + if vL, ok := d.GetOk("block_device"); ok { + needImage := false + for _, v := range vL.([]interface{}) { + vM := v.(map[string]interface{}) + if vM["source_type"] == "image" && vM["destination_type"] == "local" { + needImage = true + } + } + if !needImage { + return "", nil + } } if imageId := d.Get("image_id").(string); imageId != "" { @@ -1177,11 +1195,20 @@ func getImageIDFromConfig(computeClient *gophercloud.ServiceClient, d *schema.Re } func setImageInformation(computeClient *gophercloud.ServiceClient, server *servers.Server, d *schema.ResourceData) error { - // If block_device was used, an Image does not need to be specified. - // If an Image was specified, ignore it - if _, ok := d.GetOk("block_device"); ok { - d.Set("image_id", "Attempt to boot from volume - no image supplied") - return nil + // If block_device was used, an Image does not need to be specified, unless an image/local + // combination was used. This emulates normal boot behavior. Otherwise, ignore the image altogether. + if vL, ok := d.GetOk("block_device"); ok { + needImage := false + for _, v := range vL.([]interface{}) { + vM := v.(map[string]interface{}) + if vM["source_type"] == "image" && vM["destination_type"] == "local" { + needImage = true + } + } + if !needImage { + d.Set("image_id", "Attempt to boot from volume - no image supplied") + return nil + } } imageId := server.Image["id"].(string) @@ -1394,9 +1421,29 @@ func checkVolumeConfig(d *schema.ResourceData) error { } } + return nil +} + +func checkBlockDeviceConfig(d *schema.ResourceData) error { if vL, ok := d.GetOk("block_device"); ok { - if len(vL.([]interface{})) > 1 { - return fmt.Errorf("Can only specify one block device to boot from.") + for _, v := range vL.([]interface{}) { + vM := v.(map[string]interface{}) + + if vM["source_type"] != "blank" && vM["uuid"] == "" { + return fmt.Errorf("You must specify a uuid for %s block device types", vM["source_type"]) + } + + if vM["source_type"] == "image" && vM["destination_type"] == "volume" { + if vM["volume_size"] == 0 { + return fmt.Errorf("You must specify a volume_size when creating a volume from an image") + } + } + + if vM["source_type"] == "blank" && vM["destination_type"] == "local" { + if vM["volume_size"] == 0 { + return fmt.Errorf("You must specify a volume_size when creating a blank block device") + } + } } } diff --git a/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go b/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go index 77f970f853bf..1627693ba351 100644 --- a/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go +++ b/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go @@ -403,7 +403,6 @@ func TestAccComputeV2Instance_bootFromVolumeVolume(t *testing.T) { block_device { uuid = "${openstack_blockstorage_volume_v1.foo.id}" source_type = "volume" - volume_size = 5 boot_index = 0 destination_type = "volume" delete_on_termination = true @@ -459,6 +458,51 @@ func TestAccComputeV2Instance_personality(t *testing.T) { }) } +func TestAccComputeV2Instance_multiEphemeral(t *testing.T) { + var instance servers.Server + var testAccComputeV2Instance_multiEphemeral = fmt.Sprintf(` + resource "openstack_compute_instance_v2" "foo" { + name = "terraform-test" + security_groups = ["default"] + block_device { + boot_index = 0 + delete_on_termination = true + destination_type = "local" + source_type = "image" + uuid = "%s" + } + block_device { + boot_index = -1 + delete_on_termination = true + destination_type = "local" + source_type = "blank" + volume_size = 1 + } + block_device { + boot_index = -1 + delete_on_termination = true + destination_type = "local" + source_type = "blank" + volume_size = 1 + } + }`, + os.Getenv("OS_IMAGE_ID")) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeV2InstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeV2Instance_multiEphemeral, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeV2InstanceExists(t, "openstack_compute_instance_v2.foo", &instance), + ), + }, + }, + }) +} + func testAccCheckComputeV2InstanceDestroy(s *terraform.State) error { config := testAccProvider.Meta().(*Config) computeClient, err := config.computeV2Client(OS_REGION_NAME) diff --git a/website/source/docs/providers/openstack/r/compute_instance_v2.html.markdown b/website/source/docs/providers/openstack/r/compute_instance_v2.html.markdown index 2b8dc1b55f4b..ac855aa305f0 100644 --- a/website/source/docs/providers/openstack/r/compute_instance_v2.html.markdown +++ b/website/source/docs/providers/openstack/r/compute_instance_v2.html.markdown @@ -82,6 +82,8 @@ The following arguments are supported: * `block_device` - (Optional) The object for booting by volume. The block_device object structure is documented below. Changing this creates a new server. + You can specify multiple block devices which will create an instance with + multiple ephemeral (local) disks. * `volume` - (Optional) Attach an existing volume to the instance. The volume structure is described below. @@ -121,7 +123,9 @@ The `block_device` block supports: * `source_type` - (Required) The source type of the device. Must be one of "image", "volume", or "snapshot". -* `volume_size` - (Required) The size of the volume to create (in gigabytes). +* `volume_size` - The size of the volume to create (in gigabytes). Required + in the following combinations: source=image and destination=volume, + source=blank and destination=local. * `boot_index` - (Optional) The boot index of the volume. It defaults to 0. @@ -187,6 +191,8 @@ The following attributes are exported: ## Notes +### Floating IPs + Floating IPs can be associated in one of two ways: * You can specify a Floating IP address by using the top-level `floating_ip` @@ -199,3 +205,44 @@ defined in the `network` block. Each `network` block can have its own floating IP address. Only one of the above methods can be used. + +### Multiple Ephemeral Disks + +It's possible to specify multiple `block_device` entries to create an instance +with multiple ephemeral (local) disks. In order to create multiple ephemeral +disks, the sum of the total amount of ephemeral space must be less than or +equal to what the chosen flavor supports. + +The following example shows how to create an instance with multiple ephemeral +disks: + +``` +resource "openstack_compute_instance_v2" "foo" { + name = "terraform-test" + security_groups = ["default"] + + block_device { + boot_index = 0 + delete_on_termination = true + destination_type = "local" + source_type = "image" + uuid = "" + } + + block_device { + boot_index = -1 + delete_on_termination = true + destination_type = "local" + source_type = "blank" + volume_size = 1 + } + + block_device { + boot_index = -1 + delete_on_termination = true + destination_type = "local" + source_type = "blank" + volume_size = 1 + } +} +```