Merge pull request #1045 from hashicorp/f-block-devices

providers/aws: rework instance block devices
This commit is contained in:
Paul Hinze 2015-03-19 09:09:45 -05:00
commit 46b63074e0
8 changed files with 617 additions and 163 deletions

View File

@ -24,6 +24,9 @@ func resourceAwsInstance() *schema.Resource {
Update: resourceAwsInstanceUpdate, Update: resourceAwsInstanceUpdate,
Delete: resourceAwsInstanceDelete, Delete: resourceAwsInstanceDelete,
SchemaVersion: 1,
MigrateState: resourceAwsInstanceMigrateState,
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"ami": &schema.Schema{ "ami": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
@ -127,53 +130,28 @@ func resourceAwsInstance() *schema.Resource {
ForceNew: true, ForceNew: true,
Optional: true, Optional: true,
}, },
"tenancy": &schema.Schema{ "tenancy": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
}, },
"tags": tagsSchema(), "tags": tagsSchema(),
"block_device": &schema.Schema{ "block_device": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
Removed: "Split out into three sub-types; see Changelog and Docs",
},
"ebs_block_device": &schema.Schema{
Type: schema.TypeSet, Type: schema.TypeSet,
Optional: true, Optional: true,
Computed: true, Computed: true,
Elem: &schema.Resource{ Elem: &schema.Resource{
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"device_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"virtual_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"snapshot_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"volume_type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"volume_size": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
},
"delete_on_termination": &schema.Schema{ "delete_on_termination": &schema.Schema{
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
@ -181,6 +159,12 @@ func resourceAwsInstance() *schema.Resource {
ForceNew: true, ForceNew: true,
}, },
"device_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"encrypted": &schema.Schema{ "encrypted": &schema.Schema{
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
@ -194,17 +178,79 @@ func resourceAwsInstance() *schema.Resource {
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
}, },
"snapshot_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"volume_size": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
},
"volume_type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
}, },
}, },
Set: resourceAwsInstanceBlockDevicesHash, Set: func(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool)))
buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string)))
buf.WriteString(fmt.Sprintf("%t-", m["encrypted"].(bool)))
// NOTE: Not considering IOPS in hash; when using gp2, IOPS can come
// back set to something like "33", which throws off the set
// calculation and generates an unresolvable diff.
// buf.WriteString(fmt.Sprintf("%d-", m["iops"].(int)))
buf.WriteString(fmt.Sprintf("%s-", m["snapshot_id"].(string)))
buf.WriteString(fmt.Sprintf("%d-", m["volume_size"].(int)))
buf.WriteString(fmt.Sprintf("%s-", m["volume_type"].(string)))
return hashcode.String(buf.String())
},
},
"ephemeral_block_device": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Computed: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"device_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"virtual_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
},
},
Set: func(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string)))
buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string)))
return hashcode.String(buf.String())
},
}, },
"root_block_device": &schema.Schema{ "root_block_device": &schema.Schema{
// TODO: This is a list because we don't support singleton // TODO: This is a set because we don't support singleton
// sub-resources today. We'll enforce that the list only ever has // sub-resources today. We'll enforce that the set only ever has
// length zero or one below. When TF gains support for // length zero or one below. When TF gains support for
// sub-resources this can be converted. // sub-resources this can be converted.
Type: schema.TypeList, Type: schema.TypeSet,
Optional: true, Optional: true,
Computed: true, Computed: true,
Elem: &schema.Resource{ Elem: &schema.Resource{
@ -226,6 +272,13 @@ func resourceAwsInstance() *schema.Resource {
Default: "/dev/sda1", Default: "/dev/sda1",
}, },
"iops": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
},
"volume_size": &schema.Schema{ "volume_size": &schema.Schema{
Type: schema.TypeInt, Type: schema.TypeInt,
Optional: true, Optional: true,
@ -239,15 +292,19 @@ func resourceAwsInstance() *schema.Resource {
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
}, },
"iops": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
},
}, },
}, },
Set: func(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool)))
buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string)))
// See the NOTE in "ebs_block_device" for why we skip iops here.
// buf.WriteString(fmt.Sprintf("%d-", m["iops"].(int)))
buf.WriteString(fmt.Sprintf("%d-", m["volume_size"].(int)))
buf.WriteString(fmt.Sprintf("%s-", m["volume_type"].(string)))
return hashcode.String(buf.String())
},
}, },
}, },
} }
@ -349,46 +406,87 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
runOpts.KeyName = aws.String(v.(string)) runOpts.KeyName = aws.String(v.(string))
} }
blockDevices := make([]interface{}, 0) blockDevices := make([]ec2.BlockDeviceMapping, 0)
if v := d.Get("block_device"); v != nil { if v, ok := d.GetOk("ebs_block_device"); ok {
blockDevices = append(blockDevices, v.(*schema.Set).List()...) vL := v.(*schema.Set).List()
} for _, v := range vL {
if v := d.Get("root_block_device"); v != nil {
rootBlockDevices := v.([]interface{})
if len(rootBlockDevices) > 1 {
return fmt.Errorf("Cannot specify more than one root_block_device.")
}
blockDevices = append(blockDevices, rootBlockDevices...)
}
if len(blockDevices) > 0 {
runOpts.BlockDeviceMappings = make([]ec2.BlockDeviceMapping, len(blockDevices))
for i, v := range blockDevices {
bd := v.(map[string]interface{}) bd := v.(map[string]interface{})
runOpts.BlockDeviceMappings[i].DeviceName = aws.String(bd["device_name"].(string)) ebs := &ec2.EBSBlockDevice{
runOpts.BlockDeviceMappings[i].EBS = &ec2.EBSBlockDevice{
VolumeType: aws.String(bd["volume_type"].(string)),
VolumeSize: aws.Integer(bd["volume_size"].(int)),
DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)), DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)),
} }
if v, ok := bd["virtual_name"].(string); ok {
runOpts.BlockDeviceMappings[i].VirtualName = aws.String(v)
}
if v, ok := bd["snapshot_id"].(string); ok && v != "" { if v, ok := bd["snapshot_id"].(string); ok && v != "" {
runOpts.BlockDeviceMappings[i].EBS.SnapshotID = aws.String(v) ebs.SnapshotID = aws.String(v)
} }
if v, ok := bd["encrypted"].(bool); ok {
runOpts.BlockDeviceMappings[i].EBS.Encrypted = aws.Boolean(v) if v, ok := bd["volume_size"].(int); ok && v != 0 {
ebs.VolumeSize = aws.Integer(v)
} }
if v, ok := bd["volume_type"].(string); ok && v != "" {
ebs.VolumeType = aws.String(v)
}
if v, ok := bd["iops"].(int); ok && v > 0 { if v, ok := bd["iops"].(int); ok && v > 0 {
runOpts.BlockDeviceMappings[i].EBS.IOPS = aws.Integer(v) ebs.IOPS = aws.Integer(v)
} }
blockDevices = append(blockDevices, ec2.BlockDeviceMapping{
DeviceName: aws.String(bd["device_name"].(string)),
EBS: ebs,
})
} }
} }
if v, ok := d.GetOk("ephemeral_block_device"); ok {
vL := v.(*schema.Set).List()
for _, v := range vL {
bd := v.(map[string]interface{})
blockDevices = append(blockDevices, ec2.BlockDeviceMapping{
DeviceName: aws.String(bd["device_name"].(string)),
VirtualName: aws.String(bd["virtual_name"].(string)),
})
}
// if err := d.Set("ephemeral_block_device", vL); err != nil {
// return err
// }
}
if v, ok := d.GetOk("root_block_device"); ok {
vL := v.(*schema.Set).List()
if len(vL) > 1 {
return fmt.Errorf("Cannot specify more than one root_block_device.")
}
for _, v := range vL {
bd := v.(map[string]interface{})
ebs := &ec2.EBSBlockDevice{
DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)),
}
if v, ok := bd["volume_size"].(int); ok && v != 0 {
ebs.VolumeSize = aws.Integer(v)
}
if v, ok := bd["volume_type"].(string); ok && v != "" {
ebs.VolumeType = aws.String(v)
}
if v, ok := bd["iops"].(int); ok && v > 0 {
ebs.IOPS = aws.Integer(v)
}
blockDevices = append(blockDevices, ec2.BlockDeviceMapping{
DeviceName: aws.String(bd["device_name"].(string)),
EBS: ebs,
})
}
}
if len(blockDevices) > 0 {
runOpts.BlockDeviceMappings = blockDevices
}
// Create the instance // Create the instance
log.Printf("[DEBUG] Run configuration: %#v", runOpts) log.Printf("[DEBUG] Run configuration: %#v", runOpts)
runResp, err := ec2conn.RunInstances(runOpts) runResp, err := ec2conn.RunInstances(runOpts)
@ -520,50 +618,10 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
} }
d.Set("security_groups", sgs) d.Set("security_groups", sgs)
blockDevices := make(map[string]ec2.InstanceBlockDeviceMapping) if err := readBlockDevices(d, instance, ec2conn); err != nil {
for _, bd := range instance.BlockDeviceMappings {
blockDevices[*bd.EBS.VolumeID] = bd
}
volIDs := make([]string, 0, len(blockDevices))
for _, vol := range blockDevices {
volIDs = append(volIDs, *vol.EBS.VolumeID)
}
volResp, err := ec2conn.DescribeVolumes(&ec2.DescribeVolumesRequest{
VolumeIDs: volIDs,
})
if err != nil {
return err return err
} }
nonRootBlockDevices := make([]map[string]interface{}, 0)
rootBlockDevice := make([]interface{}, 0, 1)
for _, vol := range volResp.Volumes {
blockDevice := make(map[string]interface{})
blockDevice["device_name"] = *blockDevices[*vol.VolumeID].DeviceName
blockDevice["volume_type"] = *vol.VolumeType
blockDevice["volume_size"] = *vol.Size
if vol.IOPS != nil {
blockDevice["iops"] = *vol.IOPS
}
blockDevice["delete_on_termination"] =
*blockDevices[*vol.VolumeID].EBS.DeleteOnTermination
// If this is the root device, save it. We stop here since we
// can't put invalid keys into this map.
if blockDevice["device_name"] == *instance.RootDeviceName {
rootBlockDevice = []interface{}{blockDevice}
continue
}
blockDevice["snapshot_id"] = *vol.SnapshotID
blockDevice["encrypted"] = *vol.Encrypted
nonRootBlockDevices = append(nonRootBlockDevices, blockDevice)
}
d.Set("block_device", nonRootBlockDevices)
d.Set("root_block_device", rootBlockDevice)
return nil return nil
} }
@ -659,11 +717,89 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, instanceID string) resource.StateRe
} }
} }
func resourceAwsInstanceBlockDevicesHash(v interface{}) int { func readBlockDevices(d *schema.ResourceData, instance *ec2.Instance, ec2conn *ec2.EC2) error {
var buf bytes.Buffer ibds, err := readBlockDevicesFromInstance(instance, ec2conn)
m := v.(map[string]interface{}) if err != nil {
buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) return err
buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string))) }
buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool)))
return hashcode.String(buf.String()) if err := d.Set("ebs_block_device", ibds["ebs"]); err != nil {
return err
}
if ibds["root"] != nil {
if err := d.Set("root_block_device", []interface{}{ibds["root"]}); err != nil {
return err
}
}
return nil
}
func readBlockDevicesFromInstance(instance *ec2.Instance, ec2conn *ec2.EC2) (map[string]interface{}, error) {
blockDevices := make(map[string]interface{})
blockDevices["ebs"] = make([]map[string]interface{}, 0)
blockDevices["root"] = nil
instanceBlockDevices := make(map[string]ec2.InstanceBlockDeviceMapping)
for _, bd := range instance.BlockDeviceMappings {
if bd.EBS != nil {
instanceBlockDevices[*(bd.EBS.VolumeID)] = bd
}
}
volIDs := make([]string, 0, len(instanceBlockDevices))
for volID := range instanceBlockDevices {
volIDs = append(volIDs, volID)
}
// Need to call DescribeVolumes to get volume_size and volume_type for each
// EBS block device
volResp, err := ec2conn.DescribeVolumes(&ec2.DescribeVolumesRequest{
VolumeIDs: volIDs,
})
if err != nil {
return nil, err
}
for _, vol := range volResp.Volumes {
instanceBd := instanceBlockDevices[*vol.VolumeID]
bd := make(map[string]interface{})
if instanceBd.EBS != nil && instanceBd.EBS.DeleteOnTermination != nil {
bd["delete_on_termination"] = *instanceBd.EBS.DeleteOnTermination
}
if instanceBd.DeviceName != nil {
bd["device_name"] = *instanceBd.DeviceName
}
if vol.Size != nil {
bd["volume_size"] = *vol.Size
}
if vol.VolumeType != nil {
bd["volume_type"] = *vol.VolumeType
}
if vol.IOPS != nil {
bd["iops"] = *vol.IOPS
}
if blockDeviceIsRoot(instanceBd, instance) {
blockDevices["root"] = bd
} else {
if vol.Encrypted != nil {
bd["encrypted"] = *vol.Encrypted
}
if vol.SnapshotID != nil {
bd["snapshot_id"] = *vol.SnapshotID
}
blockDevices["ebs"] = append(blockDevices["ebs"].([]map[string]interface{}), bd)
}
}
return blockDevices, nil
}
func blockDeviceIsRoot(bd ec2.InstanceBlockDeviceMapping, instance *ec2.Instance) bool {
return (bd.DeviceName != nil &&
instance.RootDeviceName != nil &&
*bd.DeviceName == *instance.RootDeviceName)
} }

View File

@ -0,0 +1,107 @@
package aws
import (
"fmt"
"log"
"strconv"
"strings"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/terraform"
)
func resourceAwsInstanceMigrateState(
v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) {
switch v {
case 0:
log.Println("[INFO] Found AWS Instance State v0; migrating to v1")
return migrateStateV0toV1(is)
default:
return is, fmt.Errorf("Unexpected schema version: %d", v)
}
return is, nil
}
func migrateStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) {
log.Printf("[DEBUG] Attributes before migration: %#v", is.Attributes)
// Delete old count
delete(is.Attributes, "block_device.#")
oldBds, err := readV0BlockDevices(is)
if err != nil {
return is, err
}
// seed count fields for new types
is.Attributes["ebs_block_device.#"] = "0"
is.Attributes["ephemeral_block_device.#"] = "0"
// depending on if state was v0.3.7 or an earlier version, it might have
// root_block_device defined already
if _, ok := is.Attributes["root_block_device.#"]; !ok {
is.Attributes["root_block_device.#"] = "0"
}
for _, oldBd := range oldBds {
if err := writeV1BlockDevice(is, oldBd); err != nil {
return is, err
}
}
log.Printf("[DEBUG] Attributes after migration: %#v", is.Attributes)
return is, nil
}
func readV0BlockDevices(is *terraform.InstanceState) (map[string]map[string]string, error) {
oldBds := make(map[string]map[string]string)
for k, v := range is.Attributes {
if !strings.HasPrefix(k, "block_device.") {
continue
}
path := strings.Split(k, ".")
if len(path) != 3 {
return oldBds, fmt.Errorf("Found unexpected block_device field: %#v", k)
}
hashcode, attribute := path[1], path[2]
oldBd, ok := oldBds[hashcode]
if !ok {
oldBd = make(map[string]string)
oldBds[hashcode] = oldBd
}
oldBd[attribute] = v
delete(is.Attributes, k)
}
return oldBds, nil
}
func writeV1BlockDevice(
is *terraform.InstanceState, oldBd map[string]string) error {
code := hashcode.String(oldBd["device_name"])
bdType := "ebs_block_device"
if vn, ok := oldBd["virtual_name"]; ok && strings.HasPrefix(vn, "ephemeral") {
bdType = "ephemeral_block_device"
} else if dn, ok := oldBd["device_name"]; ok && dn == "/dev/sda1" {
bdType = "root_block_device"
}
switch bdType {
case "ebs_block_device":
delete(oldBd, "virtual_name")
case "root_block_device":
delete(oldBd, "virtual_name")
delete(oldBd, "encrypted")
delete(oldBd, "snapshot_id")
case "ephemeral_block_device":
delete(oldBd, "delete_on_termination")
delete(oldBd, "encrypted")
delete(oldBd, "iops")
delete(oldBd, "volume_size")
delete(oldBd, "volume_type")
}
for attr, val := range oldBd {
attrKey := fmt.Sprintf("%s.%d.%s", bdType, code, attr)
is.Attributes[attrKey] = val
}
countAttr := fmt.Sprintf("%s.#", bdType)
count, _ := strconv.Atoi(is.Attributes[countAttr])
is.Attributes[countAttr] = strconv.Itoa(count + 1)
return nil
}

View File

@ -0,0 +1,135 @@
package aws
import (
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestAWSInstanceMigrateState(t *testing.T) {
cases := map[string]struct {
StateVersion int
Attributes map[string]string
Expected map[string]string
Meta interface{}
}{
"v0.3.6 and earlier": {
StateVersion: 0,
Attributes: map[string]string{
// EBS
"block_device.#": "2",
"block_device.3851383343.delete_on_termination": "true",
"block_device.3851383343.device_name": "/dev/sdx",
"block_device.3851383343.encrypted": "false",
"block_device.3851383343.snapshot_id": "",
"block_device.3851383343.virtual_name": "",
"block_device.3851383343.volume_size": "5",
"block_device.3851383343.volume_type": "standard",
// Ephemeral
"block_device.3101711606.delete_on_termination": "false",
"block_device.3101711606.device_name": "/dev/sdy",
"block_device.3101711606.encrypted": "false",
"block_device.3101711606.snapshot_id": "",
"block_device.3101711606.virtual_name": "ephemeral0",
"block_device.3101711606.volume_size": "",
"block_device.3101711606.volume_type": "",
// Root
"block_device.56575650.delete_on_termination": "true",
"block_device.56575650.device_name": "/dev/sda1",
"block_device.56575650.encrypted": "false",
"block_device.56575650.snapshot_id": "",
"block_device.56575650.volume_size": "10",
"block_device.56575650.volume_type": "standard",
},
Expected: map[string]string{
"ebs_block_device.#": "1",
"ebs_block_device.3851383343.delete_on_termination": "true",
"ebs_block_device.3851383343.device_name": "/dev/sdx",
"ebs_block_device.3851383343.encrypted": "false",
"ebs_block_device.3851383343.snapshot_id": "",
"ebs_block_device.3851383343.volume_size": "5",
"ebs_block_device.3851383343.volume_type": "standard",
"ephemeral_block_device.#": "1",
"ephemeral_block_device.2458403513.device_name": "/dev/sdy",
"ephemeral_block_device.2458403513.virtual_name": "ephemeral0",
"root_block_device.#": "1",
"root_block_device.3018388612.delete_on_termination": "true",
"root_block_device.3018388612.device_name": "/dev/sda1",
"root_block_device.3018388612.snapshot_id": "",
"root_block_device.3018388612.volume_size": "10",
"root_block_device.3018388612.volume_type": "standard",
},
},
"v0.3.7": {
StateVersion: 0,
Attributes: map[string]string{
// EBS
"block_device.#": "2",
"block_device.3851383343.delete_on_termination": "true",
"block_device.3851383343.device_name": "/dev/sdx",
"block_device.3851383343.encrypted": "false",
"block_device.3851383343.snapshot_id": "",
"block_device.3851383343.virtual_name": "",
"block_device.3851383343.volume_size": "5",
"block_device.3851383343.volume_type": "standard",
"block_device.3851383343.iops": "",
// Ephemeral
"block_device.3101711606.delete_on_termination": "false",
"block_device.3101711606.device_name": "/dev/sdy",
"block_device.3101711606.encrypted": "false",
"block_device.3101711606.snapshot_id": "",
"block_device.3101711606.virtual_name": "ephemeral0",
"block_device.3101711606.volume_size": "",
"block_device.3101711606.volume_type": "",
"block_device.3101711606.iops": "",
// Root
"root_block_device.#": "1",
"root_block_device.3018388612.delete_on_termination": "true",
"root_block_device.3018388612.device_name": "/dev/sda1",
"root_block_device.3018388612.snapshot_id": "",
"root_block_device.3018388612.volume_size": "10",
"root_block_device.3018388612.volume_type": "io1",
"root_block_device.3018388612.iops": "1000",
},
Expected: map[string]string{
"ebs_block_device.#": "1",
"ebs_block_device.3851383343.delete_on_termination": "true",
"ebs_block_device.3851383343.device_name": "/dev/sdx",
"ebs_block_device.3851383343.encrypted": "false",
"ebs_block_device.3851383343.snapshot_id": "",
"ebs_block_device.3851383343.volume_size": "5",
"ebs_block_device.3851383343.volume_type": "standard",
"ephemeral_block_device.#": "1",
"ephemeral_block_device.2458403513.device_name": "/dev/sdy",
"ephemeral_block_device.2458403513.virtual_name": "ephemeral0",
"root_block_device.#": "1",
"root_block_device.3018388612.delete_on_termination": "true",
"root_block_device.3018388612.device_name": "/dev/sda1",
"root_block_device.3018388612.snapshot_id": "",
"root_block_device.3018388612.volume_size": "10",
"root_block_device.3018388612.volume_type": "io1",
"root_block_device.3018388612.iops": "1000",
},
},
}
for tn, tc := range cases {
is := &terraform.InstanceState{
Attributes: tc.Attributes,
}
is, err := resourceAwsInstanceMigrateState(
tc.StateVersion, is, tc.Meta)
if err != nil {
t.Fatalf("bad: %s, err: %#v", tn, err)
}
for k, v := range tc.Expected {
if is.Attributes[k] != v {
t.Fatalf(
"bad: %s\n\n expected: %#v -> %#v\n got: %#v -> %#v\n in: %#v",
tn, k, v, k, is.Attributes[k], is.Attributes)
}
}
}
}

View File

@ -111,31 +111,33 @@ func TestAccAWSInstance_blockDevices(t *testing.T) {
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "root_block_device.#", "1"), "aws_instance.foo", "root_block_device.#", "1"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "root_block_device.0.device_name", "/dev/sda1"), "aws_instance.foo", "root_block_device.3018388612.device_name", "/dev/sda1"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "root_block_device.0.volume_size", "11"), "aws_instance.foo", "root_block_device.3018388612.volume_size", "11"),
// this one is important because it's the only root_block_device
// attribute that comes back from the API. so checking it verifies
// that we set state properly
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "root_block_device.0.volume_type", "gp2"), "aws_instance.foo", "root_block_device.3018388612.volume_type", "gp2"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.#", "2"), "aws_instance.foo", "ebs_block_device.#", "2"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.172787947.device_name", "/dev/sdb"), "aws_instance.foo", "ebs_block_device.418220885.device_name", "/dev/sdb"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.172787947.volume_size", "9"), "aws_instance.foo", "ebs_block_device.418220885.volume_size", "9"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.172787947.iops", "0"), "aws_instance.foo", "ebs_block_device.418220885.volume_type", "standard"),
// Check provisioned SSD device
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.3336996981.volume_type", "io1"), "aws_instance.foo", "ebs_block_device.1877654467.device_name", "/dev/sdc"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.3336996981.device_name", "/dev/sdc"), "aws_instance.foo", "ebs_block_device.1877654467.volume_size", "10"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.3336996981.volume_size", "10"), "aws_instance.foo", "ebs_block_device.1877654467.volume_type", "io1"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.3336996981.iops", "100"), "aws_instance.foo", "ebs_block_device.1877654467.iops", "100"),
resource.TestCheckResourceAttr(
"aws_instance.foo", "ephemeral_block_device.#", "1"),
resource.TestCheckResourceAttr(
"aws_instance.foo", "ephemeral_block_device.2087552357.device_name", "/dev/sde"),
resource.TestCheckResourceAttr(
"aws_instance.foo", "ephemeral_block_device.2087552357.virtual_name", "ephemeral0"),
testCheck(), testCheck(),
), ),
}, },
@ -420,21 +422,26 @@ resource "aws_instance" "foo" {
# us-west-2 # us-west-2
ami = "ami-55a7ea65" ami = "ami-55a7ea65"
instance_type = "m1.small" instance_type = "m1.small"
root_block_device { root_block_device {
device_name = "/dev/sda1" device_name = "/dev/sda1"
volume_type = "gp2" volume_type = "gp2"
volume_size = 11 volume_size = 11
} }
block_device { ebs_block_device {
device_name = "/dev/sdb" device_name = "/dev/sdb"
volume_size = 9 volume_size = 9
} }
block_device { ebs_block_device {
device_name = "/dev/sdc" device_name = "/dev/sdc"
volume_size = 10 volume_size = 10
volume_type = "io1" volume_type = "io1"
iops = 100 iops = 100
} }
ephemeral_block_device {
device_name = "/dev/sde"
virtual_name = "ephemeral0"
}
} }
` `

View File

@ -151,7 +151,7 @@ func (r *Resource) Apply(
err = r.Update(data, meta) err = r.Update(data, meta)
} }
return data.State(), err return r.recordCurrentSchemaVersion(data.State()), err
} }
// Diff returns a diff of this resource and is API compatible with the // Diff returns a diff of this resource and is API compatible with the
@ -207,14 +207,7 @@ func (r *Resource) Refresh(
state = nil state = nil
} }
if state != nil && r.SchemaVersion > 0 { return r.recordCurrentSchemaVersion(state), err
if state.Meta == nil {
state.Meta = make(map[string]string)
}
state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion)
}
return state, err
} }
// InternalValidate should be called to validate the structure // InternalValidate should be called to validate the structure
@ -241,3 +234,14 @@ func (r *Resource) checkSchemaVersion(is *terraform.InstanceState) (bool, int) {
stateSchemaVersion, _ := strconv.Atoi(is.Meta["schema_version"]) stateSchemaVersion, _ := strconv.Atoi(is.Meta["schema_version"])
return stateSchemaVersion < r.SchemaVersion, stateSchemaVersion return stateSchemaVersion < r.SchemaVersion, stateSchemaVersion
} }
func (r *Resource) recordCurrentSchemaVersion(
state *terraform.InstanceState) *terraform.InstanceState {
if state != nil && r.SchemaVersion > 0 {
if state.Meta == nil {
state.Meta = make(map[string]string)
}
state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion)
}
return state
}

View File

@ -11,6 +11,7 @@ import (
func TestResourceApply_create(t *testing.T) { func TestResourceApply_create(t *testing.T) {
r := &Resource{ r := &Resource{
SchemaVersion: 2,
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"foo": &Schema{ "foo": &Schema{
Type: TypeInt, Type: TypeInt,
@ -51,6 +52,9 @@ func TestResourceApply_create(t *testing.T) {
"id": "foo", "id": "foo",
"foo": "42", "foo": "42",
}, },
Meta: map[string]string{
"schema_version": "2",
},
} }
if !reflect.DeepEqual(actual, expected) { if !reflect.DeepEqual(actual, expected) {
@ -339,6 +343,7 @@ func TestResourceInternalValidate(t *testing.T) {
func TestResourceRefresh(t *testing.T) { func TestResourceRefresh(t *testing.T) {
r := &Resource{ r := &Resource{
SchemaVersion: 2,
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"foo": &Schema{ "foo": &Schema{
Type: TypeInt, Type: TypeInt,
@ -368,6 +373,9 @@ func TestResourceRefresh(t *testing.T) {
"id": "bar", "id": "bar",
"foo": "13", "foo": "13",
}, },
Meta: map[string]string{
"schema_version": "2",
},
} }
actual, err := r.Refresh(s, 42) actual, err := r.Refresh(s, 42)

View File

@ -843,6 +843,9 @@ func (i *InstanceState) init() {
if i.Attributes == nil { if i.Attributes == nil {
i.Attributes = make(map[string]string) i.Attributes = make(map[string]string)
} }
if i.Meta == nil {
i.Meta = make(map[string]string)
}
i.Ephemeral.init() i.Ephemeral.init()
} }
@ -860,6 +863,12 @@ func (i *InstanceState) deepcopy() *InstanceState {
n.Attributes[k] = v n.Attributes[k] = v
} }
} }
if i.Meta != nil {
n.Meta = make(map[string]string, len(i.Meta))
for k, v := range i.Meta {
n.Meta[k] = v
}
}
return n return n
} }

View File

@ -14,7 +14,8 @@ and deleted. Instances also support [provisioning](/docs/provisioners/index.html
## Example Usage ## Example Usage
``` ```
# Create a new instance of the ami-1234 on an m1.small node with an AWS Tag naming it "HelloWorld" # Create a new instance of the ami-1234 on an m1.small node
# with an AWS Tag naming it "HelloWorld"
resource "aws_instance" "web" { resource "aws_instance" "web" {
ami = "ami-1234" ami = "ami-1234"
instance_type = "m1.small" instance_type = "m1.small"
@ -47,32 +48,79 @@ The following arguments are supported:
* `iam_instance_profile` - (Optional) The IAM Instance Profile to * `iam_instance_profile` - (Optional) The IAM Instance Profile to
launch the instance with. launch the instance with.
* `tags` - (Optional) A mapping of tags to assign to the resource. * `tags` - (Optional) A mapping of tags to assign to the resource.
* `block_device` - (Optional) A list of block devices to add. Their keys are documented below.
* `root_block_device` - (Optional) Customize details about the root block * `root_block_device` - (Optional) Customize details about the root block
device of the instance. Available keys are documented below. device of the instance. See [Block Devices](#block-devices) below for details.
* `ebs_block_device` - (Optional) Additional EBS block devices to attach to the
instance. See [Block Devices](#block-devices) below for details.
* `ephemeral_block_device` - (Optional) Customize Ephemeral (also known as
"Instance Store") volumes on the instance. See [Block Devices](#block-devices) below for details.
Each `block_device` supports the following:
* `device_name` - The name of the device to mount. <a id="block-devices"></a>
* `virtual_name` - (Optional) The virtual device name. ## Block devices
* `snapshot_id` - (Optional) The Snapshot ID to mount.
* `volume_type` - (Optional) The type of volume. Can be standard, gp2, or io1. Defaults to standard. Each of the `*_block_device` attributes controls a portion of the AWS
* `volume_size` - (Optional) The size of the volume in gigabytes. Instance's "Block Device Mapping". It's a good idea to familiarize yourself with [AWS's Block Device
* `iops` - (Optional) The amount of provisioned IOPS. Setting this implies a Mapping docs](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html)
volume_type of "io1". to understand the implications of using these attributes.
* `delete_on_termination` - (Optional) Should the volume be destroyed on instance termination (defaults true).
* `encrypted` - (Optional) Should encryption be enabled (defaults false).
The `root_block_device` mapping supports the following: The `root_block_device` mapping supports the following:
* `device_name` - The name of the root device on the target instance. Must * `device_name` - The name of the root device on the target instance. Must
match the root device as defined in the AMI. Defaults to "/dev/sda1", which match the root device as defined in the AMI. Defaults to `"/dev/sda1"`, which
is the typical root volume for Linux instances. is the typical root volume for Linux instances.
* `volume_type` - (Optional) The type of volume. Can be standard, gp2, or io1. Defaults to standard. * `volume_type` - (Optional) The type of volume. Can be `"standard"`, `"gp2"`,
or `"io1"`. (Default: `"standard"`).
* `volume_size` - (Optional) The size of the volume in gigabytes. * `volume_size` - (Optional) The size of the volume in gigabytes.
* `iops` - (Optional) The amount of provisioned IOPS. Setting this implies a * `iops` - (Optional) The amount of provisioned
volume_type of "io1". [IOPS](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-io-characteristics.html).
* `delete_on_termination` - (Optional) Should the volume be destroyed on instance termination (defaults true). This must be set with a `volume_type` of `"io1"`.
* `delete_on_termination` - (Optional) Whether the volume should be destroyed
on instance termination (Default: `true`).
Modifying any of the `root_block_device` settings requires resource
replacement.
Each `ebs_block_device` supports the following:
* `device_name` - The name of the device to mount.
* `snapshot_id` - (Optional) The Snapshot ID to mount.
* `volume_type` - (Optional) The type of volume. Can be `"standard"`, `"gp2"`,
or `"io1"`. (Default: `"standard"`).
* `volume_size` - (Optional) The size of the volume in gigabytes.
* `iops` - (Optional) The amount of provisioned
[IOPS](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-io-characteristics.html).
This must be set with a `volume_type` of `"io1"`.
* `delete_on_termination` - (Optional) Whether the volume should be destroyed
on instance termination (Default: `true`).
* `encrypted` - (Optional) Enables [EBS
encryption](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html)
on the volume (Default: `false`).
Modifying any `ebs_block_device` currently requires resource replacement.
Each `ephemeral_block_device` supports the following:
* `device_name` - The name of the block device to mount on the instance.
* `virtual_name` - The [Instance Store Device
Name](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html#InstanceStoreDeviceNames)
(e.g. `"ephemeral0"`)
Each AWS Instance type has a different set of Instance Store block devices
available for attachment. AWS [publishes a
list](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html#StorageOnInstanceTypes)
of which ephemeral devices are available on each type. The devices are always
identified by the `virtual_name` in the format `"ephemeral{0..N}"`.
~> **NOTE:** Because AWS [does not expose Instance Store mapping
details](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html#bdm-instance-metadata)
via an externally accessible API, `ephemeral_block_device` configuration may
only be applied at instance creation time, and changes to configuration of
existing resources cannot be detected by Terraform. Updates to Instance Store
block device configuration can be manually triggered by using the [`taint`
command](/docs/commands/taint.html).
## Attributes Reference ## Attributes Reference