diff --git a/builtin/providers/openstack/import_openstack_blockstorage_volume_attach_v2_test.go b/builtin/providers/openstack/import_openstack_blockstorage_volume_attach_v2_test.go new file mode 100644 index 000000000..8b3eb609c --- /dev/null +++ b/builtin/providers/openstack/import_openstack_blockstorage_volume_attach_v2_test.go @@ -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"}, + }, + }, + }) +} diff --git a/builtin/providers/openstack/provider.go b/builtin/providers/openstack/provider.go index e6ef6e32f..cbc25c2ed 100644 --- a/builtin/providers/openstack/provider.go +++ b/builtin/providers/openstack/provider.go @@ -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(), diff --git a/builtin/providers/openstack/resource_openstack_blockstorage_volume_attach_v2.go b/builtin/providers/openstack/resource_openstack_blockstorage_volume_attach_v2.go new file mode 100644 index 000000000..48a6a62d9 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_blockstorage_volume_attach_v2.go @@ -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 +} diff --git a/builtin/providers/openstack/resource_openstack_blockstorage_volume_attach_v2_test.go b/builtin/providers/openstack/resource_openstack_blockstorage_volume_attach_v2_test.go new file mode 100644 index 000000000..88c055759 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_blockstorage_volume_attach_v2_test.go @@ -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" + } +` diff --git a/website/source/docs/providers/openstack/r/blockstorage_volume_attach_v2.html.markdown b/website/source/docs/providers/openstack/r/blockstorage_volume_attach_v2.html.markdown new file mode 100644 index 000000000..a249426a4 --- /dev/null +++ b/website/source/docs/providers/openstack/r/blockstorage_volume_attach_v2.html.markdown @@ -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 +``` + diff --git a/website/source/layouts/openstack.erb b/website/source/layouts/openstack.erb index 5f1b43b59..48073c242 100644 --- a/website/source/layouts/openstack.erb +++ b/website/source/layouts/openstack.erb @@ -19,6 +19,9 @@ > openstack_blockstorage_volume_v2 + > + openstack_blockstorage_volume_attach_v2 +