diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 8b04be2d3..c65ef6eec 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -127,6 +127,7 @@ func Provider() terraform.ResourceProvider { "aws_sns_topic": resourceAwsSnsTopic(), "aws_sns_topic_subscription": resourceAwsSnsTopicSubscription(), "aws_subnet": resourceAwsSubnet(), + "aws_volume_attachment": resourceAwsVolumeAttachment(), "aws_vpc_dhcp_options_association": resourceAwsVpcDhcpOptionsAssociation(), "aws_vpc_dhcp_options": resourceAwsVpcDhcpOptions(), "aws_vpc_peering_connection": resourceAwsVpcPeeringConnection(), diff --git a/builtin/providers/aws/resource_aws_ebs_volume.go b/builtin/providers/aws/resource_aws_ebs_volume.go index 815ec59a0..450fd7f74 100644 --- a/builtin/providers/aws/resource_aws_ebs_volume.go +++ b/builtin/providers/aws/resource_aws_ebs_volume.go @@ -2,11 +2,14 @@ package aws import ( "fmt" + "log" + "time" "github.com/awslabs/aws-sdk-go/aws" "github.com/awslabs/aws-sdk-go/aws/awserr" "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" ) @@ -91,9 +94,55 @@ func resourceAwsEbsVolumeCreate(d *schema.ResourceData, meta interface{}) error if err != nil { return fmt.Errorf("Error creating EC2 volume: %s", err) } + + log.Printf( + "[DEBUG] Waiting for Volume (%s) to become available", + d.Id()) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"creating"}, + Target: "available", + Refresh: volumeStateRefreshFunc(conn, *result.VolumeID), + Timeout: 5 * 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", + *result.VolumeID, err) + } + return readVolume(d, result) } +// volumeStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch +// a the state of a Volume. Returns successfully when volume is available +func volumeStateRefreshFunc(conn *ec2.EC2, volumeID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + resp, err := conn.DescribeVolumes(&ec2.DescribeVolumesInput{ + VolumeIDs: []*string{aws.String(volumeID)}, + }) + + if err != nil { + if ec2err, ok := err.(awserr.Error); ok { + // Set this to nil as if we didn't find anything. + log.Printf("Error on Volume State Refresh: message: \"%s\", code:\"%s\"", ec2err.Message(), ec2err.Code()) + resp = nil + return nil, "", err + } else { + log.Printf("Error on Volume State Refresh: %s", err) + return nil, "", err + } + } + + v := resp.Volumes[0] + return v, *v.State, nil + } +} + func resourceAwsEbsVolumeRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).ec2conn diff --git a/builtin/providers/aws/resource_aws_ebs_volume_test.go b/builtin/providers/aws/resource_aws_ebs_volume_test.go index 8a555183f..d5bc21203 100644 --- a/builtin/providers/aws/resource_aws_ebs_volume_test.go +++ b/builtin/providers/aws/resource_aws_ebs_volume_test.go @@ -1,23 +1,59 @@ package aws import ( + "fmt" "testing" + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" ) func TestAccAWSEBSVolume(t *testing.T) { + var v ec2.Volume resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ resource.TestStep{ Config: testAccAwsEbsVolumeConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckVolumeExists("aws_ebs_volume.test", &v), + ), }, }, }) } +func testAccCheckVolumeExists(n string, v *ec2.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") + } + + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + request := &ec2.DescribeVolumesInput{ + VolumeIDs: []*string{aws.String(rs.Primary.ID)}, + } + + response, err := conn.DescribeVolumes(request) + if err == nil { + if response.Volumes != nil && len(response.Volumes) > 0 { + *v = *response.Volumes[0] + return nil + } + } + return fmt.Errorf("Error finding EC2 volume %s", rs.Primary.ID) + } +} + const testAccAwsEbsVolumeConfig = ` resource "aws_ebs_volume" "test" { availability_zone = "us-west-2a" diff --git a/builtin/providers/aws/resource_aws_volume_attachment.go b/builtin/providers/aws/resource_aws_volume_attachment.go new file mode 100644 index 000000000..14a028d4e --- /dev/null +++ b/builtin/providers/aws/resource_aws_volume_attachment.go @@ -0,0 +1,191 @@ +package aws + +import ( + "bytes" + "fmt" + "log" + "time" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/aws/awserr" + "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsVolumeAttachment() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsVolumeAttachmentCreate, + Read: resourceAwsVolumeAttachmentRead, + Delete: resourceAwsVolumeAttachmentDelete, + + Schema: map[string]*schema.Schema{ + "device_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "instance_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "volume_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "force_detach": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + }, + } +} + +func resourceAwsVolumeAttachmentCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + name := d.Get("device_name").(string) + iID := d.Get("instance_id").(string) + vID := d.Get("volume_id").(string) + + opts := &ec2.AttachVolumeInput{ + Device: aws.String(name), + InstanceID: aws.String(iID), + VolumeID: aws.String(vID), + } + + log.Printf("[DEBUG] Attaching Volume (%s) to Instance (%s)", vID, iID) + _, err := conn.AttachVolume(opts) + if err != nil { + if awsErr, ok := err.(awserr.Error); ok { + return fmt.Errorf("[WARN] Error attaching volume (%s) to instance (%s), message: \"%s\", code: \"%s\"", + vID, iID, awsErr.Message(), awsErr.Code()) + } + return err + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"attaching"}, + Target: "attached", + Refresh: volumeAttachmentStateRefreshFunc(conn, vID, iID), + Timeout: 5 * 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 attach to Instance: %s, error:", + vID, iID, err) + } + + d.SetId(volumeAttachmentID(name, vID, iID)) + return resourceAwsVolumeAttachmentRead(d, meta) +} + +func volumeAttachmentStateRefreshFunc(conn *ec2.EC2, volumeID, instanceID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + + request := &ec2.DescribeVolumesInput{ + VolumeIDs: []*string{aws.String(volumeID)}, + Filters: []*ec2.Filter{ + &ec2.Filter{ + Name: aws.String("attachment.instance-id"), + Values: []*string{aws.String(instanceID)}, + }, + }, + } + + resp, err := conn.DescribeVolumes(request) + if err != nil { + if awsErr, ok := err.(awserr.Error); ok { + return nil, "failed", fmt.Errorf("code: %s, message: %s", awsErr.Code(), awsErr.Message()) + } + return nil, "failed", err + } + + if len(resp.Volumes) > 0 { + v := resp.Volumes[0] + for _, a := range v.Attachments { + if a.InstanceID != nil && *a.InstanceID == instanceID { + return a, *a.State, nil + } + } + } + // assume detached if volume count is 0 + return 42, "detached", nil + } +} +func resourceAwsVolumeAttachmentRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + request := &ec2.DescribeVolumesInput{ + VolumeIDs: []*string{aws.String(d.Get("volume_id").(string))}, + Filters: []*ec2.Filter{ + &ec2.Filter{ + Name: aws.String("attachment.instance-id"), + Values: []*string{aws.String(d.Get("instance_id").(string))}, + }, + }, + } + + _, err := conn.DescribeVolumes(request) + if err != nil { + if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidVolume.NotFound" { + d.SetId("") + return nil + } + return fmt.Errorf("Error reading EC2 volume %s for instance: %s: %#v", d.Get("volume_id").(string), d.Get("instance_id").(string), err) + } + return nil +} + +func resourceAwsVolumeAttachmentDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + vID := d.Get("volume_id").(string) + iID := d.Get("instance_id").(string) + + opts := &ec2.DetachVolumeInput{ + Device: aws.String(d.Get("device_name").(string)), + InstanceID: aws.String(iID), + VolumeID: aws.String(vID), + Force: aws.Boolean(d.Get("force_detach").(bool)), + } + + _, err := conn.DetachVolume(opts) + stateConf := &resource.StateChangeConf{ + Pending: []string{"detaching"}, + Target: "detached", + Refresh: volumeAttachmentStateRefreshFunc(conn, vID, iID), + Timeout: 5 * time.Minute, + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + + log.Printf("[DEBUG] Detaching Volume (%s) from Instance (%s)", vID, iID) + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf( + "Error waiting for Volume (%s) to detach from Instance: %s", + vID, iID) + } + d.SetId("") + return nil +} + +func volumeAttachmentID(name, volumeID, instanceID string) string { + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf("%s-", name)) + buf.WriteString(fmt.Sprintf("%s-", instanceID)) + buf.WriteString(fmt.Sprintf("%s-", volumeID)) + + return fmt.Sprintf("vai-%d", hashcode.String(buf.String())) +} diff --git a/builtin/providers/aws/resource_aws_volume_attachment_test.go b/builtin/providers/aws/resource_aws_volume_attachment_test.go new file mode 100644 index 000000000..22df8c085 --- /dev/null +++ b/builtin/providers/aws/resource_aws_volume_attachment_test.go @@ -0,0 +1,93 @@ +package aws + +import ( + "fmt" + "log" + "testing" + + "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSVolumeAttachment_basic(t *testing.T) { + var i ec2.Instance + var v ec2.Volume + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVolumeAttachmentDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccVolumeAttachmentConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "aws_volume_attachment.ebs_att", "device_name", "/dev/sdh"), + testAccCheckInstanceExists( + "aws_instance.web", &i), + testAccCheckVolumeExists( + "aws_ebs_volume.example", &v), + testAccCheckVolumeAttachmentExists( + "aws_volume_attachment.ebs_att", &i, &v), + ), + }, + }, + }) +} + +func testAccCheckVolumeAttachmentExists(n string, i *ec2.Instance, v *ec2.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") + } + + for _, b := range i.BlockDeviceMappings { + if rs.Primary.Attributes["device_name"] == *b.DeviceName { + if b.EBS.VolumeID != nil && rs.Primary.Attributes["volume_id"] == *b.EBS.VolumeID { + // pass + return nil + } + } + } + + return fmt.Errorf("Error finding instance/volume") + } +} + +func testAccCheckVolumeAttachmentDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + log.Printf("\n\n----- This is never called") + if rs.Type != "aws_volume_attachment" { + continue + } + } + return nil +} + +const testAccVolumeAttachmentConfig = ` +resource "aws_instance" "web" { + ami = "ami-21f78e11" + availability_zone = "us-west-2a" + instance_type = "t1.micro" + tags { + Name = "HelloWorld" + } +} + +resource "aws_ebs_volume" "example" { + availability_zone = "us-west-2a" + size = 1 +} + +resource "aws_volume_attachment" "ebs_att" { + device_name = "/dev/sdh" + volume_id = "${aws_ebs_volume.example.id}" + instance_id = "${aws_instance.web.id}" +} +` diff --git a/website/source/docs/providers/aws/r/volume_attachment.html.markdown b/website/source/docs/providers/aws/r/volume_attachment.html.markdown new file mode 100644 index 000000000..d3421dc8c --- /dev/null +++ b/website/source/docs/providers/aws/r/volume_attachment.html.markdown @@ -0,0 +1,57 @@ +--- +layout: "aws" +page_title: "AWS: aws_volume_attachment" +sidebar_current: "docs-aws-resource-volume-attachment" +description: |- + Provides an AWS EBS Volume Attachment +--- + +# aws\_volume\_attachment + +Provides an AWS EBS Volume Attachment as a top level resource, to attach and +detach volumes from AWS Instances. + +## Example Usage + +``` +resource "aws_volume_attachment" "ebs_att" { + device_name = "/dev/sdh" + volume_id = "${aws_ebs_volume.example.id}" + instance_id = "${aws_instance.web.id}" +} + +resource "aws_instance" "web" { + ami = "ami-21f78e11" + availability_zone = "us-west-2a" + instance_type = "t1.micro" + tags { + Name = "HelloWorld" + } +} + +resource "aws_ebs_volume" "example" { + availability_zone = "us-west-2a" + size = 1 +} +``` + +## Argument Reference + +The following arguments are supported: + +* `device_name` - (Required) The device name to expose to the instance (for +example, `/dev/sdh` or `xvdh`) +* `instance_id` - (Required) ID of the Instance to attach to +* `volume_id` - (Required) ID of the Volume to be attached +* `force_detach` - (Optional, Boolean) Set to `true` if you want to force the +volume to detach. Useful if previous attempts failed, but use this option only +as a last resort, as this can result in **data loss**. See +[Detaching an Amazon EBS Volume from an Instance][1] for more information. + +## Attributes Reference + +* `device_name` - The device name exposed to the instance +* `instance_id` - ID of the Instance +* `volume_id` - ID of the Volume + +[1]: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-detaching-volume.html diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index d2ccd1cf9..3b55600f0 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -180,6 +180,10 @@ aws_subnet + > + aws_volume_attachment + + > aws_vpc