|
|
|
|
@ -6,6 +6,7 @@ import (
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log"
|
|
|
|
|
"os"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/hashicorp/terraform/helper/hashcode"
|
|
|
|
|
@ -45,18 +46,16 @@ func resourceComputeInstanceV2() *schema.Resource {
|
|
|
|
|
ForceNew: false,
|
|
|
|
|
},
|
|
|
|
|
"image_id": &schema.Schema{
|
|
|
|
|
Type: schema.TypeString,
|
|
|
|
|
Optional: true,
|
|
|
|
|
ForceNew: true,
|
|
|
|
|
Computed: true,
|
|
|
|
|
DefaultFunc: envDefaultFunc("OS_IMAGE_ID"),
|
|
|
|
|
Type: schema.TypeString,
|
|
|
|
|
Optional: true,
|
|
|
|
|
ForceNew: true,
|
|
|
|
|
Computed: true,
|
|
|
|
|
},
|
|
|
|
|
"image_name": &schema.Schema{
|
|
|
|
|
Type: schema.TypeString,
|
|
|
|
|
Optional: true,
|
|
|
|
|
ForceNew: true,
|
|
|
|
|
Computed: true,
|
|
|
|
|
DefaultFunc: envDefaultFunc("OS_IMAGE_NAME"),
|
|
|
|
|
Type: schema.TypeString,
|
|
|
|
|
Optional: true,
|
|
|
|
|
ForceNew: true,
|
|
|
|
|
Computed: true,
|
|
|
|
|
},
|
|
|
|
|
"flavor_id": &schema.Schema{
|
|
|
|
|
Type: schema.TypeString,
|
|
|
|
|
@ -176,7 +175,12 @@ func resourceComputeInstanceV2() *schema.Resource {
|
|
|
|
|
ForceNew: true,
|
|
|
|
|
},
|
|
|
|
|
"block_device": &schema.Schema{
|
|
|
|
|
Type: schema.TypeList,
|
|
|
|
|
// TODO: This is a set because we don't support singleton
|
|
|
|
|
// sub-resources today. We'll enforce that the set only ever has
|
|
|
|
|
// length zero or one below. When TF gains support for
|
|
|
|
|
// sub-resources this can be converted.
|
|
|
|
|
// As referenced in resource_aws_instance.go
|
|
|
|
|
Type: schema.TypeSet,
|
|
|
|
|
Optional: true,
|
|
|
|
|
ForceNew: true,
|
|
|
|
|
Elem: &schema.Resource{
|
|
|
|
|
@ -201,12 +205,22 @@ func resourceComputeInstanceV2() *schema.Resource {
|
|
|
|
|
Type: schema.TypeInt,
|
|
|
|
|
Optional: true,
|
|
|
|
|
},
|
|
|
|
|
"delete_on_termination": &schema.Schema{
|
|
|
|
|
Type: schema.TypeBool,
|
|
|
|
|
Optional: true,
|
|
|
|
|
Default: false,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Set: func(v interface{}) int {
|
|
|
|
|
// there can only be one bootable block device; no need to hash anything
|
|
|
|
|
return 0
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"volume": &schema.Schema{
|
|
|
|
|
Type: schema.TypeSet,
|
|
|
|
|
Optional: true,
|
|
|
|
|
Computed: true,
|
|
|
|
|
Elem: &schema.Resource{
|
|
|
|
|
Schema: map[string]*schema.Schema{
|
|
|
|
|
"id": &schema.Schema{
|
|
|
|
|
@ -215,7 +229,8 @@ func resourceComputeInstanceV2() *schema.Resource {
|
|
|
|
|
},
|
|
|
|
|
"volume_id": &schema.Schema{
|
|
|
|
|
Type: schema.TypeString,
|
|
|
|
|
Required: true,
|
|
|
|
|
Optional: true,
|
|
|
|
|
Computed: true,
|
|
|
|
|
},
|
|
|
|
|
"device": &schema.Schema{
|
|
|
|
|
Type: schema.TypeString,
|
|
|
|
|
@ -281,7 +296,11 @@ func resourceComputeInstanceV2Create(d *schema.ResourceData, meta interface{}) e
|
|
|
|
|
|
|
|
|
|
var createOpts servers.CreateOptsBuilder
|
|
|
|
|
|
|
|
|
|
imageId, err := getImageID(computeClient, d)
|
|
|
|
|
// Determines the Image ID using the following rules:
|
|
|
|
|
// If a bootable block_device was specified, ignore the image altogether.
|
|
|
|
|
// If an image_id was specified, use it.
|
|
|
|
|
// If an image_name was specified, look up the image ID, report if error.
|
|
|
|
|
imageId, err := getImageIDFromConfig(computeClient, d)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
@ -296,6 +315,13 @@ func resourceComputeInstanceV2Create(d *schema.ResourceData, meta interface{}) e
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// determine if volume/block_device 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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
networks := make([]servers.Network, len(networkDetails))
|
|
|
|
|
for i, net := range networkDetails {
|
|
|
|
|
networks[i] = servers.Network{
|
|
|
|
|
@ -325,11 +351,16 @@ func resourceComputeInstanceV2Create(d *schema.ResourceData, meta interface{}) e
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if blockDeviceRaw, ok := d.Get("block_device").(map[string]interface{}); ok && blockDeviceRaw != nil {
|
|
|
|
|
blockDevice := resourceInstanceBlockDeviceV2(d, blockDeviceRaw)
|
|
|
|
|
createOpts = &bootfromvolume.CreateOptsExt{
|
|
|
|
|
createOpts,
|
|
|
|
|
blockDevice,
|
|
|
|
|
if v, ok := d.GetOk("block_device"); ok {
|
|
|
|
|
vL := v.(*schema.Set).List()
|
|
|
|
|
for _, v := range vL {
|
|
|
|
|
blockDeviceRaw := v.(map[string]interface{})
|
|
|
|
|
blockDevice := resourceInstanceBlockDeviceV2(d, blockDeviceRaw)
|
|
|
|
|
createOpts = &bootfromvolume.CreateOptsExt{
|
|
|
|
|
createOpts,
|
|
|
|
|
blockDevice,
|
|
|
|
|
}
|
|
|
|
|
log.Printf("[DEBUG] Create BFV Options: %+v", createOpts)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -344,7 +375,16 @@ func resourceComputeInstanceV2Create(d *schema.ResourceData, meta interface{}) e
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.Printf("[DEBUG] Create Options: %#v", createOpts)
|
|
|
|
|
server, err := servers.Create(computeClient, createOpts).Extract()
|
|
|
|
|
|
|
|
|
|
// If a block_device is used, use the bootfromvolume.Create function as it allows an empty ImageRef.
|
|
|
|
|
// Otherwise, use the normal servers.Create function.
|
|
|
|
|
var server *servers.Server
|
|
|
|
|
if _, ok := d.GetOk("block_device"); ok {
|
|
|
|
|
server, err = bootfromvolume.Create(computeClient, createOpts).Extract()
|
|
|
|
|
} else {
|
|
|
|
|
server, err = servers.Create(computeClient, createOpts).Extract()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Error creating OpenStack server: %s", err)
|
|
|
|
|
}
|
|
|
|
|
@ -381,16 +421,14 @@ func resourceComputeInstanceV2Create(d *schema.ResourceData, meta interface{}) e
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// were volume attachments specified?
|
|
|
|
|
if v := d.Get("volume"); v != nil {
|
|
|
|
|
// if volumes were specified, attach them after the instance has launched.
|
|
|
|
|
if v, ok := d.GetOk("volume"); ok {
|
|
|
|
|
vols := v.(*schema.Set).List()
|
|
|
|
|
if len(vols) > 0 {
|
|
|
|
|
if blockClient, err := config.blockStorageV1Client(d.Get("region").(string)); err != nil {
|
|
|
|
|
return fmt.Errorf("Error creating OpenStack block storage client: %s", err)
|
|
|
|
|
} else {
|
|
|
|
|
if err := attachVolumesToInstance(computeClient, blockClient, d.Id(), vols); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if blockClient, err := config.blockStorageV1Client(d.Get("region").(string)); err != nil {
|
|
|
|
|
return fmt.Errorf("Error creating OpenStack block storage client: %s", err)
|
|
|
|
|
} else {
|
|
|
|
|
if err := attachVolumesToInstance(computeClient, blockClient, d.Id(), vols); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -528,34 +566,15 @@ func resourceComputeInstanceV2Read(d *schema.ResourceData, meta interface{}) err
|
|
|
|
|
}
|
|
|
|
|
d.Set("flavor_name", flavor.Name)
|
|
|
|
|
|
|
|
|
|
imageId, ok := server.Image["id"].(string)
|
|
|
|
|
if !ok {
|
|
|
|
|
return fmt.Errorf("Error setting OpenStack server's image: %v", server.Image)
|
|
|
|
|
}
|
|
|
|
|
d.Set("image_id", imageId)
|
|
|
|
|
|
|
|
|
|
image, err := images.Get(computeClient, imageId).Extract()
|
|
|
|
|
if err != nil {
|
|
|
|
|
// Set the instance's image information appropriately
|
|
|
|
|
if err := setImageInformation(computeClient, server, d); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
d.Set("image_name", image.Name)
|
|
|
|
|
|
|
|
|
|
// volume attachments
|
|
|
|
|
vas, err := getVolumeAttachments(computeClient, d.Id())
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err := getVolumeAttachments(computeClient, d); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if len(vas) > 0 {
|
|
|
|
|
attachments := make([]map[string]interface{}, len(vas))
|
|
|
|
|
for i, attachment := range vas {
|
|
|
|
|
attachments[i] = make(map[string]interface{})
|
|
|
|
|
attachments[i]["id"] = attachment.ID
|
|
|
|
|
attachments[i]["volume_id"] = attachment.VolumeID
|
|
|
|
|
attachments[i]["device"] = attachment.Device
|
|
|
|
|
}
|
|
|
|
|
log.Printf("[INFO] Volume attachments: %v", attachments)
|
|
|
|
|
d.Set("volume", attachments)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
@ -665,30 +684,31 @@ func resourceComputeInstanceV2Update(d *schema.ResourceData, meta interface{}) e
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if d.HasChange("volume") {
|
|
|
|
|
// ensure the volume configuration is correct
|
|
|
|
|
if err := checkVolumeConfig(d); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// old attachments and new attachments
|
|
|
|
|
oldAttachments, newAttachments := d.GetChange("volume")
|
|
|
|
|
|
|
|
|
|
// for each old attachment, detach the volume
|
|
|
|
|
oldAttachmentSet := oldAttachments.(*schema.Set).List()
|
|
|
|
|
if len(oldAttachmentSet) > 0 {
|
|
|
|
|
if blockClient, err := config.blockStorageV1Client(d.Get("region").(string)); err != nil {
|
|
|
|
|
if blockClient, err := config.blockStorageV1Client(d.Get("region").(string)); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
} else {
|
|
|
|
|
if err := detachVolumesFromInstance(computeClient, blockClient, d.Id(), oldAttachmentSet); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
} else {
|
|
|
|
|
if err := detachVolumesFromInstance(computeClient, blockClient, d.Id(), oldAttachmentSet); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// for each new attachment, attach the volume
|
|
|
|
|
newAttachmentSet := newAttachments.(*schema.Set).List()
|
|
|
|
|
if len(newAttachmentSet) > 0 {
|
|
|
|
|
if blockClient, err := config.blockStorageV1Client(d.Get("region").(string)); err != nil {
|
|
|
|
|
if blockClient, err := config.blockStorageV1Client(d.Get("region").(string)); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
} else {
|
|
|
|
|
if err := attachVolumesToInstance(computeClient, blockClient, d.Id(), newAttachmentSet); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
} else {
|
|
|
|
|
if err := attachVolumesToInstance(computeClient, blockClient, d.Id(), newAttachmentSet); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -919,11 +939,12 @@ func resourceInstanceBlockDeviceV2(d *schema.ResourceData, bd map[string]interfa
|
|
|
|
|
sourceType := bootfromvolume.SourceType(bd["source_type"].(string))
|
|
|
|
|
bfvOpts := []bootfromvolume.BlockDevice{
|
|
|
|
|
bootfromvolume.BlockDevice{
|
|
|
|
|
UUID: bd["uuid"].(string),
|
|
|
|
|
SourceType: sourceType,
|
|
|
|
|
VolumeSize: bd["volume_size"].(int),
|
|
|
|
|
DestinationType: bd["destination_type"].(string),
|
|
|
|
|
BootIndex: bd["boot_index"].(int),
|
|
|
|
|
UUID: bd["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),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -964,44 +985,72 @@ func resourceInstanceSchedulerHintsV2(d *schema.ResourceData, schedulerHintsRaw
|
|
|
|
|
return schedulerHints
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getImageID(client *gophercloud.ServiceClient, d *schema.ResourceData) (string, error) {
|
|
|
|
|
imageId := d.Get("image_id").(string)
|
|
|
|
|
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 imageId != "" {
|
|
|
|
|
if imageId := d.Get("image_id").(string); imageId != "" {
|
|
|
|
|
return imageId, nil
|
|
|
|
|
} else {
|
|
|
|
|
// try the OS_IMAGE_ID environment variable
|
|
|
|
|
if v := os.Getenv("OS_IMAGE_ID"); v != "" {
|
|
|
|
|
return v, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
imageCount := 0
|
|
|
|
|
imageName := d.Get("image_name").(string)
|
|
|
|
|
if imageName == "" {
|
|
|
|
|
// try the OS_IMAGE_NAME environment variable
|
|
|
|
|
if v := os.Getenv("OS_IMAGE_NAME"); v != "" {
|
|
|
|
|
imageName = v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if imageName != "" {
|
|
|
|
|
pager := images.ListDetail(client, &images.ListOpts{
|
|
|
|
|
Name: imageName,
|
|
|
|
|
})
|
|
|
|
|
pager.EachPage(func(page pagination.Page) (bool, error) {
|
|
|
|
|
imageList, err := images.ExtractImages(page)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
|
imageId, err := images.IDFromName(computeClient, imageName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return imageId, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, i := range imageList {
|
|
|
|
|
if i.Name == imageName {
|
|
|
|
|
imageCount++
|
|
|
|
|
imageId = i.ID
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true, nil
|
|
|
|
|
})
|
|
|
|
|
return "", fmt.Errorf("Neither a boot device, image ID, or image name were able to be determined.")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch imageCount {
|
|
|
|
|
case 0:
|
|
|
|
|
return "", fmt.Errorf("Unable to find image: %s", imageName)
|
|
|
|
|
case 1:
|
|
|
|
|
return imageId, nil
|
|
|
|
|
default:
|
|
|
|
|
return "", fmt.Errorf("Found %d images matching %s", imageCount, imageName)
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
imageId := server.Image["id"].(string)
|
|
|
|
|
if imageId != "" {
|
|
|
|
|
d.Set("image_id", imageId)
|
|
|
|
|
if image, err := images.Get(computeClient, imageId).Extract(); err != nil {
|
|
|
|
|
errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError)
|
|
|
|
|
if !ok {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if errCode.Actual == 404 {
|
|
|
|
|
// 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
|
|
|
|
|
} else {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
d.Set("image_name", image.Name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return "", fmt.Errorf("Neither an image ID nor an image name were able to be determined.")
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getFlavorID(client *gophercloud.ServiceClient, d *schema.ResourceData) (string, error) {
|
|
|
|
|
@ -1046,6 +1095,7 @@ func resourceComputeVolumeAttachmentHash(v interface{}) int {
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
m := v.(map[string]interface{})
|
|
|
|
|
buf.WriteString(fmt.Sprintf("%s-", m["volume_id"].(string)))
|
|
|
|
|
|
|
|
|
|
return hashcode.String(buf.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -1073,81 +1123,78 @@ func resourceComputeSchedulerHintsHash(v interface{}) int {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func attachVolumesToInstance(computeClient *gophercloud.ServiceClient, blockClient *gophercloud.ServiceClient, serverId string, vols []interface{}) error {
|
|
|
|
|
if len(vols) > 0 {
|
|
|
|
|
for _, v := range vols {
|
|
|
|
|
va := v.(map[string]interface{})
|
|
|
|
|
volumeId := va["volume_id"].(string)
|
|
|
|
|
device := va["device"].(string)
|
|
|
|
|
|
|
|
|
|
s := ""
|
|
|
|
|
if serverId != "" {
|
|
|
|
|
s = serverId
|
|
|
|
|
} else if va["server_id"] != "" {
|
|
|
|
|
s = va["server_id"].(string)
|
|
|
|
|
} else {
|
|
|
|
|
return fmt.Errorf("Unable to determine server ID to attach volume.")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vaOpts := &volumeattach.CreateOpts{
|
|
|
|
|
Device: device,
|
|
|
|
|
VolumeID: volumeId,
|
|
|
|
|
}
|
|
|
|
|
for _, v := range vols {
|
|
|
|
|
va := v.(map[string]interface{})
|
|
|
|
|
volumeId := va["volume_id"].(string)
|
|
|
|
|
device := va["device"].(string)
|
|
|
|
|
|
|
|
|
|
s := ""
|
|
|
|
|
if serverId != "" {
|
|
|
|
|
s = serverId
|
|
|
|
|
} else if va["server_id"] != "" {
|
|
|
|
|
s = va["server_id"].(string)
|
|
|
|
|
} else {
|
|
|
|
|
return fmt.Errorf("Unable to determine server ID to attach volume.")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, err := volumeattach.Create(computeClient, s, vaOpts).Extract(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
vaOpts := &volumeattach.CreateOpts{
|
|
|
|
|
Device: device,
|
|
|
|
|
VolumeID: volumeId,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stateConf := &resource.StateChangeConf{
|
|
|
|
|
Pending: []string{"attaching", "available"},
|
|
|
|
|
Target: "in-use",
|
|
|
|
|
Refresh: VolumeV1StateRefreshFunc(blockClient, va["volume_id"].(string)),
|
|
|
|
|
Timeout: 30 * time.Minute,
|
|
|
|
|
Delay: 5 * time.Second,
|
|
|
|
|
MinTimeout: 2 * time.Second,
|
|
|
|
|
}
|
|
|
|
|
if _, err := volumeattach.Create(computeClient, s, vaOpts).Extract(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, err := stateConf.WaitForState(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
stateConf := &resource.StateChangeConf{
|
|
|
|
|
Pending: []string{"attaching", "available"},
|
|
|
|
|
Target: "in-use",
|
|
|
|
|
Refresh: VolumeV1StateRefreshFunc(blockClient, va["volume_id"].(string)),
|
|
|
|
|
Timeout: 30 * time.Minute,
|
|
|
|
|
Delay: 5 * time.Second,
|
|
|
|
|
MinTimeout: 2 * time.Second,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.Printf("[INFO] Attached volume %s to instance %s", volumeId, serverId)
|
|
|
|
|
if _, err := stateConf.WaitForState(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.Printf("[INFO] Attached volume %s to instance %s", volumeId, serverId)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func detachVolumesFromInstance(computeClient *gophercloud.ServiceClient, blockClient *gophercloud.ServiceClient, serverId string, vols []interface{}) error {
|
|
|
|
|
if len(vols) > 0 {
|
|
|
|
|
for _, v := range vols {
|
|
|
|
|
va := v.(map[string]interface{})
|
|
|
|
|
aId := va["id"].(string)
|
|
|
|
|
for _, v := range vols {
|
|
|
|
|
va := v.(map[string]interface{})
|
|
|
|
|
aId := va["id"].(string)
|
|
|
|
|
|
|
|
|
|
if err := volumeattach.Delete(computeClient, serverId, aId).ExtractErr(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if err := volumeattach.Delete(computeClient, serverId, aId).ExtractErr(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stateConf := &resource.StateChangeConf{
|
|
|
|
|
Pending: []string{"detaching", "in-use"},
|
|
|
|
|
Target: "available",
|
|
|
|
|
Refresh: VolumeV1StateRefreshFunc(blockClient, va["volume_id"].(string)),
|
|
|
|
|
Timeout: 30 * time.Minute,
|
|
|
|
|
Delay: 5 * time.Second,
|
|
|
|
|
MinTimeout: 2 * time.Second,
|
|
|
|
|
}
|
|
|
|
|
stateConf := &resource.StateChangeConf{
|
|
|
|
|
Pending: []string{"detaching", "in-use"},
|
|
|
|
|
Target: "available",
|
|
|
|
|
Refresh: VolumeV1StateRefreshFunc(blockClient, va["volume_id"].(string)),
|
|
|
|
|
Timeout: 30 * time.Minute,
|
|
|
|
|
Delay: 5 * time.Second,
|
|
|
|
|
MinTimeout: 2 * time.Second,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, err := stateConf.WaitForState(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
log.Printf("[INFO] Detached volume %s from instance %s", va["volume_id"], serverId)
|
|
|
|
|
if _, err := stateConf.WaitForState(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
log.Printf("[INFO] Detached volume %s from instance %s", va["volume_id"], serverId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getVolumeAttachments(computeClient *gophercloud.ServiceClient, serverId string) ([]volumeattach.VolumeAttachment, error) {
|
|
|
|
|
func getVolumeAttachments(computeClient *gophercloud.ServiceClient, d *schema.ResourceData) error {
|
|
|
|
|
var attachments []volumeattach.VolumeAttachment
|
|
|
|
|
err := volumeattach.List(computeClient, serverId).EachPage(func(page pagination.Page) (bool, error) {
|
|
|
|
|
|
|
|
|
|
err := volumeattach.List(computeClient, d.Id()).EachPage(func(page pagination.Page) (bool, error) {
|
|
|
|
|
actual, err := volumeattach.ExtractVolumeAttachments(page)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
@ -1158,8 +1205,45 @@ func getVolumeAttachments(computeClient *gophercloud.ServiceClient, serverId str
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vols := make([]map[string]interface{}, len(attachments))
|
|
|
|
|
for i, attachment := range attachments {
|
|
|
|
|
vols[i] = make(map[string]interface{})
|
|
|
|
|
vols[i]["id"] = attachment.ID
|
|
|
|
|
vols[i]["volume_id"] = attachment.VolumeID
|
|
|
|
|
vols[i]["device"] = attachment.Device
|
|
|
|
|
}
|
|
|
|
|
log.Printf("[INFO] Volume attachments: %v", vols)
|
|
|
|
|
d.Set("volume", vols)
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func checkVolumeConfig(d *schema.ResourceData) error {
|
|
|
|
|
// Although a volume_id is required to attach a volume, in order to be able to report
|
|
|
|
|
// the attached volumes of an instance, it must be "computed" and thus "optional".
|
|
|
|
|
// This accounts for situations such as "boot from volume" as well as volumes being
|
|
|
|
|
// attached to the instance outside of Terraform.
|
|
|
|
|
if v := d.Get("volume"); v != nil {
|
|
|
|
|
vols := v.(*schema.Set).List()
|
|
|
|
|
if len(vols) > 0 {
|
|
|
|
|
for _, v := range vols {
|
|
|
|
|
va := v.(map[string]interface{})
|
|
|
|
|
if va["volume_id"].(string) == "" {
|
|
|
|
|
return fmt.Errorf("A volume_id must be specified when attaching volumes.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return attachments, nil
|
|
|
|
|
if v, ok := d.GetOk("block_device"); ok {
|
|
|
|
|
vL := v.(*schema.Set).List()
|
|
|
|
|
if len(vL) > 1 {
|
|
|
|
|
return fmt.Errorf("Can only specify one block device to boot from.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|