diff --git a/openstack/import_openstack_compute_instance_v2_test.go b/openstack/import_openstack_compute_instance_v2_test.go new file mode 100644 index 0000000000..612974cce4 --- /dev/null +++ b/openstack/import_openstack_compute_instance_v2_test.go @@ -0,0 +1,78 @@ +package openstack + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccComputeV2Instance_importBasic(t *testing.T) { + resourceName := "openstack_compute_instance_v2.instance_1" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeV2InstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeV2Instance_basic, + }, + + resource.TestStep{ + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "stop_before_destroy", + "force_delete", + }, + }, + }, + }) +} +func TestAccComputeV2Instance_importbootFromVolumeForceNew_1(t *testing.T) { + resourceName := "openstack_compute_instance_v2.instance_1" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeV2InstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeV2Instance_bootFromVolumeForceNew_1, + }, + resource.TestStep{ + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "stop_before_destroy", + "force_delete", + }, + }, + }, + }) +} +func TestAccComputeV2Instance_importbootFromVolumeImage(t *testing.T) { + resourceName := "openstack_compute_instance_v2.instance_1" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeV2InstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeV2Instance_bootFromVolumeImage, + }, + resource.TestStep{ + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "stop_before_destroy", + "force_delete", + }, + }, + }, + }) +} diff --git a/openstack/resource_openstack_compute_instance_v2.go b/openstack/resource_openstack_compute_instance_v2.go index 31075072a4..d35a2223c9 100644 --- a/openstack/resource_openstack_compute_instance_v2.go +++ b/openstack/resource_openstack_compute_instance_v2.go @@ -11,6 +11,7 @@ import ( "time" "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs" @@ -33,6 +34,9 @@ func resourceComputeInstanceV2() *schema.Resource { Update: resourceComputeInstanceV2Update, Delete: resourceComputeInstanceV2Delete, + Importer: &schema.ResourceImporter{ + State: resourceOpenStackComputeInstanceV2ImportState, + }, Timeouts: &schema.ResourceTimeout{ Create: schema.DefaultTimeout(30 * time.Minute), Update: schema.DefaultTimeout(30 * time.Minute), @@ -608,6 +612,7 @@ func resourceComputeInstanceV2Read(d *schema.ResourceData, meta interface{}) err } d.Set("flavor_id", flavorId) + d.Set("key_pair", server.KeyName) flavor, err := flavors.Get(computeClient, flavorId).Extract() if err != nil { return err @@ -630,7 +635,6 @@ func resourceComputeInstanceV2Read(d *schema.ResourceData, meta interface{}) err if err != nil { return CheckDeleted(d, err, "server") } - // Set the availability zone d.Set("availability_zone", serverWithAZ.AvailabilityZone) @@ -945,6 +949,77 @@ func resourceComputeInstanceV2Delete(d *schema.ResourceData, meta interface{}) e return nil } +func resourceOpenStackComputeInstanceV2ImportState(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + + var serverWithAttachments struct { + VolumesAttached []map[string]interface{} `json:"os-extended-volumes:volumes_attached"` + } + + config := meta.(*Config) + computeClient, err := config.computeV2Client(GetRegion(d, config)) + if err != nil { + return nil, fmt.Errorf("Error creating OpenStack compute client: %s", err) + } + + results := make([]*schema.ResourceData, 1) + err = resourceComputeInstanceV2Read(d, meta) + if err != nil { + return nil, fmt.Errorf("Error reading openstack_compute_instance_v2 %s: %s", d.Id(), err) + } + + raw := servers.Get(computeClient, d.Id()) + if raw.Err != nil { + return nil, CheckDeleted(d, raw.Err, "openstack_compute_instance_v2") + } + + raw.ExtractInto(&serverWithAttachments) + + log.Printf("[DEBUG] Retrieved openstack_compute_instance_v2 %s volume attachments: %#v", + d.Id(), serverWithAttachments) + + bds := []map[string]interface{}{} + if len(serverWithAttachments.VolumesAttached) > 0 { + blockStorageClient, err := config.blockStorageV2Client(GetRegion(d, config)) + if err != nil { + return nil, fmt.Errorf("Error creating OpenStack volume client: %s", err) + } + + var volMetaData = struct { + VolumeImageMetadata map[string]interface{} `json:"volume_image_metadata"` + Id string `json:"id"` + Size int `json:"size"` + Bootable string `json:"bootable"` + }{} + for i, b := range serverWithAttachments.VolumesAttached { + rawVolume := volumes.Get(blockStorageClient, b["id"].(string)) + rawVolume.ExtractInto(&volMetaData) + + log.Printf("[DEBUG] retrieved volume%+v", volMetaData) + v := map[string]interface{}{ + "delete_on_termination": true, + "uuid": volMetaData.VolumeImageMetadata["image_id"], + "boot_index": i, + "destination_type": "volume", + "source_type": "image", + "volume_size": volMetaData.Size, + "disk_bus": "", + "device_type": "", + } + + if volMetaData.Bootable == "true" { + bds = append(bds, v) + } + } + + d.Set("block_device", bds) + } + metadata, err := servers.Metadata(computeClient, d.Id()).Extract() + d.Set("metadata", metadata) + results[0] = d + + return results, nil +} + // ServerV2StateRefreshFunc returns a resource.StateRefreshFunc that is used to watch // an OpenStack instance. func ServerV2StateRefreshFunc(client *gophercloud.ServiceClient, instanceID string) resource.StateRefreshFunc { @@ -1117,20 +1192,22 @@ func setImageInformation(computeClient *gophercloud.ServiceClient, server *serve } } - imageId := server.Image["id"].(string) - if imageId != "" { - d.Set("image_id", imageId) - if image, err := images.Get(computeClient, imageId).Extract(); err != nil { - if _, ok := err.(gophercloud.ErrDefault404); ok { - // If the image name can't be found, set the value to "Image not found". - // The most likely scenario is that the image no longer exists in the Image Service - // but the instance still has a record from when it existed. - d.Set("image_name", "Image not found") - return nil + if server.Image["id"] != nil { + imageId := server.Image["id"].(string) + if imageId != "" { + d.Set("image_id", imageId) + if image, err := images.Get(computeClient, imageId).Extract(); err != nil { + if _, ok := err.(gophercloud.ErrDefault404); ok { + // If the image name can't be found, set the value to "Image not found". + // The most likely scenario is that the image no longer exists in the Image Service + // but the instance still has a record from when it existed. + d.Set("image_name", "Image not found") + return nil + } + return err + } else { + d.Set("image_name", image.Name) } - return err - } else { - d.Set("image_name", image.Name) } } diff --git a/website/docs/r/compute_instance_v2.html.markdown b/website/docs/r/compute_instance_v2.html.markdown index eb8f29450b..bd2e20472e 100644 --- a/website/docs/r/compute_instance_v2.html.markdown +++ b/website/docs/r/compute_instance_v2.html.markdown @@ -619,3 +619,141 @@ Expected HTTP response code [201 202] when accessing [POST https://example.com:8 you still need to make sure one of the above points is satisfied. An instance cannot be created without a valid network configuration even if you intend to use `openstack_compute_interface_attach_v2` after the instance has been created. + +## Importing instances + +Importing instances can be tricky, since the nova api does not offer all +information provided at creation time for later retrieval. +Network interface attachment order, and number and sizes of ephemeral +disks are examples of this. + +### Importing basic instance +Assume you want to import an instance with one ephemeral root disk, +and one network interface. + +Your configuration would look like the following: + +```hcl +resource "openstack_compute_instance_v2" "basic_instance" { + name = "basic" + flavor_id = "" + key_pair = "" + security_groups = ["default"] + image_id = "" + + network { + name = "" + } +} + +``` +Then you execute +``` +terraform import openstack_compute_instance_v2.basic_instance +``` + +### Importing an instance with multiple emphemeral disks + +The importer cannot read the emphemeral disk configuration +of an instance, so just specify image_id as in the configuration +of the basic instance example. + +### Importing instance with multiple network interfaces. + +Nova returns the network interfaces grouped by network, thus not in creation +order. +That means that if you have multiple network interfaces you must take +care of the order of networks in your configuration. + + +As example we want to import an instance with one ephemeral root disk, +and 3 network interfaces. + +Examples +```hcl +resource "openstack_compute_instance_v2" "boot-from-volume" { + name = "boot-from-volume" + flavor_id = " + security_groups = ["default"] + + network { + name = "" + } + network { + name = "" + } + network { + name = "" + fixed_ip_v4 = "" + } + +} +``` + +In the above configuration the networks are out of order compared to what nova +and thus the import code returns, which means the plan will not +be empty after import. + +So either with care check the plan and modify configuration, or read the +network order in the state file after import and modify your +configuration accordingly. + + * A note on ports. If you have created a neutron port independent of an + instance, then the import code has no way to detect that the port is created + idenpendently, and therefore on deletion of imported instances you might have + port resources in your project, which you expected to be created by the + instance and thus to also be deleted with the instance. + + +### Importing instances with multiple block storage volumes. + +We have an instance with two block storage volumes, one bootable and one +non-bootable. +Note that we only configure the bootable device as block_device. +The other volumes can be specified as `openstack_blockstorage_volume_v2` +```hcl +resource "openstack_compute_instance_v2" "instance_2" { + name = "instance_2" + image_id = "" + flavor_id = "" + key_pair = "" + security_groups = ["default"] + + block_device { + uuid = " + source_type = "image" + destination_type = "volume" + boot_index = 0 + delete_on_termination = true + } + + network { + name = "" + } +} +resource "openstack_blockstorage_volume_v2" "volume_1" { + size = 1 + name = "" +} +resource "openstack_compute_volume_attach_v2" "va_1" { + volume_id = "${openstack_blockstorage_volume_v2.volume_1.id}" + instance_id = "${openstack_compute_instance_v2.instance_2.id}" +} +``` +To import the instance outlined in the above configuration +do the following: + +``` +terraform import openstack_compute_instance_v2.instance_2 +import openstack_blockstorage_volume_v2.volume_1 +terraform import openstack_compute_volume_attach_v2.va_1 +/ +``` + +* A note on block storage volumes, the importer does not read + delete_on_termination flag, and always assumes true. If you + import an instance created with delete_on_termination false, + you end up with "orphaned" volumes after destruction of + instances.