Skip to content

Commit

Permalink
Enable import instance (gophercloud#768)
Browse files Browse the repository at this point in the history
* Enable import instance
Since openstack does not keep track of NIC ordering, the user
must specify network in the order of the imported state.

* Add a test case and documentation.
  • Loading branch information
jsm222 authored and ozerovandrei committed Jul 26, 2019
1 parent 3a2a39e commit ef78348
Show file tree
Hide file tree
Showing 3 changed files with 307 additions and 14 deletions.
78 changes: 78 additions & 0 deletions openstack/import_openstack_compute_instance_v2_test.go
Original file line number Diff line number Diff line change
@@ -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",
},
},
},
})
}
105 changes: 91 additions & 14 deletions openstack/resource_openstack_compute_instance_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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),
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}

Expand Down
138 changes: 138 additions & 0 deletions website/docs/r/compute_instance_v2.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<flavor_id>"
key_pair = "<keyname>"
security_groups = ["default"]
image_id = "<image_id>"
network {
name = "<network_name>"
}
}
```
Then you execute
```
terraform import openstack_compute_instance_v2.basic_instance <instance_id>
```

### 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 = "<flavor_id"
key_pair = "<keyname>"
image_id = <image_id>
security_groups = ["default"]
network {
name = "<network1>"
}
network {
name = "<netowork2>"
}
network {
name = "<network1>"
fixed_ip_v4 = "<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 = "<image_id>"
flavor_id = "<flavor_id>"
key_pair = "<keyname>"
security_groups = ["default"]
block_device {
uuid = <image_id>"
source_type = "image"
destination_type = "volume"
boot_index = 0
delete_on_termination = true
}
network {
name = "<network_name>"
}
}
resource "openstack_blockstorage_volume_v2" "volume_1" {
size = 1
name = "<vol_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 <instance_id>
import openstack_blockstorage_volume_v2.volume_1 <volume_id>
terraform import openstack_compute_volume_attach_v2.va_1
<instance_id>/<volume_id>
```

* 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.

0 comments on commit ef78348

Please sign in to comment.