provider/openstack: openstack_blockstorage_volume_attach_v2 resource

This commit adds the openstack_blockstorage_volume_attach_v2 resource. This
resource enables a volume to be attached to an instance by using the OpenStack
Block Storage (Cinder) v2 API.
This commit is contained in:
Joe Topjian 2016-11-20 21:15:07 +00:00
parent fd8d41f6a5
commit 672d4d20d7
6 changed files with 492 additions and 0 deletions

View File

@ -0,0 +1,29 @@
package openstack
import (
"testing"
"github.com/hashicorp/terraform/helper/resource"
)
func TestAccBlockStorageVolumeAttachV2_importBasic(t *testing.T) {
resourceName := "openstack_blockstorage_volume_attach_v2.va_1"
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckBlockStorageVolumeAttachV2Destroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccBlockStorageVolumeAttachV2_basic,
},
resource.TestStep{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"region"},
},
},
})
}

View File

@ -130,6 +130,7 @@ func Provider() terraform.ResourceProvider {
ResourcesMap: map[string]*schema.Resource{
"openstack_blockstorage_volume_v1": resourceBlockStorageVolumeV1(),
"openstack_blockstorage_volume_v2": resourceBlockStorageVolumeV2(),
"openstack_blockstorage_volume_attach_v2": resourceBlockStorageVolumeAttachV2(),
"openstack_compute_instance_v2": resourceComputeInstanceV2(),
"openstack_compute_keypair_v2": resourceComputeKeypairV2(),
"openstack_compute_secgroup_v2": resourceComputeSecGroupV2(),

View File

@ -0,0 +1,254 @@
package openstack
import (
"fmt"
"log"
"strings"
"time"
"github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions"
"github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceBlockStorageVolumeAttachV2() *schema.Resource {
return &schema.Resource{
Create: resourceBlockStorageVolumeAttachV2Create,
Read: resourceBlockStorageVolumeAttachV2Read,
Update: nil,
Delete: resourceBlockStorageVolumeAttachV2Delete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"region": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
DefaultFunc: schema.EnvDefaultFunc("OS_REGION_NAME", ""),
},
"volume_id": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"instance_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ConflictsWith: []string{"host_name"},
},
"host_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ConflictsWith: []string{"instance_id"},
},
"device": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"attach_mode": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if value != "ro" && value != "rw" {
errors = append(errors, fmt.Errorf(
"Only 'ro' and 'rw' are supported values for 'attach_mode'"))
}
return
},
},
},
}
}
func resourceBlockStorageVolumeAttachV2Create(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
client, err := config.blockStorageV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack block storage client: %s", err)
}
// Check if either instance_id or host_name was set.
instanceId := d.Get("instance_id").(string)
hostName := d.Get("host_name").(string)
if instanceId == "" && hostName == "" {
return fmt.Errorf("One of 'instance_id' or 'host_name' must be set.")
}
volumeId := d.Get("volume_id").(string)
attachMode, err := blockStorageVolumeAttachV2AttachMode(d.Get("attach_mode").(string))
if err != nil {
return nil
}
attachOpts := &volumeactions.AttachOpts{
InstanceUUID: d.Get("instance_id").(string),
HostName: d.Get("host_name").(string),
MountPoint: d.Get("device").(string),
Mode: attachMode,
}
log.Printf("[DEBUG] Attachment Options: %#v", attachOpts)
if err := volumeactions.Attach(client, volumeId, attachOpts).ExtractErr(); err != nil {
return err
}
// Wait for the volume to become available.
log.Printf("[DEBUG] Waiting for volume (%s) to become available", volumeId)
stateConf := &resource.StateChangeConf{
Pending: []string{"available", "attaching"},
Target: []string{"in-use"},
Refresh: VolumeV2StateRefreshFunc(client, volumeId),
Timeout: 10 * time.Minute,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
}
_, err = stateConf.WaitForState()
if err != nil {
return fmt.Errorf("Error waiting for volume (%s) to become ready: %s", volumeId, err)
}
volume, err := volumes.Get(client, volumeId).Extract()
if err != nil {
return err
}
var attachmentId string
for _, attachment := range volume.Attachments {
if instanceId != "" && instanceId == attachment.ServerID {
attachmentId = attachment.AttachmentID
}
if hostName != "" && hostName == attachment.HostName {
attachmentId = attachment.AttachmentID
}
}
if attachmentId == "" {
return fmt.Errorf("Unable to determine attachment ID.")
}
// The ID must be a combination of the volume and attachment ID
// in order to import attachments.
id := fmt.Sprintf("%s/%s", volumeId, attachmentId)
d.SetId(id)
return resourceBlockStorageVolumeAttachV2Read(d, meta)
}
func resourceBlockStorageVolumeAttachV2Read(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
client, err := config.blockStorageV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack block storage client: %s", err)
}
volumeId, attachmentId, err := blockStorageVolumeAttachV2ParseId(d.Id())
if err != nil {
return err
}
volume, err := volumes.Get(client, volumeId).Extract()
if err != nil {
return err
}
log.Printf("[DEBUG] Retrieved volume %s: %#v", d.Id(), volume)
var attachment volumes.Attachment
for _, v := range volume.Attachments {
if attachmentId == v.AttachmentID {
attachment = v
}
}
log.Printf("[DEBUG] Retrieved volume attachment: %#v", attachment)
d.Set("volume_id", volumeId)
d.Set("attachment_id", attachmentId)
d.Set("device", attachment.Device)
d.Set("instance_id", attachment.ServerID)
d.Set("host_name", attachment.HostName)
return nil
}
func resourceBlockStorageVolumeAttachV2Delete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
client, err := config.blockStorageV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack block storage client: %s", err)
}
volumeId, attachmentId, err := blockStorageVolumeAttachV2ParseId(d.Id())
if err != nil {
return err
}
detachOpts := volumeactions.DetachOpts{
AttachmentID: attachmentId,
}
log.Printf("[DEBUG] Detachment Options: %#v", detachOpts)
if err := volumeactions.Detach(client, volumeId, detachOpts).ExtractErr(); err != nil {
return err
}
stateConf := &resource.StateChangeConf{
Pending: []string{"in-use", "attaching", "detaching"},
Target: []string{"available"},
Refresh: VolumeV2StateRefreshFunc(client, volumeId),
Timeout: 10 * time.Minute,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
}
_, err = stateConf.WaitForState()
if err != nil {
return fmt.Errorf("Error waiting for volume (%s) to become available: %s", volumeId, err)
}
return nil
}
func blockStorageVolumeAttachV2AttachMode(v string) (volumeactions.AttachMode, error) {
var attachMode volumeactions.AttachMode
var attachError error
switch v {
case "":
case "ro":
attachMode = volumeactions.ReadOnly
case "rw":
attachMode = volumeactions.ReadWrite
default:
attachError = fmt.Errorf("Invalid attach_mode specified")
}
return attachMode, attachError
}
func blockStorageVolumeAttachV2ParseId(id string) (string, string, error) {
parts := strings.Split(id, "/")
if len(parts) < 2 {
return "", "", fmt.Errorf("Unable to determine attachment ID")
}
return parts[0], parts[1], nil
}

View File

@ -0,0 +1,126 @@
package openstack
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes"
)
func TestAccBlockStorageVolumeAttachV2_basic(t *testing.T) {
var va volumes.Attachment
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckBlockStorageVolumeAttachV2Destroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccBlockStorageVolumeAttachV2_basic,
Check: resource.ComposeTestCheckFunc(
testAccCheckBlockStorageVolumeAttachV2Exists(t, "openstack_blockstorage_volume_attach_v2.va_1", &va),
),
},
},
})
}
func testAccCheckBlockStorageVolumeAttachV2Destroy(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)
client, err := config.blockStorageV2Client(OS_REGION_NAME)
if err != nil {
return fmt.Errorf("Error creating OpenStack block storage client: %s", err)
}
for _, rs := range s.RootModule().Resources {
if rs.Type != "openstack_blockstorage_volume_attach_v2" {
continue
}
volumeId, attachmentId, err := blockStorageVolumeAttachV2ParseId(rs.Primary.ID)
if err != nil {
return err
}
volume, err := volumes.Get(client, volumeId).Extract()
if err != nil {
if _, ok := err.(gophercloud.ErrDefault404); ok {
return nil
}
return err
}
for _, v := range volume.Attachments {
if attachmentId == v.AttachmentID {
return fmt.Errorf("Volume attachment still exists")
}
}
}
return nil
}
func testAccCheckBlockStorageVolumeAttachV2Exists(t *testing.T, n string, va *volumes.Attachment) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}
if rs.Primary.ID == "" {
return fmt.Errorf("No ID is set")
}
config := testAccProvider.Meta().(*Config)
client, err := config.blockStorageV2Client(OS_REGION_NAME)
if err != nil {
return fmt.Errorf("Error creating OpenStack block storage client: %s", err)
}
volumeId, attachmentId, err := blockStorageVolumeAttachV2ParseId(rs.Primary.ID)
if err != nil {
return err
}
volume, err := volumes.Get(client, volumeId).Extract()
if err != nil {
return err
}
var found bool
for _, v := range volume.Attachments {
if attachmentId == v.AttachmentID {
found = true
*va = v
}
}
if !found {
return fmt.Errorf("Volume Attachment not found")
}
return nil
}
}
var testAccBlockStorageVolumeAttachV2_basic = `
resource "openstack_blockstorage_volume_v2" "volume_1" {
name = "volume_1"
size = 1
}
resource "openstack_compute_instance_v2" "instance_1" {
name = "instance_1"
security_groups = ["default"]
}
resource "openstack_blockstorage_volume_attach_v2" "va_1" {
instance_id = "${openstack_compute_instance_v2.instance_1.id}"
volume_id = "${openstack_blockstorage_volume_v2.volume_1.id}"
device = "auto"
}
`

View File

@ -0,0 +1,79 @@
---
layout: "openstack"
page_title: "OpenStack: openstack_blockstorage_volume_attach_v2"
sidebar_current: "docs-openstack-resource-blockstorage-volume-attach-v2"
description: |-
Attaches a Block Storage Volume to an Instance.
---
# openstack\_blockstorage\_volume_attach_v2
Attaches a Block Storage Volume to an Instance using the OpenStack
Block Storage (Cinder) v2 API.
## Example Usage
```
resource "openstack_blockstorage_volume_v2" "volume_1" {
name = "volume_1"
size = 1
}
resource "openstack_blockstorage_instance_v2" "instance_1" {
name = "instance_1"
security_groups = ["default"]
}
resource "openstack_blockstorage_volume_attach_v2" "va_1" {
instance_id = "${openstack_blockstorage_instance_v2.instance_1.id}"
volume_id = "${openstack_blockstorage_volume_v2.volume_1.id}"
device = "auto"
attach_mode = "rw"
}
```
## Argument Reference
The following arguments are supported:
* `region` - (Required) The region in which to obtain the V2 Block Storage
client. A Block Storage client is needed to create a volume attachment.
If omitted, the `OS_REGION_NAME` environment variable is used. Changing
this creates a new volume attachment.
* `volume_id` - (Required) The ID of the Volume to attach to an Instance.
* `instance_id` - (Required if `host_name` is not used) The ID of the Instance
to attach the Volume to.
* `host_name` - (Required if `instance_id` is not used) The host to attach the
volume to.
* `device` - (Optional) The device to attach the volume as.
* `attach_mode` - (Optional) Specify whether to attach the volume as Read-Only
(`ro`) or Read-Write (`rw`). Only values of `ro` and `rw` are accepted.
## Attributes Reference
The following attributes are exported:
* `region` - See Argument Reference above.
* `volume_id` - See Argument Reference above.
* `instance_id` - See Argument Reference above.
* `host_name` - See Argument Reference above.
* `attach_mode` - See Argument Reference above.
* `device` - See Argument Reference above.
_NOTE_: Whether or not this is really the device the volume was attached
as depends on the hypervisor being used in the OpenStack cloud. Do not
consider this an authoritative piece of information.
## Import
Volume Attachments can be imported using the Volume and Attachment ID
separated by a slash, e.g.
```
$ terraform import openstack_blockstorage_volume_attach_v2.va_1 89c60255-9bd6-460c-822a-e2b959ede9d2/45670584-225f-46c3-b33e-6707b589b666
```

View File

@ -19,6 +19,9 @@
<li<%= sidebar_current("docs-openstack-resource-blockstorage-volume-v2") %>>
<a href="/docs/providers/openstack/r/blockstorage_volume_v2.html">openstack_blockstorage_volume_v2</a>
</li>
<li<%= sidebar_current("docs-openstack-resource-blockstorage-volume-attach-v2") %>>
<a href="/docs/providers/openstack/r/blockstorage_volume_attach_v2.html">openstack_blockstorage_volume_attach_v2</a>
</li>
</ul>
</li>