Merge pull request #1045 from hashicorp/f-block-devices
providers/aws: rework instance block devices
This commit is contained in:
commit
46b63074e0
|
@ -24,6 +24,9 @@ func resourceAwsInstance() *schema.Resource {
|
|||
Update: resourceAwsInstanceUpdate,
|
||||
Delete: resourceAwsInstanceDelete,
|
||||
|
||||
SchemaVersion: 1,
|
||||
MigrateState: resourceAwsInstanceMigrateState,
|
||||
|
||||
Schema: map[string]*schema.Schema{
|
||||
"ami": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
|
@ -127,53 +130,28 @@ func resourceAwsInstance() *schema.Resource {
|
|||
ForceNew: true,
|
||||
Optional: true,
|
||||
},
|
||||
|
||||
"tenancy": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
"tags": tagsSchema(),
|
||||
|
||||
"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,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Elem: &schema.Resource{
|
||||
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{
|
||||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
|
@ -181,6 +159,12 @@ func resourceAwsInstance() *schema.Resource {
|
|||
ForceNew: true,
|
||||
},
|
||||
|
||||
"device_name": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
"encrypted": &schema.Schema{
|
||||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
|
@ -194,17 +178,79 @@ func resourceAwsInstance() *schema.Resource {
|
|||
Computed: 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{
|
||||
// TODO: This is a list because we don't support singleton
|
||||
// sub-resources today. We'll enforce that the list only ever has
|
||||
// TODO: This is a set because we don't support singleton
|
||||
// sub-resources today. We'll enforce that the set only ever has
|
||||
// length zero or one below. When TF gains support for
|
||||
// sub-resources this can be converted.
|
||||
Type: schema.TypeList,
|
||||
Type: schema.TypeSet,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Elem: &schema.Resource{
|
||||
|
@ -226,6 +272,13 @@ func resourceAwsInstance() *schema.Resource {
|
|||
Default: "/dev/sda1",
|
||||
},
|
||||
|
||||
"iops": &schema.Schema{
|
||||
Type: schema.TypeInt,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
"volume_size": &schema.Schema{
|
||||
Type: schema.TypeInt,
|
||||
Optional: true,
|
||||
|
@ -239,15 +292,19 @@ func resourceAwsInstance() *schema.Resource {
|
|||
Computed: 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))
|
||||
}
|
||||
|
||||
blockDevices := make([]interface{}, 0)
|
||||
blockDevices := make([]ec2.BlockDeviceMapping, 0)
|
||||
|
||||
if v := d.Get("block_device"); v != nil {
|
||||
blockDevices = append(blockDevices, v.(*schema.Set).List()...)
|
||||
}
|
||||
|
||||
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 {
|
||||
if v, ok := d.GetOk("ebs_block_device"); ok {
|
||||
vL := v.(*schema.Set).List()
|
||||
for _, v := range vL {
|
||||
bd := v.(map[string]interface{})
|
||||
runOpts.BlockDeviceMappings[i].DeviceName = aws.String(bd["device_name"].(string))
|
||||
runOpts.BlockDeviceMappings[i].EBS = &ec2.EBSBlockDevice{
|
||||
VolumeType: aws.String(bd["volume_type"].(string)),
|
||||
VolumeSize: aws.Integer(bd["volume_size"].(int)),
|
||||
ebs := &ec2.EBSBlockDevice{
|
||||
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 != "" {
|
||||
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 {
|
||||
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
|
||||
log.Printf("[DEBUG] Run configuration: %#v", runOpts)
|
||||
runResp, err := ec2conn.RunInstances(runOpts)
|
||||
|
@ -520,50 +618,10 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
|
|||
}
|
||||
d.Set("security_groups", sgs)
|
||||
|
||||
blockDevices := make(map[string]ec2.InstanceBlockDeviceMapping)
|
||||
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 {
|
||||
if err := readBlockDevices(d, instance, ec2conn); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -659,11 +717,89 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, instanceID string) resource.StateRe
|
|||
}
|
||||
}
|
||||
|
||||
func resourceAwsInstanceBlockDevicesHash(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)))
|
||||
buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool)))
|
||||
return hashcode.String(buf.String())
|
||||
func readBlockDevices(d *schema.ResourceData, instance *ec2.Instance, ec2conn *ec2.EC2) error {
|
||||
ibds, err := readBlockDevicesFromInstance(instance, ec2conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -111,31 +111,33 @@ func TestAccAWSInstance_blockDevices(t *testing.T) {
|
|||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "root_block_device.#", "1"),
|
||||
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(
|
||||
"aws_instance.foo", "root_block_device.0.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
|
||||
"aws_instance.foo", "root_block_device.3018388612.volume_size", "11"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "root_block_device.0.volume_type", "gp2"),
|
||||
"aws_instance.foo", "root_block_device.3018388612.volume_type", "gp2"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "block_device.#", "2"),
|
||||
"aws_instance.foo", "ebs_block_device.#", "2"),
|
||||
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(
|
||||
"aws_instance.foo", "block_device.172787947.volume_size", "9"),
|
||||
"aws_instance.foo", "ebs_block_device.418220885.volume_size", "9"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "block_device.172787947.iops", "0"),
|
||||
// Check provisioned SSD device
|
||||
"aws_instance.foo", "ebs_block_device.418220885.volume_type", "standard"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "block_device.3336996981.volume_type", "io1"),
|
||||
"aws_instance.foo", "ebs_block_device.1877654467.device_name", "/dev/sdc"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "block_device.3336996981.device_name", "/dev/sdc"),
|
||||
"aws_instance.foo", "ebs_block_device.1877654467.volume_size", "10"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "block_device.3336996981.volume_size", "10"),
|
||||
"aws_instance.foo", "ebs_block_device.1877654467.volume_type", "io1"),
|
||||
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(),
|
||||
),
|
||||
},
|
||||
|
@ -420,21 +422,26 @@ resource "aws_instance" "foo" {
|
|||
# us-west-2
|
||||
ami = "ami-55a7ea65"
|
||||
instance_type = "m1.small"
|
||||
|
||||
root_block_device {
|
||||
device_name = "/dev/sda1"
|
||||
volume_type = "gp2"
|
||||
volume_size = 11
|
||||
}
|
||||
block_device {
|
||||
ebs_block_device {
|
||||
device_name = "/dev/sdb"
|
||||
volume_size = 9
|
||||
}
|
||||
block_device {
|
||||
ebs_block_device {
|
||||
device_name = "/dev/sdc"
|
||||
volume_size = 10
|
||||
volume_type = "io1"
|
||||
iops = 100
|
||||
}
|
||||
ephemeral_block_device {
|
||||
device_name = "/dev/sde"
|
||||
virtual_name = "ephemeral0"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
|
|
|
@ -151,7 +151,7 @@ func (r *Resource) Apply(
|
|||
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
|
||||
|
@ -207,14 +207,7 @@ func (r *Resource) Refresh(
|
|||
state = nil
|
||||
}
|
||||
|
||||
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, err
|
||||
return r.recordCurrentSchemaVersion(state), err
|
||||
}
|
||||
|
||||
// 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"])
|
||||
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
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
|
||||
func TestResourceApply_create(t *testing.T) {
|
||||
r := &Resource{
|
||||
SchemaVersion: 2,
|
||||
Schema: map[string]*Schema{
|
||||
"foo": &Schema{
|
||||
Type: TypeInt,
|
||||
|
@ -51,6 +52,9 @@ func TestResourceApply_create(t *testing.T) {
|
|||
"id": "foo",
|
||||
"foo": "42",
|
||||
},
|
||||
Meta: map[string]string{
|
||||
"schema_version": "2",
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
|
@ -339,6 +343,7 @@ func TestResourceInternalValidate(t *testing.T) {
|
|||
|
||||
func TestResourceRefresh(t *testing.T) {
|
||||
r := &Resource{
|
||||
SchemaVersion: 2,
|
||||
Schema: map[string]*Schema{
|
||||
"foo": &Schema{
|
||||
Type: TypeInt,
|
||||
|
@ -368,6 +373,9 @@ func TestResourceRefresh(t *testing.T) {
|
|||
"id": "bar",
|
||||
"foo": "13",
|
||||
},
|
||||
Meta: map[string]string{
|
||||
"schema_version": "2",
|
||||
},
|
||||
}
|
||||
|
||||
actual, err := r.Refresh(s, 42)
|
||||
|
|
|
@ -843,6 +843,9 @@ func (i *InstanceState) init() {
|
|||
if i.Attributes == nil {
|
||||
i.Attributes = make(map[string]string)
|
||||
}
|
||||
if i.Meta == nil {
|
||||
i.Meta = make(map[string]string)
|
||||
}
|
||||
i.Ephemeral.init()
|
||||
}
|
||||
|
||||
|
@ -860,6 +863,12 @@ func (i *InstanceState) deepcopy() *InstanceState {
|
|||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,8 @@ and deleted. Instances also support [provisioning](/docs/provisioners/index.html
|
|||
## 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" {
|
||||
ami = "ami-1234"
|
||||
instance_type = "m1.small"
|
||||
|
@ -47,32 +48,79 @@ The following arguments are supported:
|
|||
* `iam_instance_profile` - (Optional) The IAM Instance Profile to
|
||||
launch the instance with.
|
||||
* `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
|
||||
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.
|
||||
* `virtual_name` - (Optional) The virtual device name.
|
||||
* `snapshot_id` - (Optional) The Snapshot ID to mount.
|
||||
* `volume_type` - (Optional) The type of volume. Can be standard, gp2, or io1. Defaults to standard.
|
||||
* `volume_size` - (Optional) The size of the volume in gigabytes.
|
||||
* `iops` - (Optional) The amount of provisioned IOPS. Setting this implies a
|
||||
volume_type of "io1".
|
||||
* `delete_on_termination` - (Optional) Should the volume be destroyed on instance termination (defaults true).
|
||||
* `encrypted` - (Optional) Should encryption be enabled (defaults false).
|
||||
<a id="block-devices"></a>
|
||||
## Block devices
|
||||
|
||||
Each of the `*_block_device` attributes controls a portion of the AWS
|
||||
Instance's "Block Device Mapping". It's a good idea to familiarize yourself with [AWS's Block Device
|
||||
Mapping docs](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html)
|
||||
to understand the implications of using these attributes.
|
||||
|
||||
The `root_block_device` mapping supports the following:
|
||||
|
||||
* `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.
|
||||
* `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.
|
||||
* `iops` - (Optional) The amount of provisioned IOPS. Setting this implies a
|
||||
volume_type of "io1".
|
||||
* `delete_on_termination` - (Optional) Should the volume be destroyed on instance termination (defaults true).
|
||||
* `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`).
|
||||
|
||||
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
|
||||
|
||||
|
|
Loading…
Reference in New Issue