diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 37ced3df5..6a761ff82 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,7 +1,7 @@ { "ImportPath": "github.com/hashicorp/terraform", "GoVersion": "go1.6", - "GodepVersion": "v62", + "GodepVersion": "v67", "Packages": [ "./..." ], @@ -471,6 +471,11 @@ "Comment": "v1.1.15", "Rev": "2cc71659118a868dc7544a7ef0808eb42d487011" }, + { + "ImportPath": "github.com/aws/aws-sdk-go/service/emr/emriface", + "Comment": "v1.1.23", + "Rev": "2cc71659118a868dc7544a7ef0808eb42d487011" + }, { "ImportPath": "github.com/aws/aws-sdk-go/service/firehose", "Comment": "v1.1.23", @@ -1094,198 +1099,198 @@ }, { "ImportPath": "github.com/rackspace/gophercloud", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" + }, + { + "ImportPath": "github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes", + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/schedulerhints", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/compute/v2/flavors", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/compute/v2/images", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/compute/v2/servers", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/identity/v2/tenants", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/identity/v2/tokens", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/identity/v3/tokens", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/networks", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/ports", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/subnets", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/openstack/utils", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/pagination", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/testhelper", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rackspace/gophercloud/testhelper/client", - "Comment": "v1.0.0-884-gc54bbac", - "Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de" - }, - { - "ImportPath": "github.com/ryanuber/columnize", - "Comment": "v2.0.1-8-g983d3a5", - "Rev": "983d3a5fab1bf04d1b412465d2d9f8430e2e917e" + "Comment": "v1.0.0-905-gadc2065", + "Rev": "adc206589ed49d18cecc9890ab93534704b04702" }, { "ImportPath": "github.com/rainycape/unidecode", diff --git a/builtin/providers/openstack/config.go b/builtin/providers/openstack/config.go index 6f9e0fa63..09e87f881 100644 --- a/builtin/providers/openstack/config.go +++ b/builtin/providers/openstack/config.go @@ -103,6 +103,13 @@ func (c *Config) blockStorageV1Client(region string) (*gophercloud.ServiceClient }) } +func (c *Config) blockStorageV2Client(region string) (*gophercloud.ServiceClient, error) { + return openstack.NewBlockStorageV2(c.osClient, gophercloud.EndpointOpts{ + Region: region, + Availability: c.getEndpointType(), + }) +} + func (c *Config) computeV2Client(region string) (*gophercloud.ServiceClient, error) { return openstack.NewComputeV2(c.osClient, gophercloud.EndpointOpts{ Region: region, diff --git a/builtin/providers/openstack/provider.go b/builtin/providers/openstack/provider.go index 5d8dfc59b..867691cff 100644 --- a/builtin/providers/openstack/provider.go +++ b/builtin/providers/openstack/provider.go @@ -92,6 +92,7 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "openstack_blockstorage_volume_v1": resourceBlockStorageVolumeV1(), + "openstack_blockstorage_volume_v2": resourceBlockStorageVolumeV2(), "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_v2.go b/builtin/providers/openstack/resource_openstack_blockstorage_volume_v2.go new file mode 100644 index 000000000..75dcc0a4d --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_blockstorage_volume_v2.go @@ -0,0 +1,341 @@ +package openstack + +import ( + "bytes" + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach" +) + +func resourceBlockStorageVolumeV2() *schema.Resource { + return &schema.Resource{ + Create: resourceBlockStorageVolumeV2Create, + Read: resourceBlockStorageVolumeV2Read, + Update: resourceBlockStorageVolumeV2Update, + Delete: resourceBlockStorageVolumeV2Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: schema.EnvDefaultFunc("OS_REGION_NAME", ""), + }, + "size": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "availability_zone": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, + "metadata": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + ForceNew: false, + Computed: true, + }, + "snapshot_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "source_vol_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "image_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "volume_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, + "consistency_group_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "source_replica": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "attachment": &schema.Schema{ + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "instance_id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "device": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + }, + Set: resourceVolumeV2AttachmentHash, + }, + }, + } +} + +func resourceBlockStorageVolumeV2Create(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + blockStorageClient, err := config.blockStorageV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack block storage client: %s", err) + } + + createOpts := &volumes.CreateOpts{ + AvailabilityZone: d.Get("availability_zone").(string), + ConsistencyGroupID: d.Get("consistency_group_id").(string), + Description: d.Get("description").(string), + ImageID: d.Get("image_id").(string), + Metadata: resourceContainerMetadataV2(d), + Name: d.Get("name").(string), + Size: d.Get("size").(int), + SnapshotID: d.Get("snapshot_id").(string), + SourceReplica: d.Get("source_replica").(string), + SourceVolID: d.Get("source_vol_id").(string), + VolumeType: d.Get("volume_type").(string), + } + + log.Printf("[DEBUG] Create Options: %#v", createOpts) + v, err := volumes.Create(blockStorageClient, createOpts).Extract() + if err != nil { + return fmt.Errorf("Error creating OpenStack volume: %s", err) + } + log.Printf("[INFO] Volume ID: %s", v.ID) + + // Store the ID now + d.SetId(v.ID) + + // Wait for the volume to become available. + log.Printf( + "[DEBUG] Waiting for volume (%s) to become available", + v.ID) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"downloading", "creating"}, + Target: []string{"available"}, + Refresh: VolumeV2StateRefreshFunc(blockStorageClient, v.ID), + 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", + v.ID, err) + } + + return resourceBlockStorageVolumeV2Read(d, meta) +} + +func resourceBlockStorageVolumeV2Read(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + blockStorageClient, err := config.blockStorageV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack block storage client: %s", err) + } + + v, err := volumes.Get(blockStorageClient, d.Id()).Extract() + if err != nil { + return CheckDeleted(d, err, "volume") + } + + log.Printf("[DEBUG] Retreived volume %s: %+v", d.Id(), v) + + d.Set("size", v.Size) + d.Set("description", v.Description) + d.Set("availability_zone", v.AvailabilityZone) + d.Set("name", v.Name) + d.Set("snapshot_id", v.SnapshotID) + d.Set("source_vol_id", v.SourceVolID) + d.Set("volume_type", v.VolumeType) + d.Set("metadata", v.Metadata) + + if len(v.Attachments) > 0 { + attachments := make([]map[string]interface{}, len(v.Attachments)) + for i, attachment := range v.Attachments { + attachments[i] = make(map[string]interface{}) + attachments[i]["id"] = attachment["id"] + attachments[i]["instance_id"] = attachment["server_id"] + attachments[i]["device"] = attachment["device"] + log.Printf("[DEBUG] attachment: %v", attachment) + } + d.Set("attachment", attachments) + } + + return nil +} + +func resourceBlockStorageVolumeV2Update(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + blockStorageClient, err := config.blockStorageV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack block storage client: %s", err) + } + + updateOpts := volumes.UpdateOpts{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + } + + if d.HasChange("metadata") { + updateOpts.Metadata = resourceVolumeMetadataV2(d) + } + + _, err = volumes.Update(blockStorageClient, d.Id(), updateOpts).Extract() + if err != nil { + return fmt.Errorf("Error updating OpenStack volume: %s", err) + } + + return resourceBlockStorageVolumeV2Read(d, meta) +} + +func resourceBlockStorageVolumeV2Delete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + blockStorageClient, err := config.blockStorageV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack block storage client: %s", err) + } + + v, err := volumes.Get(blockStorageClient, d.Id()).Extract() + if err != nil { + return CheckDeleted(d, err, "volume") + } + + // make sure this volume is detached from all instances before deleting + if len(v.Attachments) > 0 { + log.Printf("[DEBUG] detaching volumes") + if computeClient, err := config.computeV2Client(d.Get("region").(string)); err != nil { + return err + } else { + for _, volumeAttachment := range v.Attachments { + log.Printf("[DEBUG] Attachment: %v", volumeAttachment) + if err := volumeattach.Delete(computeClient, volumeAttachment["server_id"].(string), volumeAttachment["id"].(string)).ExtractErr(); err != nil { + return err + } + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"in-use", "attaching", "detaching"}, + Target: []string{"available"}, + Refresh: VolumeV2StateRefreshFunc(blockStorageClient, d.Id()), + 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", + d.Id(), err) + } + } + } + + // It's possible that this volume was used as a boot device and is currently + // in a "deleting" state from when the instance was terminated. + // If this is true, just move on. It'll eventually delete. + if v.Status != "deleting" { + if err := volumes.Delete(blockStorageClient, d.Id()).ExtractErr(); err != nil { + return CheckDeleted(d, err, "volume") + } + } + + // Wait for the volume to delete before moving on. + log.Printf("[DEBUG] Waiting for volume (%s) to delete", d.Id()) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"deleting", "downloading", "available"}, + Target: []string{"deleted"}, + Refresh: VolumeV2StateRefreshFunc(blockStorageClient, d.Id()), + 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 delete: %s", + d.Id(), err) + } + + d.SetId("") + return nil +} + +func resourceVolumeMetadataV2(d *schema.ResourceData) map[string]string { + m := make(map[string]string) + for key, val := range d.Get("metadata").(map[string]interface{}) { + m[key] = val.(string) + } + return m +} + +// VolumeV2StateRefreshFunc returns a resource.StateRefreshFunc that is used to watch +// an OpenStack volume. +func VolumeV2StateRefreshFunc(client *gophercloud.ServiceClient, volumeID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + v, err := volumes.Get(client, volumeID).Extract() + if err != nil { + errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok { + return nil, "", err + } + if errCode.Actual == 404 { + return v, "deleted", nil + } + return nil, "", err + } + + return v, v.Status, nil + } +} + +func resourceVolumeV2AttachmentHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + if m["instance_id"] != nil { + buf.WriteString(fmt.Sprintf("%s-", m["instance_id"].(string))) + } + return hashcode.String(buf.String()) +} diff --git a/builtin/providers/openstack/resource_openstack_blockstorage_volume_v2_test.go b/builtin/providers/openstack/resource_openstack_blockstorage_volume_v2_test.go new file mode 100644 index 000000000..7600a6527 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_blockstorage_volume_v2_test.go @@ -0,0 +1,188 @@ +package openstack + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes" +) + +func TestAccBlockStorageV2Volume_basic(t *testing.T) { + var volume volumes.Volume + + var testAccBlockStorageV2Volume_basic = fmt.Sprintf(` + resource "openstack_blockstorage_volume_v2" "volume_1" { + name = "volume_1" + description = "first test volume" + metadata { + foo = "bar" + } + size = 1 + }`) + + var testAccBlockStorageV2Volume_update = fmt.Sprintf(` + resource "openstack_blockstorage_volume_v2" "volume_1" { + name = "volume_1-updated" + description = "first test volume" + metadata { + foo = "bar" + } + size = 1 + }`) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckBlockStorageV2VolumeDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccBlockStorageV2Volume_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckBlockStorageV2VolumeExists(t, "openstack_blockstorage_volume_v2.volume_1", &volume), + resource.TestCheckResourceAttr("openstack_blockstorage_volume_v2.volume_1", "name", "volume_1"), + testAccCheckBlockStorageV2VolumeMetadata(&volume, "foo", "bar"), + ), + }, + resource.TestStep{ + Config: testAccBlockStorageV2Volume_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckBlockStorageV2VolumeExists(t, "openstack_blockstorage_volume_v2.volume_1", &volume), + resource.TestCheckResourceAttr("openstack_blockstorage_volume_v2.volume_1", "name", "volume_1-updated"), + testAccCheckBlockStorageV2VolumeMetadata(&volume, "foo", "bar"), + ), + }, + }, + }) +} + +func TestAccBlockStorageV2Volume_bootable(t *testing.T) { + var volume volumes.Volume + + var testAccBlockStorageV2Volume_bootable = fmt.Sprintf(` + resource "openstack_blockstorage_volume_v2" "volume_1" { + name = "volume_1-bootable" + size = 5 + image_id = "%s" + }`, + os.Getenv("OS_IMAGE_ID")) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckBlockStorageV2VolumeDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccBlockStorageV2Volume_bootable, + Check: resource.ComposeTestCheckFunc( + testAccCheckBlockStorageV2VolumeExists(t, "openstack_blockstorage_volume_v2.volume_1", &volume), + resource.TestCheckResourceAttr("openstack_blockstorage_volume_v2.volume_1", "name", "volume_1-bootable"), + ), + }, + }, + }) +} + +func testAccCheckBlockStorageV2VolumeDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + blockStorageClient, 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_v2" { + continue + } + + _, err := volumes.Get(blockStorageClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Volume still exists") + } + } + + return nil +} + +func testAccCheckBlockStorageV2VolumeExists(t *testing.T, n string, volume *volumes.Volume) 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) + blockStorageClient, err := config.blockStorageV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("Error creating OpenStack block storage client: %s", err) + } + + found, err := volumes.Get(blockStorageClient, rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.ID != rs.Primary.ID { + return fmt.Errorf("Volume not found") + } + + *volume = *found + + return nil + } +} + +func testAccCheckBlockStorageV2VolumeDoesNotExist(t *testing.T, n string, volume *volumes.Volume) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + blockStorageClient, err := config.blockStorageV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("Error creating OpenStack block storage client: %s", err) + } + + _, err = volumes.Get(blockStorageClient, volume.ID).Extract() + if err != nil { + errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok { + return err + } + if errCode.Actual == 404 { + return nil + } + return err + } + + return fmt.Errorf("Volume still exists") + } +} + +func testAccCheckBlockStorageV2VolumeMetadata( + volume *volumes.Volume, k string, v string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if volume.Metadata == nil { + return fmt.Errorf("No metadata") + } + + for key, value := range volume.Metadata { + if k != key { + continue + } + + if v == value { + return nil + } + + return fmt.Errorf("Bad value for %s: %s", k, value) + } + + return fmt.Errorf("Metadata not found: %s", k) + } +} diff --git a/vendor/github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes/doc.go b/vendor/github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes/doc.go new file mode 100644 index 000000000..307b8b12d --- /dev/null +++ b/vendor/github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes/doc.go @@ -0,0 +1,5 @@ +// Package volumes provides information and interaction with volumes in the +// OpenStack Block Storage service. A volume is a detachable block storage +// device, akin to a USB hard drive. It can only be attached to one instance at +// a time. +package volumes diff --git a/vendor/github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes/fixtures.go b/vendor/github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes/fixtures.go new file mode 100644 index 000000000..b70e2987b --- /dev/null +++ b/vendor/github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes/fixtures.go @@ -0,0 +1,204 @@ +package volumes + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func MockListResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` + { + "volumes": [ + { + "volume_type": "lvmdriver-1", + "created_at": "2015-09-17T03:35:03.000000", + "bootable": "false", + "name": "vol-001", + "os-vol-mig-status-attr:name_id": null, + "consistencygroup_id": null, + "source_volid": null, + "os-volume-replication:driver_data": null, + "multiattach": false, + "snapshot_id": null, + "replication_status": "disabled", + "os-volume-replication:extended_status": null, + "encrypted": false, + "os-vol-host-attr:host": null, + "availability_zone": "nova", + "attachments": [ + { + "attachment_id": "03987cd1-0ad5-40d1-9b2a-7cc48295d4fa", + "id": "47e9ecc5-4045-4ee3-9a4b-d859d546a0cf", + "volume_id": "289da7f8-6440-407c-9fb4-7db01ec49164", + "server_id": "d1c4788b-9435-42e2-9b81-29f3be1cd01f", + "host_name": "stack", + "device": "/dev/vdc" + } + ], + "id": "289da7f8-6440-407c-9fb4-7db01ec49164", + "size": 75, + "user_id": "ff1ce52c03ab433aaba9108c2e3ef541", + "os-vol-tenant-attr:tenant_id": "304dc00909ac4d0da6c62d816bcb3459", + "os-vol-mig-status-attr:migstat": null, + "metadata": {"foo": "bar"}, + "status": "available", + "description": null + }, + { + "volume_type": "lvmdriver-1", + "created_at": "2015-09-17T03:32:29.000000", + "bootable": "false", + "name": "vol-002", + "os-vol-mig-status-attr:name_id": null, + "consistencygroup_id": null, + "source_volid": null, + "os-volume-replication:driver_data": null, + "multiattach": false, + "snapshot_id": null, + "replication_status": "disabled", + "os-volume-replication:extended_status": null, + "encrypted": false, + "os-vol-host-attr:host": null, + "availability_zone": "nova", + "attachments": [], + "id": "96c3bda7-c82a-4f50-be73-ca7621794835", + "size": 75, + "user_id": "ff1ce52c03ab433aaba9108c2e3ef541", + "os-vol-tenant-attr:tenant_id": "304dc00909ac4d0da6c62d816bcb3459", + "os-vol-mig-status-attr:migstat": null, + "metadata": {}, + "status": "available", + "description": null + } + ] +} + + `) + }) +} + +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` +{ + "volume": { + "volume_type": "lvmdriver-1", + "created_at": "2015-09-17T03:32:29.000000", + "bootable": "false", + "name": "vol-001", + "os-vol-mig-status-attr:name_id": null, + "consistencygroup_id": null, + "source_volid": null, + "os-volume-replication:driver_data": null, + "multiattach": false, + "snapshot_id": null, + "replication_status": "disabled", + "os-volume-replication:extended_status": null, + "encrypted": false, + "os-vol-host-attr:host": null, + "availability_zone": "nova", + "attachments": [{ + "attachment_id": "dbce64e3-f3b9-4423-a44f-a2b15deffa1b", + "id": "3eafc6f5-ed74-456d-90fb-f253f594dbae", + "volume_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "server_id": "d1c4788b-9435-42e2-9b81-29f3be1cd01f", + "host_name": "stack", + "device": "/dev/vdd" + }], + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "size": 75, + "user_id": "ff1ce52c03ab433aaba9108c2e3ef541", + "os-vol-tenant-attr:tenant_id": "304dc00909ac4d0da6c62d816bcb3459", + "os-vol-mig-status-attr:migstat": null, + "metadata": {}, + "status": "available", + "description": null + } +} + `) + }) +} + +func MockCreateResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "volume": { + "name": "vol-001", + "size": 75 + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, ` +{ + "volume": { + "size": 75, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "metadata": {}, + "created_at": "2015-09-17T03:32:29.044216", + "encrypted": false, + "bootable": "false", + "availability_zone": "nova", + "attachments": [], + "user_id": "ff1ce52c03ab433aaba9108c2e3ef541", + "status": "creating", + "description": null, + "volume_type": "lvmdriver-1", + "name": "vol-001", + "replication_status": "disabled", + "consistencygroup_id": null, + "source_volid": null, + "snapshot_id": null, + "multiattach": false + } +} + `) + }) +} + +func MockDeleteResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} + +func MockUpdateResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` +{ + "volume": { + "name": "vol-002" + } +} + `) + }) +} diff --git a/vendor/github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes/requests.go b/vendor/github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes/requests.go new file mode 100644 index 000000000..4c60936ac --- /dev/null +++ b/vendor/github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes/requests.go @@ -0,0 +1,251 @@ +package volumes + +import ( + "fmt" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToVolumeCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains options for creating a Volume. This object is passed to +// the volumes.Create function. For more information about these parameters, +// see the Volume object. +type CreateOpts struct { + // The availability zone [OPTIONAL] + AvailabilityZone string + // ConsistencyGroupID is the ID of a consistency group [OPTINAL] + ConsistencyGroupID string + // The volume description [OPTIONAL] + Description string + // One or more metadata key and value pairs to associate with the volume [OPTIONAL] + Metadata map[string]string + // The volume name [OPTIONAL] + Name string + // The size of the volume, in gibibytes (GiB) [REQUIRED] + Size int + // the ID of the existing volume snapshot [OPTIONAL] + SnapshotID string + // SourceReplica is a UUID of an existing volume to replicate with [OPTIONAL] + SourceReplica string + // the ID of the existing volume [OPTIONAL] + SourceVolID string + // The ID of the image from which you want to create the volume. + // Required to create a bootable volume. + ImageID string + // The associated volume type [OPTIONAL] + VolumeType string +} + +// ToVolumeCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) { + v := make(map[string]interface{}) + + if opts.Size == 0 { + return nil, fmt.Errorf("Required CreateOpts field 'Size' not set.") + } + v["size"] = opts.Size + + if opts.AvailabilityZone != "" { + v["availability_zone"] = opts.AvailabilityZone + } + if opts.ConsistencyGroupID != "" { + v["consistencygroup_id"] = opts.ConsistencyGroupID + } + if opts.Description != "" { + v["description"] = opts.Description + } + if opts.ImageID != "" { + v["imageRef"] = opts.ImageID + } + if opts.Metadata != nil { + v["metadata"] = opts.Metadata + } + if opts.Name != "" { + v["name"] = opts.Name + } + if opts.SourceReplica != "" { + v["source_replica"] = opts.SourceReplica + } + if opts.SourceVolID != "" { + v["source_volid"] = opts.SourceVolID + } + if opts.SnapshotID != "" { + v["snapshot_id"] = opts.SnapshotID + } + if opts.VolumeType != "" { + v["volume_type"] = opts.VolumeType + } + + return map[string]interface{}{"volume": v}, nil +} + +// Create will create a new Volume based on the values in CreateOpts. To extract +// the Volume object from the response, call the Extract method on the +// CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToVolumeCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + return res +} + +// Delete will delete the existing Volume with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = client.Delete(deleteURL(client, id), nil) + return res +} + +// Get retrieves the Volume with the provided ID. To extract the Volume object +// from the response, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = client.Get(getURL(client, id), &res.Body, nil) + return res +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToVolumeListQuery() (string, error) +} + +// ListOpts holds options for listing Volumes. It is passed to the volumes.List +// function. +type ListOpts struct { + // admin-only option. Set it to true to see all tenant volumes. + AllTenants bool `q:"all_tenants"` + // List only volumes that contain Metadata. + Metadata map[string]string `q:"metadata"` + // List only volumes that have Name as the display name. + Name string `q:"name"` + // List only volumes that have a status of Status. + Status string `q:"status"` +} + +// ToVolumeListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToVolumeListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List returns Volumes optionally limited by the conditions provided in ListOpts. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToVolumeListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + createPage := func(r pagination.PageResult) pagination.Page { + return ListResult{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(client, url, createPage) +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToVolumeUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contain options for updating an existing Volume. This object is passed +// to the volumes.Update function. For more information about the parameters, see +// the Volume object. +type UpdateOpts struct { + // OPTIONAL + Name string + // OPTIONAL + Description string + // OPTIONAL + Metadata map[string]string +} + +// ToVolumeUpdateMap assembles a request body based on the contents of an +// UpdateOpts. +func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) { + v := make(map[string]interface{}) + + if opts.Description != "" { + v["description"] = opts.Description + } + if opts.Metadata != nil { + v["metadata"] = opts.Metadata + } + if opts.Name != "" { + v["name"] = opts.Name + } + + return map[string]interface{}{"volume": v}, nil +} + +// Update will update the Volume with provided information. To extract the updated +// Volume from the response, call the Extract method on the UpdateResult. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToVolumeUpdateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = client.Put(updateURL(client, id), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res +} + +// IDFromName is a convienience function that returns a server's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + volumeCount := 0 + volumeID := "" + if name == "" { + return "", fmt.Errorf("A volume name must be provided.") + } + pager := List(client, nil) + pager.EachPage(func(page pagination.Page) (bool, error) { + volumeList, err := ExtractVolumes(page) + if err != nil { + return false, err + } + + for _, s := range volumeList { + if s.Name == name { + volumeCount++ + volumeID = s.ID + } + } + return true, nil + }) + + switch volumeCount { + case 0: + return "", fmt.Errorf("Unable to find volume: %s", name) + case 1: + return volumeID, nil + default: + return "", fmt.Errorf("Found %d volumes matching %s", volumeCount, name) + } +} diff --git a/vendor/github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes/results.go b/vendor/github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes/results.go new file mode 100644 index 000000000..59fa53074 --- /dev/null +++ b/vendor/github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes/results.go @@ -0,0 +1,137 @@ +package volumes + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// Volume contains all the information associated with an OpenStack Volume. +type Volume struct { + // Instances onto which the volume is attached. + Attachments []map[string]interface{} `mapstructure:"attachments"` + + // AvailabilityZone is which availability zone the volume is in. + AvailabilityZone string `mapstructure:"availability_zone"` + + // Indicates whether this is a bootable volume. + Bootable string `mapstructure:"bootable"` + + // ConsistencyGroupID is the consistency group ID. + ConsistencyGroupID string `mapstructure:"consistencygroup_id"` + + // The date when this volume was created. + CreatedAt string `mapstructure:"created_at"` + + // Human-readable description for the volume. + Description string `mapstructure:"description"` + + // Encrypted denotes if the volume is encrypted. + Encrypted bool `mapstructure:"encrypted"` + + // Human-readable display name for the volume. + Name string `mapstructure:"name"` + + // The type of volume to create, either SATA or SSD. + VolumeType string `mapstructure:"volume_type"` + + // ReplicationDriverData contains data about the replication driver. + ReplicationDriverData string `mapstructure:"os-volume-replication:driver_data"` + + // ReplicationExtendedStatus contains extended status about replication. + ReplicationExtendedStatus string `mapstructure:"os-volume-replication:extended_status"` + + // ReplicationStatus is the status of replication. + ReplicationStatus string `mapstructure:"replication_status"` + + // The ID of the snapshot from which the volume was created + SnapshotID string `mapstructure:"snapshot_id"` + + // The ID of another block storage volume from which the current volume was created + SourceVolID string `mapstructure:"source_volid"` + + // Current status of the volume. + Status string `mapstructure:"status"` + + // TenantID is the id of the project that owns the volume. + TenantID string `mapstructure:"os-vol-tenant-attr:tenant_id"` + + // Arbitrary key-value pairs defined by the user. + Metadata map[string]string `mapstructure:"metadata"` + + // Multiattach denotes if the volume is multi-attach capable. + Multiattach bool `mapstructure:"multiattach"` + + // Unique identifier for the volume. + ID string `mapstructure:"id"` + + // Size of the volume in GB. + Size int `mapstructure:"size"` + + // UserID is the id of the user who created the volume. + UserID string `mapstructure:"user_id"` +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} + +// ListResult is a pagination.pager that is returned from a call to the List function. +type ListResult struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a ListResult contains no Volumes. +func (r ListResult) IsEmpty() (bool, error) { + volumes, err := ExtractVolumes(r) + if err != nil { + return true, err + } + return len(volumes) == 0, nil +} + +// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call. +func ExtractVolumes(page pagination.Page) ([]Volume, error) { + var response struct { + Volumes []Volume `json:"volumes"` + } + + err := mapstructure.Decode(page.(ListResult).Body, &response) + return response.Volumes, err +} + +// UpdateResult contains the response body and error from an Update request. +type UpdateResult struct { + commonResult +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the Volume object out of the commonResult object. +func (r commonResult) Extract() (*Volume, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Volume *Volume `json:"volume"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Volume, err +} diff --git a/vendor/github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes/urls.go b/vendor/github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes/urls.go new file mode 100644 index 000000000..2523ec62d --- /dev/null +++ b/vendor/github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes/urls.go @@ -0,0 +1,23 @@ +package volumes + +import "github.com/rackspace/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("volumes") +} + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("volumes", "detail") +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("volumes", id) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} diff --git a/vendor/github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes/util.go b/vendor/github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes/util.go new file mode 100644 index 000000000..1dda695ea --- /dev/null +++ b/vendor/github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes/util.go @@ -0,0 +1,22 @@ +package volumes + +import ( + "github.com/rackspace/gophercloud" +) + +// WaitForStatus will continually poll the resource, checking for a particular +// status. It will do this for the amount of seconds defined. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := Get(c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/vendor/github.com/rackspace/gophercloud/openstack/client.go b/vendor/github.com/rackspace/gophercloud/openstack/client.go index 951f4ed40..b533e834e 100644 --- a/vendor/github.com/rackspace/gophercloud/openstack/client.go +++ b/vendor/github.com/rackspace/gophercloud/openstack/client.go @@ -281,6 +281,29 @@ func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.Endpoi return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil } +// NewBlockStorageV2 creates a ServiceClient that may be used to access the v2 block storage service. +func NewBlockStorageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("volume") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + + // Force using v2 API + if strings.Contains(url, "/v1") { + url = strings.Replace(url, "/v1", "/v2", -1) + } + if !strings.Contains(url, "/v2") { + return nil, fmt.Errorf("Block Storage v2 endpoint not found") + } + + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: url, + ResourceBase: url, + }, nil +} + // NewCDNV1 creates a ServiceClient that may be used to access the OpenStack v1 // CDN service. func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { diff --git a/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/images/results.go b/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/images/results.go index 40e814d1d..482e7d6f6 100644 --- a/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/images/results.go +++ b/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/images/results.go @@ -51,6 +51,8 @@ type Image struct { Status string Updated string + + Metadata map[string]string } // ImagePage contains a single page of results from a List operation. diff --git a/vendor/github.com/rackspace/gophercloud/pagination/pager.go b/vendor/github.com/rackspace/gophercloud/pagination/pager.go index a7593ac88..6f1ca046f 100644 --- a/vendor/github.com/rackspace/gophercloud/pagination/pager.go +++ b/vendor/github.com/rackspace/gophercloud/pagination/pager.go @@ -138,6 +138,11 @@ func (p Pager) AllPages() (Page, error) { // that type. pageType := reflect.TypeOf(testPage) + // if it's a single page, just return the testPage (first page) + if _, found := pageType.FieldByName("SinglePageBase"); found { + return testPage, nil + } + // Switch on the page body type. Recognized types are `map[string]interface{}`, // `[]byte`, and `[]interface{}`. switch testPage.GetBody().(type) { @@ -153,7 +158,14 @@ func (p Pager) AllPages() (Page, error) { key = k } } - pagesSlice = append(pagesSlice, b[key].([]interface{})...) + switch keyType := b[key].(type) { + case map[string]interface{}: + pagesSlice = append(pagesSlice, keyType) + case []interface{}: + pagesSlice = append(pagesSlice, b[key].([]interface{})...) + default: + return false, fmt.Errorf("Unsupported page body type: %+v", keyType) + } return true, nil }) if err != nil { diff --git a/website/source/docs/providers/openstack/r/blockstorage_volume_v2.html.markdown b/website/source/docs/providers/openstack/r/blockstorage_volume_v2.html.markdown new file mode 100644 index 000000000..edbe592f7 --- /dev/null +++ b/website/source/docs/providers/openstack/r/blockstorage_volume_v2.html.markdown @@ -0,0 +1,80 @@ +--- +layout: "openstack" +page_title: "OpenStack: openstack_blockstorage_volume_v2" +sidebar_current: "docs-openstack-resource-blockstorage-volume-v2" +description: |- + Manages a V2 volume resource within OpenStack. +--- + +# openstack\_blockstorage\_volume_v2 + +Manages a V2 volume resource within OpenStack. + +## Example Usage + +``` +resource "openstack_blockstorage_volume_v2" "volume_1" { + region = "RegionOne" + name = "volume_1" + description = "first test volume" + size = 3 +} +``` + +## Argument Reference + +The following arguments are supported: + +* `region` - (Required) The region in which to create the volume. If + omitted, the `OS_REGION_NAME` environment variable is used. Changing this + creates a new volume. + +* `size` - (Required) The size of the volume to create (in gigabytes). Changing + this creates a new volume. + +* `availability_zone` - (Optional) The availability zone for the volume. + Changing this creates a new volume. + +* `consistency_group_id` - (Optional) The consistency group to place the volume + in. + +* `description` - (Optional) A description of the volume. Changing this updates + the volume's description. + +* `image_id` - (Optional) The image ID from which to create the volume. + Changing this creates a new volume. + +* `metadata` - (Optional) Metadata key/value pairs to associate with the volume. + Changing this updates the existing volume metadata. + +* `name` - (Optional) A unique name for the volume. Changing this updates the + volume's name. + +* `snapshot_id` - (Optional) The snapshot ID from which to create the volume. + Changing this creates a new volume. + +* `source_replica` - (Optional) The volume ID to replicate with. + +* `source_vol_id` - (Optional) The volume ID from which to create the volume. + Changing this creates a new volume. + +* `volume_type` - (Optional) The type of volume to create. + Changing this creates a new volume. + +## Attributes Reference + +The following attributes are exported: + +* `region` - See Argument Reference above. +* `size` - See Argument Reference above. +* `name` - See Argument Reference above. +* `description` - See Argument Reference above. +* `availability_zone` - See Argument Reference above. +* `image_id` - See Argument Reference above. +* `source_vol_id` - See Argument Reference above. +* `snapshot_id` - See Argument Reference above. +* `metadata` - See Argument Reference above. +* `volume_type` - See Argument Reference above. +* `attachment` - If a volume is attached to an instance, this attribute will + display the Attachment ID, Instance ID, and the Device as the Instance + sees it. diff --git a/website/source/layouts/openstack.erb b/website/source/layouts/openstack.erb index 3be421471..985ba40ba 100644 --- a/website/source/layouts/openstack.erb +++ b/website/source/layouts/openstack.erb @@ -16,6 +16,9 @@