From 24d7ada0e4fe70159fb787555abd9f098fa809aa Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Thu, 10 Nov 2016 16:00:37 +0000 Subject: [PATCH] provider/openstack: openstack_compute_volume_attach_v2 resource This commit adds the openstack_compute_volume_attach_v2 resource. This resource enables a volume to be attached to an instance by using the OpenStack Compute (Nova) v2 volumeattach API. --- ...openstack_compute_volume_attach_v2_test.go | 29 +++ builtin/providers/openstack/provider.go | 1 + ...urce_openstack_compute_volume_attach_v2.go | 186 ++++++++++++++++++ ...openstack_compute_volume_attach_v2_test.go | 109 ++++++++++ .../r/compute_volume_attach_v2.html.markdown | 65 ++++++ website/source/layouts/openstack.erb | 3 + 6 files changed, 393 insertions(+) create mode 100644 builtin/providers/openstack/import_openstack_compute_volume_attach_v2_test.go create mode 100644 builtin/providers/openstack/resource_openstack_compute_volume_attach_v2.go create mode 100644 builtin/providers/openstack/resource_openstack_compute_volume_attach_v2_test.go create mode 100644 website/source/docs/providers/openstack/r/compute_volume_attach_v2.html.markdown diff --git a/builtin/providers/openstack/import_openstack_compute_volume_attach_v2_test.go b/builtin/providers/openstack/import_openstack_compute_volume_attach_v2_test.go new file mode 100644 index 000000000..ae762b593 --- /dev/null +++ b/builtin/providers/openstack/import_openstack_compute_volume_attach_v2_test.go @@ -0,0 +1,29 @@ +package openstack + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccComputeV2VolumeAttach_importBasic(t *testing.T) { + resourceName := "openstack_compute_volume_attach_v2.va_1" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeV2VolumeAttachDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeV2VolumeAttach_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..f343f161a 100644 --- a/builtin/providers/openstack/provider.go +++ b/builtin/providers/openstack/provider.go @@ -135,6 +135,7 @@ func Provider() terraform.ResourceProvider { "openstack_compute_secgroup_v2": resourceComputeSecGroupV2(), "openstack_compute_servergroup_v2": resourceComputeServerGroupV2(), "openstack_compute_floatingip_v2": resourceComputeFloatingIPV2(), + "openstack_compute_volume_attach_v2": resourceComputeVolumeAttachV2(), "openstack_fw_firewall_v1": resourceFWFirewallV1(), "openstack_fw_policy_v1": resourceFWPolicyV1(), "openstack_fw_rule_v1": resourceFWRuleV1(), diff --git a/builtin/providers/openstack/resource_openstack_compute_volume_attach_v2.go b/builtin/providers/openstack/resource_openstack_compute_volume_attach_v2.go new file mode 100644 index 000000000..6342ce87e --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_compute_volume_attach_v2.go @@ -0,0 +1,186 @@ +package openstack + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceComputeVolumeAttachV2() *schema.Resource { + return &schema.Resource{ + Create: resourceComputeVolumeAttachV2Create, + Read: resourceComputeVolumeAttachV2Read, + Delete: resourceComputeVolumeAttachV2Delete, + 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", ""), + }, + + "instance_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "volume_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "device": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + }, + } +} + +func resourceComputeVolumeAttachV2Create(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + computeClient, err := config.computeV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack compute client: %s", err) + } + + instanceId := d.Get("instance_id").(string) + volumeId := d.Get("volume_id").(string) + + var device string + if v, ok := d.GetOk("device"); ok { + device = v.(string) + } + + attachOpts := volumeattach.CreateOpts{ + Device: device, + VolumeID: volumeId, + } + + log.Printf("[DEBUG] Creating volume attachment: %#v", attachOpts) + + attachment, err := volumeattach.Create(computeClient, instanceId, attachOpts).Extract() + if err != nil { + return err + } + + log.Printf("[DEBUG] Created volume attachment: %#v", attachment) + + // Use the instance ID and attachment ID as the resource ID. + // This is because an attachment cannot be retrieved just by its ID alone. + id := fmt.Sprintf("%s/%s", instanceId, attachment.ID) + + d.SetId(id) + + return resourceComputeVolumeAttachV2Read(d, meta) +} + +func resourceComputeVolumeAttachV2Read(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + computeClient, err := config.computeV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack compute client: %s", err) + } + + instanceId, attachmentId, err := parseComputeVolumeAttachmentId(d.Id()) + if err != nil { + return err + } + + attachment, err := volumeattach.Get(computeClient, instanceId, attachmentId).Extract() + if err != nil { + return err + } + + log.Printf("[DEBUG] Retrieved volume attachment: %#v", attachment) + + d.Set("instance_id", attachment.ServerID) + d.Set("volume_id", attachment.VolumeID) + d.Set("device", attachment.Device) + + return nil +} + +func resourceComputeVolumeAttachV2Delete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + computeClient, err := config.computeV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack compute client: %s", err) + } + + instanceId, attachmentId, err := parseComputeVolumeAttachmentId(d.Id()) + if err != nil { + return err + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{""}, + Target: []string{"DETACHED"}, + Refresh: volumeDetachRefreshFunc(computeClient, instanceId, attachmentId), + Timeout: 10 * time.Minute, + Delay: 15 * time.Second, + MinTimeout: 15 * time.Second, + } + + if _, err = stateConf.WaitForState(); err != nil { + return fmt.Errorf("Error detaching OpenStack volume: %s", err) + } + + return nil +} + +func volumeDetachRefreshFunc(computeClient *gophercloud.ServiceClient, instanceId, attachmentId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + log.Printf("[DEBUG] Attempting to detach OpenStack volume %s from instance %s", attachmentId, instanceId) + + va, err := volumeattach.Get(computeClient, instanceId, attachmentId).Extract() + if err != nil { + if _, ok := err.(gophercloud.ErrDefault404); ok { + return va, "DETACHED", nil + } + return va, "", err + } + + err = volumeattach.Delete(computeClient, instanceId, attachmentId).ExtractErr() + if err != nil { + if _, ok := err.(gophercloud.ErrDefault404); ok { + return va, "DETACHED", nil + } + + if _, ok := err.(gophercloud.ErrDefault400); ok { + return nil, "", nil + } + + return nil, "", err + } + + log.Printf("[DEBUG] OpenStack Volume Attachment (%s) is still active.", attachmentId) + return nil, "", nil + } +} + +func parseComputeVolumeAttachmentId(id string) (string, string, error) { + idParts := strings.Split(id, "/") + if len(idParts) < 2 { + return "", "", fmt.Errorf("Unable to determine volume attachment ID") + } + + instanceId := idParts[0] + attachmentId := idParts[1] + + return instanceId, attachmentId, nil +} diff --git a/builtin/providers/openstack/resource_openstack_compute_volume_attach_v2_test.go b/builtin/providers/openstack/resource_openstack_compute_volume_attach_v2_test.go new file mode 100644 index 000000000..5037ea576 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_compute_volume_attach_v2_test.go @@ -0,0 +1,109 @@ +package openstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach" +) + +func TestAccComputeV2VolumeAttach_basic(t *testing.T) { + var va volumeattach.VolumeAttachment + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeV2VolumeAttachDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeV2VolumeAttach_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeV2VolumeAttachExists(t, "openstack_compute_volume_attach_v2.va_1", &va), + ), + }, + }, + }) +} + +func testAccCheckComputeV2VolumeAttachDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + computeClient, err := config.computeV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("Error creating OpenStack compute client: %s", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_compute_volume_attach_v2" { + continue + } + + instanceId, volumeId, err := parseComputeVolumeAttachmentId(rs.Primary.ID) + if err != nil { + return err + } + + _, err = volumeattach.Get(computeClient, instanceId, volumeId).Extract() + if err == nil { + return fmt.Errorf("Volume attachment still exists") + } + } + + return nil +} + +func testAccCheckComputeV2VolumeAttachExists(t *testing.T, n string, va *volumeattach.VolumeAttachment) 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) + computeClient, err := config.computeV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckComputeV2VolumeAttachExists) Error creating OpenStack compute client: %s", err) + } + + instanceId, volumeId, err := parseComputeVolumeAttachmentId(rs.Primary.ID) + if err != nil { + return err + } + + found, err := volumeattach.Get(computeClient, instanceId, volumeId).Extract() + if err != nil { + return err + } + + if found.ServerID != instanceId || found.VolumeID != volumeId { + return fmt.Errorf("VolumeAttach not found") + } + + *va = *found + + return nil + } +} + +var testAccComputeV2VolumeAttach_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_compute_volume_attach_v2" "va_1" { + instance_id = "${openstack_compute_instance_v2.instance_1.id}" + volume_id = "${openstack_blockstorage_volume_v2.volume_1.id}" + } +` diff --git a/website/source/docs/providers/openstack/r/compute_volume_attach_v2.html.markdown b/website/source/docs/providers/openstack/r/compute_volume_attach_v2.html.markdown new file mode 100644 index 000000000..389c5f356 --- /dev/null +++ b/website/source/docs/providers/openstack/r/compute_volume_attach_v2.html.markdown @@ -0,0 +1,65 @@ +--- +layout: "openstack" +page_title: "OpenStack: openstack_compute_volume_attach_v2" +sidebar_current: "docs-openstack-resource-compute-volume-attach-v2" +description: |- + Attaches a Block Storage Volume to an Instance. +--- + +# openstack\_compute\_volume_attach_v2 + +Attaches a Block Storage Volume to an Instance using the OpenStack +Compute (Nova) v2 API. + +## Example Usage + +``` +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_compute_volume_attach_v2" "va_1" { + instance_id = "${openstack_compute_instance_v2.instance_1.id}" + volume_id = "${openstack_blockstorage_volume_v2.volume_1.id}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `region` - (Required) The region in which to obtain the V2 Compute client. + A Compute 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. + +* `instance_id` - (Required) The ID of the Instance to attach the Volume to. + +* `volume_id` - (Required) The ID of the Volume to attach to an Instance. + +## Attributes Reference + +The following attributes are exported: + +* `region` - See Argument Reference above. +* `instance_id` - See Argument Reference above. +* `volume_id` - See Argument Reference above. +* `device` - The device of the volume attachment (ex: `/dev/vdc`). + _NOTE_: This is the device reported by the Compute API and the real device + might actually differ depending on the hypervisor being used. This should + not be used as an authoritative piece of information. + +## Import + +Volume Attachments can be imported using the Instance ID and Volume ID +separated by a slash, e.g. + +``` +$ terraform import openstack_compute_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..d259766a7 100644 --- a/website/source/layouts/openstack.erb +++ b/website/source/layouts/openstack.erb @@ -40,6 +40,9 @@ > openstack_compute_servergroup_v2 + > + openstack_compute_volume_attach_v2 +