github.com/willrstern/terraform@v0.6.7-0.20151106173844-fa471ddbb53a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go (about)

     1  package openstack
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/sha1"
     6  	"encoding/hex"
     7  	"fmt"
     8  	"log"
     9  	"os"
    10  	"time"
    11  
    12  	"github.com/hashicorp/terraform/helper/hashcode"
    13  	"github.com/hashicorp/terraform/helper/resource"
    14  	"github.com/hashicorp/terraform/helper/schema"
    15  	"github.com/rackspace/gophercloud"
    16  	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
    17  	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip"
    18  	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
    19  	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/schedulerhints"
    20  	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups"
    21  	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks"
    22  	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach"
    23  	"github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
    24  	"github.com/rackspace/gophercloud/openstack/compute/v2/images"
    25  	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
    26  	"github.com/rackspace/gophercloud/pagination"
    27  )
    28  
    29  func resourceComputeInstanceV2() *schema.Resource {
    30  	return &schema.Resource{
    31  		Create: resourceComputeInstanceV2Create,
    32  		Read:   resourceComputeInstanceV2Read,
    33  		Update: resourceComputeInstanceV2Update,
    34  		Delete: resourceComputeInstanceV2Delete,
    35  
    36  		Schema: map[string]*schema.Schema{
    37  			"region": &schema.Schema{
    38  				Type:        schema.TypeString,
    39  				Required:    true,
    40  				ForceNew:    true,
    41  				DefaultFunc: envDefaultFuncAllowMissing("OS_REGION_NAME"),
    42  			},
    43  			"name": &schema.Schema{
    44  				Type:     schema.TypeString,
    45  				Required: true,
    46  				ForceNew: false,
    47  			},
    48  			"image_id": &schema.Schema{
    49  				Type:     schema.TypeString,
    50  				Optional: true,
    51  				ForceNew: true,
    52  				Computed: true,
    53  			},
    54  			"image_name": &schema.Schema{
    55  				Type:     schema.TypeString,
    56  				Optional: true,
    57  				ForceNew: true,
    58  				Computed: true,
    59  			},
    60  			"flavor_id": &schema.Schema{
    61  				Type:        schema.TypeString,
    62  				Optional:    true,
    63  				ForceNew:    false,
    64  				Computed:    true,
    65  				DefaultFunc: envDefaultFunc("OS_FLAVOR_ID"),
    66  			},
    67  			"flavor_name": &schema.Schema{
    68  				Type:        schema.TypeString,
    69  				Optional:    true,
    70  				ForceNew:    false,
    71  				Computed:    true,
    72  				DefaultFunc: envDefaultFunc("OS_FLAVOR_NAME"),
    73  			},
    74  			"floating_ip": &schema.Schema{
    75  				Type:     schema.TypeString,
    76  				Optional: true,
    77  				ForceNew: false,
    78  			},
    79  			"user_data": &schema.Schema{
    80  				Type:     schema.TypeString,
    81  				Optional: true,
    82  				ForceNew: true,
    83  				// just stash the hash for state & diff comparisons
    84  				StateFunc: func(v interface{}) string {
    85  					switch v.(type) {
    86  					case string:
    87  						hash := sha1.Sum([]byte(v.(string)))
    88  						return hex.EncodeToString(hash[:])
    89  					default:
    90  						return ""
    91  					}
    92  				},
    93  			},
    94  			"security_groups": &schema.Schema{
    95  				Type:     schema.TypeSet,
    96  				Optional: true,
    97  				ForceNew: false,
    98  				Elem:     &schema.Schema{Type: schema.TypeString},
    99  				Set:      schema.HashString,
   100  			},
   101  			"availability_zone": &schema.Schema{
   102  				Type:     schema.TypeString,
   103  				Optional: true,
   104  				ForceNew: true,
   105  			},
   106  			"network": &schema.Schema{
   107  				Type:     schema.TypeList,
   108  				Optional: true,
   109  				ForceNew: true,
   110  				Computed: true,
   111  				Elem: &schema.Resource{
   112  					Schema: map[string]*schema.Schema{
   113  						"uuid": &schema.Schema{
   114  							Type:     schema.TypeString,
   115  							Optional: true,
   116  							Computed: true,
   117  						},
   118  						"name": &schema.Schema{
   119  							Type:     schema.TypeString,
   120  							Optional: true,
   121  							Computed: true,
   122  						},
   123  						"port": &schema.Schema{
   124  							Type:     schema.TypeString,
   125  							Optional: true,
   126  							Computed: true,
   127  						},
   128  						"fixed_ip_v4": &schema.Schema{
   129  							Type:     schema.TypeString,
   130  							Optional: true,
   131  							Computed: true,
   132  						},
   133  						"fixed_ip_v6": &schema.Schema{
   134  							Type:     schema.TypeString,
   135  							Optional: true,
   136  							Computed: true,
   137  						},
   138  						"mac": &schema.Schema{
   139  							Type:     schema.TypeString,
   140  							Computed: true,
   141  						},
   142  					},
   143  				},
   144  			},
   145  			"metadata": &schema.Schema{
   146  				Type:     schema.TypeMap,
   147  				Optional: true,
   148  				ForceNew: false,
   149  			},
   150  			"config_drive": &schema.Schema{
   151  				Type:     schema.TypeBool,
   152  				Optional: true,
   153  				ForceNew: true,
   154  			},
   155  			"admin_pass": &schema.Schema{
   156  				Type:     schema.TypeString,
   157  				Optional: true,
   158  				ForceNew: false,
   159  			},
   160  			"access_ip_v4": &schema.Schema{
   161  				Type:     schema.TypeString,
   162  				Computed: true,
   163  				Optional: true,
   164  				ForceNew: false,
   165  			},
   166  			"access_ip_v6": &schema.Schema{
   167  				Type:     schema.TypeString,
   168  				Computed: true,
   169  				Optional: true,
   170  				ForceNew: false,
   171  			},
   172  			"key_pair": &schema.Schema{
   173  				Type:     schema.TypeString,
   174  				Optional: true,
   175  				ForceNew: true,
   176  			},
   177  			"block_device": &schema.Schema{
   178  				// TODO: This is a set because we don't support singleton
   179  				//       sub-resources today. We'll enforce that the set only ever has
   180  				//       length zero or one below. When TF gains support for
   181  				//       sub-resources this can be converted.
   182  				//       As referenced in resource_aws_instance.go
   183  				Type:     schema.TypeSet,
   184  				Optional: true,
   185  				ForceNew: true,
   186  				Elem: &schema.Resource{
   187  					Schema: map[string]*schema.Schema{
   188  						"uuid": &schema.Schema{
   189  							Type:     schema.TypeString,
   190  							Required: true,
   191  						},
   192  						"source_type": &schema.Schema{
   193  							Type:     schema.TypeString,
   194  							Required: true,
   195  						},
   196  						"volume_size": &schema.Schema{
   197  							Type:     schema.TypeInt,
   198  							Optional: true,
   199  						},
   200  						"destination_type": &schema.Schema{
   201  							Type:     schema.TypeString,
   202  							Optional: true,
   203  						},
   204  						"boot_index": &schema.Schema{
   205  							Type:     schema.TypeInt,
   206  							Optional: true,
   207  						},
   208  						"delete_on_termination": &schema.Schema{
   209  							Type:     schema.TypeBool,
   210  							Optional: true,
   211  							Default:  false,
   212  						},
   213  					},
   214  				},
   215  				Set: func(v interface{}) int {
   216  					// there can only be one bootable block device; no need to hash anything
   217  					return 0
   218  				},
   219  			},
   220  			"volume": &schema.Schema{
   221  				Type:     schema.TypeSet,
   222  				Optional: true,
   223  				Computed: true,
   224  				Elem: &schema.Resource{
   225  					Schema: map[string]*schema.Schema{
   226  						"id": &schema.Schema{
   227  							Type:     schema.TypeString,
   228  							Computed: true,
   229  						},
   230  						"volume_id": &schema.Schema{
   231  							Type:     schema.TypeString,
   232  							Optional: true,
   233  							Computed: true,
   234  						},
   235  						"device": &schema.Schema{
   236  							Type:     schema.TypeString,
   237  							Optional: true,
   238  							Computed: true,
   239  						},
   240  					},
   241  				},
   242  				Set: resourceComputeVolumeAttachmentHash,
   243  			},
   244  			"scheduler_hints": &schema.Schema{
   245  				Type:     schema.TypeSet,
   246  				Optional: true,
   247  				Elem: &schema.Resource{
   248  					Schema: map[string]*schema.Schema{
   249  						"group": &schema.Schema{
   250  							Type:     schema.TypeString,
   251  							Optional: true,
   252  							ForceNew: true,
   253  						},
   254  						"different_host": &schema.Schema{
   255  							Type:     schema.TypeList,
   256  							Optional: true,
   257  							ForceNew: true,
   258  							Elem:     &schema.Schema{Type: schema.TypeString},
   259  						},
   260  						"same_host": &schema.Schema{
   261  							Type:     schema.TypeList,
   262  							Optional: true,
   263  							ForceNew: true,
   264  							Elem:     &schema.Schema{Type: schema.TypeString},
   265  						},
   266  						"query": &schema.Schema{
   267  							Type:     schema.TypeList,
   268  							Optional: true,
   269  							ForceNew: true,
   270  							Elem:     &schema.Schema{Type: schema.TypeString},
   271  						},
   272  						"target_cell": &schema.Schema{
   273  							Type:     schema.TypeString,
   274  							Optional: true,
   275  							ForceNew: true,
   276  						},
   277  						"build_near_host_ip": &schema.Schema{
   278  							Type:     schema.TypeString,
   279  							Optional: true,
   280  							ForceNew: true,
   281  						},
   282  					},
   283  				},
   284  				Set: resourceComputeSchedulerHintsHash,
   285  			},
   286  		},
   287  	}
   288  }
   289  
   290  func resourceComputeInstanceV2Create(d *schema.ResourceData, meta interface{}) error {
   291  	config := meta.(*Config)
   292  	computeClient, err := config.computeV2Client(d.Get("region").(string))
   293  	if err != nil {
   294  		return fmt.Errorf("Error creating OpenStack compute client: %s", err)
   295  	}
   296  
   297  	var createOpts servers.CreateOptsBuilder
   298  
   299  	// Determines the Image ID using the following rules:
   300  	// If a bootable block_device was specified, ignore the image altogether.
   301  	// If an image_id was specified, use it.
   302  	// If an image_name was specified, look up the image ID, report if error.
   303  	imageId, err := getImageIDFromConfig(computeClient, d)
   304  	if err != nil {
   305  		return err
   306  	}
   307  
   308  	flavorId, err := getFlavorID(computeClient, d)
   309  	if err != nil {
   310  		return err
   311  	}
   312  
   313  	networkDetails, err := resourceInstanceNetworks(computeClient, d)
   314  	if err != nil {
   315  		return err
   316  	}
   317  
   318  	// determine if volume/block_device configuration is correct
   319  	// this includes ensuring volume_ids are set
   320  	// and if only one block_device was specified.
   321  	if err := checkVolumeConfig(d); err != nil {
   322  		return err
   323  	}
   324  
   325  	networks := make([]servers.Network, len(networkDetails))
   326  	for i, net := range networkDetails {
   327  		networks[i] = servers.Network{
   328  			UUID:    net["uuid"].(string),
   329  			Port:    net["port"].(string),
   330  			FixedIP: net["fixed_ip_v4"].(string),
   331  		}
   332  	}
   333  
   334  	createOpts = &servers.CreateOpts{
   335  		Name:             d.Get("name").(string),
   336  		ImageRef:         imageId,
   337  		FlavorRef:        flavorId,
   338  		SecurityGroups:   resourceInstanceSecGroupsV2(d),
   339  		AvailabilityZone: d.Get("availability_zone").(string),
   340  		Networks:         networks,
   341  		Metadata:         resourceInstanceMetadataV2(d),
   342  		ConfigDrive:      d.Get("config_drive").(bool),
   343  		AdminPass:        d.Get("admin_pass").(string),
   344  		UserData:         []byte(d.Get("user_data").(string)),
   345  	}
   346  
   347  	if keyName, ok := d.Get("key_pair").(string); ok && keyName != "" {
   348  		createOpts = &keypairs.CreateOptsExt{
   349  			createOpts,
   350  			keyName,
   351  		}
   352  	}
   353  
   354  	if v, ok := d.GetOk("block_device"); ok {
   355  		vL := v.(*schema.Set).List()
   356  		for _, v := range vL {
   357  			blockDeviceRaw := v.(map[string]interface{})
   358  			blockDevice := resourceInstanceBlockDeviceV2(d, blockDeviceRaw)
   359  			createOpts = &bootfromvolume.CreateOptsExt{
   360  				createOpts,
   361  				blockDevice,
   362  			}
   363  			log.Printf("[DEBUG] Create BFV Options: %+v", createOpts)
   364  		}
   365  	}
   366  
   367  	schedulerHintsRaw := d.Get("scheduler_hints").(*schema.Set).List()
   368  	if len(schedulerHintsRaw) > 0 {
   369  		log.Printf("[DEBUG] schedulerhints: %+v", schedulerHintsRaw)
   370  		schedulerHints := resourceInstanceSchedulerHintsV2(d, schedulerHintsRaw[0].(map[string]interface{}))
   371  		createOpts = &schedulerhints.CreateOptsExt{
   372  			createOpts,
   373  			schedulerHints,
   374  		}
   375  	}
   376  
   377  	log.Printf("[DEBUG] Create Options: %#v", createOpts)
   378  
   379  	// If a block_device is used, use the bootfromvolume.Create function as it allows an empty ImageRef.
   380  	// Otherwise, use the normal servers.Create function.
   381  	var server *servers.Server
   382  	if _, ok := d.GetOk("block_device"); ok {
   383  		server, err = bootfromvolume.Create(computeClient, createOpts).Extract()
   384  	} else {
   385  		server, err = servers.Create(computeClient, createOpts).Extract()
   386  	}
   387  
   388  	if err != nil {
   389  		return fmt.Errorf("Error creating OpenStack server: %s", err)
   390  	}
   391  	log.Printf("[INFO] Instance ID: %s", server.ID)
   392  
   393  	// Store the ID now
   394  	d.SetId(server.ID)
   395  
   396  	// Wait for the instance to become running so we can get some attributes
   397  	// that aren't available until later.
   398  	log.Printf(
   399  		"[DEBUG] Waiting for instance (%s) to become running",
   400  		server.ID)
   401  
   402  	stateConf := &resource.StateChangeConf{
   403  		Pending:    []string{"BUILD"},
   404  		Target:     "ACTIVE",
   405  		Refresh:    ServerV2StateRefreshFunc(computeClient, server.ID),
   406  		Timeout:    10 * time.Minute,
   407  		Delay:      10 * time.Second,
   408  		MinTimeout: 3 * time.Second,
   409  	}
   410  
   411  	_, err = stateConf.WaitForState()
   412  	if err != nil {
   413  		return fmt.Errorf(
   414  			"Error waiting for instance (%s) to become ready: %s",
   415  			server.ID, err)
   416  	}
   417  	floatingIP := d.Get("floating_ip").(string)
   418  	if floatingIP != "" {
   419  		if err := floatingip.Associate(computeClient, server.ID, floatingIP).ExtractErr(); err != nil {
   420  			return fmt.Errorf("Error associating floating IP: %s", err)
   421  		}
   422  	}
   423  
   424  	// if volumes were specified, attach them after the instance has launched.
   425  	if v, ok := d.GetOk("volume"); ok {
   426  		vols := v.(*schema.Set).List()
   427  		if blockClient, err := config.blockStorageV1Client(d.Get("region").(string)); err != nil {
   428  			return fmt.Errorf("Error creating OpenStack block storage client: %s", err)
   429  		} else {
   430  			if err := attachVolumesToInstance(computeClient, blockClient, d.Id(), vols); err != nil {
   431  				return err
   432  			}
   433  		}
   434  	}
   435  
   436  	return resourceComputeInstanceV2Read(d, meta)
   437  }
   438  
   439  func resourceComputeInstanceV2Read(d *schema.ResourceData, meta interface{}) error {
   440  	config := meta.(*Config)
   441  	computeClient, err := config.computeV2Client(d.Get("region").(string))
   442  	if err != nil {
   443  		return fmt.Errorf("Error creating OpenStack compute client: %s", err)
   444  	}
   445  
   446  	server, err := servers.Get(computeClient, d.Id()).Extract()
   447  	if err != nil {
   448  		return CheckDeleted(d, err, "server")
   449  	}
   450  
   451  	log.Printf("[DEBUG] Retreived Server %s: %+v", d.Id(), server)
   452  
   453  	d.Set("name", server.Name)
   454  
   455  	// begin reading the network configuration
   456  	d.Set("access_ip_v4", server.AccessIPv4)
   457  	d.Set("access_ip_v6", server.AccessIPv6)
   458  	hostv4 := server.AccessIPv4
   459  	hostv6 := server.AccessIPv6
   460  
   461  	networkDetails, err := resourceInstanceNetworks(computeClient, d)
   462  	addresses := resourceInstanceAddresses(server.Addresses)
   463  	if err != nil {
   464  		return err
   465  	}
   466  
   467  	// if there are no networkDetails, make networks at least a length of 1
   468  	networkLength := 1
   469  	if len(networkDetails) > 0 {
   470  		networkLength = len(networkDetails)
   471  	}
   472  	networks := make([]map[string]interface{}, networkLength)
   473  
   474  	// Loop through all networks and addresses,
   475  	// merge relevant address details.
   476  	if len(networkDetails) == 0 {
   477  		for netName, n := range addresses {
   478  			if floatingIP, ok := n["floating_ip"]; ok {
   479  				hostv4 = floatingIP.(string)
   480  			} else {
   481  				if hostv4 == "" && n["fixed_ip_v4"] != nil {
   482  					hostv4 = n["fixed_ip_v4"].(string)
   483  				}
   484  			}
   485  
   486  			if hostv6 == "" && n["fixed_ip_v6"] != nil {
   487  				hostv6 = n["fixed_ip_v6"].(string)
   488  			}
   489  
   490  			networks[0] = map[string]interface{}{
   491  				"name":        netName,
   492  				"fixed_ip_v4": n["fixed_ip_v4"],
   493  				"fixed_ip_v6": n["fixed_ip_v6"],
   494  				"mac":         n["mac"],
   495  			}
   496  		}
   497  	} else {
   498  		for i, net := range networkDetails {
   499  			n := addresses[net["name"].(string)]
   500  
   501  			if floatingIP, ok := n["floating_ip"]; ok {
   502  				hostv4 = floatingIP.(string)
   503  			} else {
   504  				if hostv4 == "" && n["fixed_ip_v4"] != nil {
   505  					hostv4 = n["fixed_ip_v4"].(string)
   506  				}
   507  			}
   508  
   509  			if hostv6 == "" && n["fixed_ip_v6"] != nil {
   510  				hostv6 = n["fixed_ip_v6"].(string)
   511  			}
   512  
   513  			networks[i] = map[string]interface{}{
   514  				"uuid":        networkDetails[i]["uuid"],
   515  				"name":        networkDetails[i]["name"],
   516  				"port":        networkDetails[i]["port"],
   517  				"fixed_ip_v4": n["fixed_ip_v4"],
   518  				"fixed_ip_v6": n["fixed_ip_v6"],
   519  				"mac":         n["mac"],
   520  			}
   521  		}
   522  	}
   523  
   524  	log.Printf("[DEBUG] new networks: %+v", networks)
   525  
   526  	d.Set("network", networks)
   527  	d.Set("access_ip_v4", hostv4)
   528  	d.Set("access_ip_v6", hostv6)
   529  	log.Printf("hostv4: %s", hostv4)
   530  	log.Printf("hostv6: %s", hostv6)
   531  
   532  	// prefer the v6 address if no v4 address exists.
   533  	preferredv := ""
   534  	if hostv4 != "" {
   535  		preferredv = hostv4
   536  	} else if hostv6 != "" {
   537  		preferredv = hostv6
   538  	}
   539  
   540  	if preferredv != "" {
   541  		// Initialize the connection info
   542  		d.SetConnInfo(map[string]string{
   543  			"type": "ssh",
   544  			"host": preferredv,
   545  		})
   546  	}
   547  	// end network configuration
   548  
   549  	d.Set("metadata", server.Metadata)
   550  
   551  	secGrpNames := []string{}
   552  	for _, sg := range server.SecurityGroups {
   553  		secGrpNames = append(secGrpNames, sg["name"].(string))
   554  	}
   555  	d.Set("security_groups", secGrpNames)
   556  
   557  	flavorId, ok := server.Flavor["id"].(string)
   558  	if !ok {
   559  		return fmt.Errorf("Error setting OpenStack server's flavor: %v", server.Flavor)
   560  	}
   561  	d.Set("flavor_id", flavorId)
   562  
   563  	flavor, err := flavors.Get(computeClient, flavorId).Extract()
   564  	if err != nil {
   565  		return err
   566  	}
   567  	d.Set("flavor_name", flavor.Name)
   568  
   569  	// Set the instance's image information appropriately
   570  	if err := setImageInformation(computeClient, server, d); err != nil {
   571  		return err
   572  	}
   573  
   574  	// volume attachments
   575  	if err := getVolumeAttachments(computeClient, d); err != nil {
   576  		return err
   577  	}
   578  
   579  	return nil
   580  }
   581  
   582  func resourceComputeInstanceV2Update(d *schema.ResourceData, meta interface{}) error {
   583  	config := meta.(*Config)
   584  	computeClient, err := config.computeV2Client(d.Get("region").(string))
   585  	if err != nil {
   586  		return fmt.Errorf("Error creating OpenStack compute client: %s", err)
   587  	}
   588  
   589  	var updateOpts servers.UpdateOpts
   590  	if d.HasChange("name") {
   591  		updateOpts.Name = d.Get("name").(string)
   592  	}
   593  	if d.HasChange("access_ip_v4") {
   594  		updateOpts.AccessIPv4 = d.Get("access_ip_v4").(string)
   595  	}
   596  	if d.HasChange("access_ip_v6") {
   597  		updateOpts.AccessIPv4 = d.Get("access_ip_v6").(string)
   598  	}
   599  
   600  	if updateOpts != (servers.UpdateOpts{}) {
   601  		_, err := servers.Update(computeClient, d.Id(), updateOpts).Extract()
   602  		if err != nil {
   603  			return fmt.Errorf("Error updating OpenStack server: %s", err)
   604  		}
   605  	}
   606  
   607  	if d.HasChange("metadata") {
   608  		var metadataOpts servers.MetadataOpts
   609  		metadataOpts = make(servers.MetadataOpts)
   610  		newMetadata := d.Get("metadata").(map[string]interface{})
   611  		for k, v := range newMetadata {
   612  			metadataOpts[k] = v.(string)
   613  		}
   614  
   615  		_, err := servers.UpdateMetadata(computeClient, d.Id(), metadataOpts).Extract()
   616  		if err != nil {
   617  			return fmt.Errorf("Error updating OpenStack server (%s) metadata: %s", d.Id(), err)
   618  		}
   619  	}
   620  
   621  	if d.HasChange("security_groups") {
   622  		oldSGRaw, newSGRaw := d.GetChange("security_groups")
   623  		oldSGSet := oldSGRaw.(*schema.Set)
   624  		newSGSet := newSGRaw.(*schema.Set)
   625  		secgroupsToAdd := newSGSet.Difference(oldSGSet)
   626  		secgroupsToRemove := oldSGSet.Difference(newSGSet)
   627  
   628  		log.Printf("[DEBUG] Security groups to add: %v", secgroupsToAdd)
   629  
   630  		log.Printf("[DEBUG] Security groups to remove: %v", secgroupsToRemove)
   631  
   632  		for _, g := range secgroupsToRemove.List() {
   633  			err := secgroups.RemoveServerFromGroup(computeClient, d.Id(), g.(string)).ExtractErr()
   634  			if err != nil {
   635  				errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError)
   636  				if !ok {
   637  					return fmt.Errorf("Error removing security group from OpenStack server (%s): %s", d.Id(), err)
   638  				}
   639  				if errCode.Actual == 404 {
   640  					continue
   641  				} else {
   642  					return fmt.Errorf("Error removing security group from OpenStack server (%s): %s", d.Id(), err)
   643  				}
   644  			} else {
   645  				log.Printf("[DEBUG] Removed security group (%s) from instance (%s)", g.(string), d.Id())
   646  			}
   647  		}
   648  		for _, g := range secgroupsToAdd.List() {
   649  			err := secgroups.AddServerToGroup(computeClient, d.Id(), g.(string)).ExtractErr()
   650  			if err != nil {
   651  				return fmt.Errorf("Error adding security group to OpenStack server (%s): %s", d.Id(), err)
   652  			}
   653  			log.Printf("[DEBUG] Added security group (%s) to instance (%s)", g.(string), d.Id())
   654  		}
   655  
   656  	}
   657  
   658  	if d.HasChange("admin_pass") {
   659  		if newPwd, ok := d.Get("admin_pass").(string); ok {
   660  			err := servers.ChangeAdminPassword(computeClient, d.Id(), newPwd).ExtractErr()
   661  			if err != nil {
   662  				return fmt.Errorf("Error changing admin password of OpenStack server (%s): %s", d.Id(), err)
   663  			}
   664  		}
   665  	}
   666  
   667  	if d.HasChange("floating_ip") {
   668  		oldFIP, newFIP := d.GetChange("floating_ip")
   669  		log.Printf("[DEBUG] Old Floating IP: %v", oldFIP)
   670  		log.Printf("[DEBUG] New Floating IP: %v", newFIP)
   671  		if oldFIP.(string) != "" {
   672  			log.Printf("[DEBUG] Attemping to disassociate %s from %s", oldFIP, d.Id())
   673  			if err := floatingip.Disassociate(computeClient, d.Id(), oldFIP.(string)).ExtractErr(); err != nil {
   674  				return fmt.Errorf("Error disassociating Floating IP during update: %s", err)
   675  			}
   676  		}
   677  
   678  		if newFIP.(string) != "" {
   679  			log.Printf("[DEBUG] Attemping to associate %s to %s", newFIP, d.Id())
   680  			if err := floatingip.Associate(computeClient, d.Id(), newFIP.(string)).ExtractErr(); err != nil {
   681  				return fmt.Errorf("Error associating Floating IP during update: %s", err)
   682  			}
   683  		}
   684  	}
   685  
   686  	if d.HasChange("volume") {
   687  		// ensure the volume configuration is correct
   688  		if err := checkVolumeConfig(d); err != nil {
   689  			return err
   690  		}
   691  
   692  		// old attachments and new attachments
   693  		oldAttachments, newAttachments := d.GetChange("volume")
   694  
   695  		// for each old attachment, detach the volume
   696  		oldAttachmentSet := oldAttachments.(*schema.Set).List()
   697  		if blockClient, err := config.blockStorageV1Client(d.Get("region").(string)); err != nil {
   698  			return err
   699  		} else {
   700  			if err := detachVolumesFromInstance(computeClient, blockClient, d.Id(), oldAttachmentSet); err != nil {
   701  				return err
   702  			}
   703  		}
   704  
   705  		// for each new attachment, attach the volume
   706  		newAttachmentSet := newAttachments.(*schema.Set).List()
   707  		if blockClient, err := config.blockStorageV1Client(d.Get("region").(string)); err != nil {
   708  			return err
   709  		} else {
   710  			if err := attachVolumesToInstance(computeClient, blockClient, d.Id(), newAttachmentSet); err != nil {
   711  				return err
   712  			}
   713  		}
   714  
   715  		d.SetPartial("volume")
   716  	}
   717  
   718  	if d.HasChange("flavor_id") || d.HasChange("flavor_name") {
   719  		flavorId, err := getFlavorID(computeClient, d)
   720  		if err != nil {
   721  			return err
   722  		}
   723  		resizeOpts := &servers.ResizeOpts{
   724  			FlavorRef: flavorId,
   725  		}
   726  		log.Printf("[DEBUG] Resize configuration: %#v", resizeOpts)
   727  		err = servers.Resize(computeClient, d.Id(), resizeOpts).ExtractErr()
   728  		if err != nil {
   729  			return fmt.Errorf("Error resizing OpenStack server: %s", err)
   730  		}
   731  
   732  		// Wait for the instance to finish resizing.
   733  		log.Printf("[DEBUG] Waiting for instance (%s) to finish resizing", d.Id())
   734  
   735  		stateConf := &resource.StateChangeConf{
   736  			Pending:    []string{"RESIZE"},
   737  			Target:     "VERIFY_RESIZE",
   738  			Refresh:    ServerV2StateRefreshFunc(computeClient, d.Id()),
   739  			Timeout:    3 * time.Minute,
   740  			Delay:      10 * time.Second,
   741  			MinTimeout: 3 * time.Second,
   742  		}
   743  
   744  		_, err = stateConf.WaitForState()
   745  		if err != nil {
   746  			return fmt.Errorf("Error waiting for instance (%s) to resize: %s", d.Id(), err)
   747  		}
   748  
   749  		// Confirm resize.
   750  		log.Printf("[DEBUG] Confirming resize")
   751  		err = servers.ConfirmResize(computeClient, d.Id()).ExtractErr()
   752  		if err != nil {
   753  			return fmt.Errorf("Error confirming resize of OpenStack server: %s", err)
   754  		}
   755  
   756  		stateConf = &resource.StateChangeConf{
   757  			Pending:    []string{"VERIFY_RESIZE"},
   758  			Target:     "ACTIVE",
   759  			Refresh:    ServerV2StateRefreshFunc(computeClient, d.Id()),
   760  			Timeout:    3 * time.Minute,
   761  			Delay:      10 * time.Second,
   762  			MinTimeout: 3 * time.Second,
   763  		}
   764  
   765  		_, err = stateConf.WaitForState()
   766  		if err != nil {
   767  			return fmt.Errorf("Error waiting for instance (%s) to confirm resize: %s", d.Id(), err)
   768  		}
   769  	}
   770  
   771  	return resourceComputeInstanceV2Read(d, meta)
   772  }
   773  
   774  func resourceComputeInstanceV2Delete(d *schema.ResourceData, meta interface{}) error {
   775  	config := meta.(*Config)
   776  	computeClient, err := config.computeV2Client(d.Get("region").(string))
   777  	if err != nil {
   778  		return fmt.Errorf("Error creating OpenStack compute client: %s", err)
   779  	}
   780  
   781  	err = servers.Delete(computeClient, d.Id()).ExtractErr()
   782  	if err != nil {
   783  		return fmt.Errorf("Error deleting OpenStack server: %s", err)
   784  	}
   785  
   786  	// Wait for the instance to delete before moving on.
   787  	log.Printf("[DEBUG] Waiting for instance (%s) to delete", d.Id())
   788  
   789  	stateConf := &resource.StateChangeConf{
   790  		Pending:    []string{"ACTIVE"},
   791  		Target:     "DELETED",
   792  		Refresh:    ServerV2StateRefreshFunc(computeClient, d.Id()),
   793  		Timeout:    10 * time.Minute,
   794  		Delay:      10 * time.Second,
   795  		MinTimeout: 3 * time.Second,
   796  	}
   797  
   798  	_, err = stateConf.WaitForState()
   799  	if err != nil {
   800  		return fmt.Errorf(
   801  			"Error waiting for instance (%s) to delete: %s",
   802  			d.Id(), err)
   803  	}
   804  
   805  	d.SetId("")
   806  	return nil
   807  }
   808  
   809  // ServerV2StateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
   810  // an OpenStack instance.
   811  func ServerV2StateRefreshFunc(client *gophercloud.ServiceClient, instanceID string) resource.StateRefreshFunc {
   812  	return func() (interface{}, string, error) {
   813  		s, err := servers.Get(client, instanceID).Extract()
   814  		if err != nil {
   815  			errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError)
   816  			if !ok {
   817  				return nil, "", err
   818  			}
   819  			if errCode.Actual == 404 {
   820  				return s, "DELETED", nil
   821  			}
   822  			return nil, "", err
   823  		}
   824  
   825  		return s, s.Status, nil
   826  	}
   827  }
   828  
   829  func resourceInstanceSecGroupsV2(d *schema.ResourceData) []string {
   830  	rawSecGroups := d.Get("security_groups").(*schema.Set).List()
   831  	secgroups := make([]string, len(rawSecGroups))
   832  	for i, raw := range rawSecGroups {
   833  		secgroups[i] = raw.(string)
   834  	}
   835  	return secgroups
   836  }
   837  
   838  func resourceInstanceNetworks(computeClient *gophercloud.ServiceClient, d *schema.ResourceData) ([]map[string]interface{}, error) {
   839  	rawNetworks := d.Get("network").([]interface{})
   840  	newNetworks := make([]map[string]interface{}, 0, len(rawNetworks))
   841  	var tenantnet tenantnetworks.Network
   842  
   843  	tenantNetworkExt := true
   844  	for _, raw := range rawNetworks {
   845  		// Not sure what causes this, but it is a possibility (see GH-2323).
   846  		// Since we call this function to reconcile what we'll save in the
   847  		// state anyways, we just ignore it.
   848  		if raw == nil {
   849  			continue
   850  		}
   851  
   852  		rawMap := raw.(map[string]interface{})
   853  		allPages, err := tenantnetworks.List(computeClient).AllPages()
   854  		if err != nil {
   855  			errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError)
   856  			if !ok {
   857  				return nil, err
   858  			}
   859  
   860  			if errCode.Actual == 404 {
   861  				tenantNetworkExt = false
   862  			} else {
   863  				return nil, err
   864  			}
   865  		}
   866  
   867  		networkID := ""
   868  		networkName := ""
   869  		if tenantNetworkExt {
   870  			networkList, err := tenantnetworks.ExtractNetworks(allPages)
   871  			if err != nil {
   872  				return nil, err
   873  			}
   874  
   875  			for _, network := range networkList {
   876  				if network.Name == rawMap["name"] {
   877  					tenantnet = network
   878  				}
   879  				if network.ID == rawMap["uuid"] {
   880  					tenantnet = network
   881  				}
   882  			}
   883  
   884  			networkID = tenantnet.ID
   885  			networkName = tenantnet.Name
   886  		} else {
   887  			networkID = rawMap["uuid"].(string)
   888  			networkName = rawMap["name"].(string)
   889  		}
   890  
   891  		newNetworks = append(newNetworks, map[string]interface{}{
   892  			"uuid":        networkID,
   893  			"name":        networkName,
   894  			"port":        rawMap["port"].(string),
   895  			"fixed_ip_v4": rawMap["fixed_ip_v4"].(string),
   896  		})
   897  	}
   898  
   899  	log.Printf("[DEBUG] networks: %+v", newNetworks)
   900  	return newNetworks, nil
   901  }
   902  
   903  func resourceInstanceAddresses(addresses map[string]interface{}) map[string]map[string]interface{} {
   904  
   905  	addrs := make(map[string]map[string]interface{})
   906  	for n, networkAddresses := range addresses {
   907  		addrs[n] = make(map[string]interface{})
   908  		for _, element := range networkAddresses.([]interface{}) {
   909  			address := element.(map[string]interface{})
   910  			if address["OS-EXT-IPS:type"] == "floating" {
   911  				addrs[n]["floating_ip"] = address["addr"]
   912  			} else {
   913  				if address["version"].(float64) == 4 {
   914  					addrs[n]["fixed_ip_v4"] = address["addr"].(string)
   915  				} else {
   916  					addrs[n]["fixed_ip_v6"] = fmt.Sprintf("[%s]", address["addr"].(string))
   917  				}
   918  			}
   919  			if mac, ok := address["OS-EXT-IPS-MAC:mac_addr"]; ok {
   920  				addrs[n]["mac"] = mac.(string)
   921  			}
   922  		}
   923  	}
   924  
   925  	log.Printf("[DEBUG] Addresses: %+v", addresses)
   926  
   927  	return addrs
   928  }
   929  
   930  func resourceInstanceMetadataV2(d *schema.ResourceData) map[string]string {
   931  	m := make(map[string]string)
   932  	for key, val := range d.Get("metadata").(map[string]interface{}) {
   933  		m[key] = val.(string)
   934  	}
   935  	return m
   936  }
   937  
   938  func resourceInstanceBlockDeviceV2(d *schema.ResourceData, bd map[string]interface{}) []bootfromvolume.BlockDevice {
   939  	sourceType := bootfromvolume.SourceType(bd["source_type"].(string))
   940  	bfvOpts := []bootfromvolume.BlockDevice{
   941  		bootfromvolume.BlockDevice{
   942  			UUID:                bd["uuid"].(string),
   943  			SourceType:          sourceType,
   944  			VolumeSize:          bd["volume_size"].(int),
   945  			DestinationType:     bd["destination_type"].(string),
   946  			BootIndex:           bd["boot_index"].(int),
   947  			DeleteOnTermination: bd["delete_on_termination"].(bool),
   948  		},
   949  	}
   950  
   951  	return bfvOpts
   952  }
   953  
   954  func resourceInstanceSchedulerHintsV2(d *schema.ResourceData, schedulerHintsRaw map[string]interface{}) schedulerhints.SchedulerHints {
   955  	differentHost := []string{}
   956  	if len(schedulerHintsRaw["different_host"].([]interface{})) > 0 {
   957  		for _, dh := range schedulerHintsRaw["different_host"].([]interface{}) {
   958  			differentHost = append(differentHost, dh.(string))
   959  		}
   960  	}
   961  
   962  	sameHost := []string{}
   963  	if len(schedulerHintsRaw["same_host"].([]interface{})) > 0 {
   964  		for _, sh := range schedulerHintsRaw["same_host"].([]interface{}) {
   965  			sameHost = append(sameHost, sh.(string))
   966  		}
   967  	}
   968  
   969  	query := make([]interface{}, len(schedulerHintsRaw["query"].([]interface{})))
   970  	if len(schedulerHintsRaw["query"].([]interface{})) > 0 {
   971  		for _, q := range schedulerHintsRaw["query"].([]interface{}) {
   972  			query = append(query, q.(string))
   973  		}
   974  	}
   975  
   976  	schedulerHints := schedulerhints.SchedulerHints{
   977  		Group:           schedulerHintsRaw["group"].(string),
   978  		DifferentHost:   differentHost,
   979  		SameHost:        sameHost,
   980  		Query:           query,
   981  		TargetCell:      schedulerHintsRaw["target_cell"].(string),
   982  		BuildNearHostIP: schedulerHintsRaw["build_near_host_ip"].(string),
   983  	}
   984  
   985  	return schedulerHints
   986  }
   987  
   988  func getImageIDFromConfig(computeClient *gophercloud.ServiceClient, d *schema.ResourceData) (string, error) {
   989  	// If block_device was used, an Image does not need to be specified.
   990  	// If an Image was specified, ignore it
   991  	if _, ok := d.GetOk("block_device"); ok {
   992  		return "", nil
   993  	}
   994  
   995  	if imageId := d.Get("image_id").(string); imageId != "" {
   996  		return imageId, nil
   997  	} else {
   998  		// try the OS_IMAGE_ID environment variable
   999  		if v := os.Getenv("OS_IMAGE_ID"); v != "" {
  1000  			return v, nil
  1001  		}
  1002  	}
  1003  
  1004  	imageName := d.Get("image_name").(string)
  1005  	if imageName == "" {
  1006  		// try the OS_IMAGE_NAME environment variable
  1007  		if v := os.Getenv("OS_IMAGE_NAME"); v != "" {
  1008  			imageName = v
  1009  		}
  1010  	}
  1011  
  1012  	if imageName != "" {
  1013  		imageId, err := images.IDFromName(computeClient, imageName)
  1014  		if err != nil {
  1015  			return "", err
  1016  		}
  1017  		return imageId, nil
  1018  	}
  1019  
  1020  	return "", fmt.Errorf("Neither a boot device, image ID, or image name were able to be determined.")
  1021  }
  1022  
  1023  func setImageInformation(computeClient *gophercloud.ServiceClient, server *servers.Server, d *schema.ResourceData) error {
  1024  	// If block_device was used, an Image does not need to be specified.
  1025  	// If an Image was specified, ignore it
  1026  	if _, ok := d.GetOk("block_device"); ok {
  1027  		d.Set("image_id", "Attempt to boot from volume - no image supplied")
  1028  		return nil
  1029  	}
  1030  
  1031  	imageId := server.Image["id"].(string)
  1032  	if imageId != "" {
  1033  		d.Set("image_id", imageId)
  1034  		if image, err := images.Get(computeClient, imageId).Extract(); err != nil {
  1035  			errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError)
  1036  			if !ok {
  1037  				return err
  1038  			}
  1039  			if errCode.Actual == 404 {
  1040  				// If the image name can't be found, set the value to "Image not found".
  1041  				// The most likely scenario is that the image no longer exists in the Image Service
  1042  				// but the instance still has a record from when it existed.
  1043  				d.Set("image_name", "Image not found")
  1044  				return nil
  1045  			} else {
  1046  				return err
  1047  			}
  1048  		} else {
  1049  			d.Set("image_name", image.Name)
  1050  		}
  1051  	}
  1052  
  1053  	return nil
  1054  }
  1055  
  1056  func getFlavorID(client *gophercloud.ServiceClient, d *schema.ResourceData) (string, error) {
  1057  	flavorId := d.Get("flavor_id").(string)
  1058  
  1059  	if flavorId != "" {
  1060  		return flavorId, nil
  1061  	}
  1062  
  1063  	flavorCount := 0
  1064  	flavorName := d.Get("flavor_name").(string)
  1065  	if flavorName != "" {
  1066  		pager := flavors.ListDetail(client, nil)
  1067  		pager.EachPage(func(page pagination.Page) (bool, error) {
  1068  			flavorList, err := flavors.ExtractFlavors(page)
  1069  			if err != nil {
  1070  				return false, err
  1071  			}
  1072  
  1073  			for _, f := range flavorList {
  1074  				if f.Name == flavorName {
  1075  					flavorCount++
  1076  					flavorId = f.ID
  1077  				}
  1078  			}
  1079  			return true, nil
  1080  		})
  1081  
  1082  		switch flavorCount {
  1083  		case 0:
  1084  			return "", fmt.Errorf("Unable to find flavor: %s", flavorName)
  1085  		case 1:
  1086  			return flavorId, nil
  1087  		default:
  1088  			return "", fmt.Errorf("Found %d flavors matching %s", flavorCount, flavorName)
  1089  		}
  1090  	}
  1091  	return "", fmt.Errorf("Neither a flavor ID nor a flavor name were able to be determined.")
  1092  }
  1093  
  1094  func resourceComputeVolumeAttachmentHash(v interface{}) int {
  1095  	var buf bytes.Buffer
  1096  	m := v.(map[string]interface{})
  1097  	buf.WriteString(fmt.Sprintf("%s-", m["volume_id"].(string)))
  1098  
  1099  	return hashcode.String(buf.String())
  1100  }
  1101  
  1102  func resourceComputeSchedulerHintsHash(v interface{}) int {
  1103  	var buf bytes.Buffer
  1104  	m := v.(map[string]interface{})
  1105  
  1106  	if m["group"] != nil {
  1107  		buf.WriteString(fmt.Sprintf("%s-", m["group"].(string)))
  1108  	}
  1109  
  1110  	if m["target_cell"] != nil {
  1111  		buf.WriteString(fmt.Sprintf("%s-", m["target_cell"].(string)))
  1112  	}
  1113  
  1114  	if m["build_host_near_ip"] != nil {
  1115  		buf.WriteString(fmt.Sprintf("%s-", m["build_host_near_ip"].(string)))
  1116  	}
  1117  
  1118  	buf.WriteString(fmt.Sprintf("%s-", m["different_host"].([]interface{})))
  1119  	buf.WriteString(fmt.Sprintf("%s-", m["same_host"].([]interface{})))
  1120  	buf.WriteString(fmt.Sprintf("%s-", m["query"].([]interface{})))
  1121  
  1122  	return hashcode.String(buf.String())
  1123  }
  1124  
  1125  func attachVolumesToInstance(computeClient *gophercloud.ServiceClient, blockClient *gophercloud.ServiceClient, serverId string, vols []interface{}) error {
  1126  	for _, v := range vols {
  1127  		va := v.(map[string]interface{})
  1128  		volumeId := va["volume_id"].(string)
  1129  		device := va["device"].(string)
  1130  
  1131  		s := ""
  1132  		if serverId != "" {
  1133  			s = serverId
  1134  		} else if va["server_id"] != "" {
  1135  			s = va["server_id"].(string)
  1136  		} else {
  1137  			return fmt.Errorf("Unable to determine server ID to attach volume.")
  1138  		}
  1139  
  1140  		vaOpts := &volumeattach.CreateOpts{
  1141  			Device:   device,
  1142  			VolumeID: volumeId,
  1143  		}
  1144  
  1145  		if _, err := volumeattach.Create(computeClient, s, vaOpts).Extract(); err != nil {
  1146  			return err
  1147  		}
  1148  
  1149  		stateConf := &resource.StateChangeConf{
  1150  			Pending:    []string{"attaching", "available"},
  1151  			Target:     "in-use",
  1152  			Refresh:    VolumeV1StateRefreshFunc(blockClient, va["volume_id"].(string)),
  1153  			Timeout:    30 * time.Minute,
  1154  			Delay:      5 * time.Second,
  1155  			MinTimeout: 2 * time.Second,
  1156  		}
  1157  
  1158  		if _, err := stateConf.WaitForState(); err != nil {
  1159  			return err
  1160  		}
  1161  
  1162  		log.Printf("[INFO] Attached volume %s to instance %s", volumeId, serverId)
  1163  	}
  1164  	return nil
  1165  }
  1166  
  1167  func detachVolumesFromInstance(computeClient *gophercloud.ServiceClient, blockClient *gophercloud.ServiceClient, serverId string, vols []interface{}) error {
  1168  	for _, v := range vols {
  1169  		va := v.(map[string]interface{})
  1170  		aId := va["id"].(string)
  1171  
  1172  		if err := volumeattach.Delete(computeClient, serverId, aId).ExtractErr(); err != nil {
  1173  			return err
  1174  		}
  1175  
  1176  		stateConf := &resource.StateChangeConf{
  1177  			Pending:    []string{"detaching", "in-use"},
  1178  			Target:     "available",
  1179  			Refresh:    VolumeV1StateRefreshFunc(blockClient, va["volume_id"].(string)),
  1180  			Timeout:    30 * time.Minute,
  1181  			Delay:      5 * time.Second,
  1182  			MinTimeout: 2 * time.Second,
  1183  		}
  1184  
  1185  		if _, err := stateConf.WaitForState(); err != nil {
  1186  			return err
  1187  		}
  1188  		log.Printf("[INFO] Detached volume %s from instance %s", va["volume_id"], serverId)
  1189  	}
  1190  
  1191  	return nil
  1192  }
  1193  
  1194  func getVolumeAttachments(computeClient *gophercloud.ServiceClient, d *schema.ResourceData) error {
  1195  	var attachments []volumeattach.VolumeAttachment
  1196  
  1197  	err := volumeattach.List(computeClient, d.Id()).EachPage(func(page pagination.Page) (bool, error) {
  1198  		actual, err := volumeattach.ExtractVolumeAttachments(page)
  1199  		if err != nil {
  1200  			return false, err
  1201  		}
  1202  
  1203  		attachments = actual
  1204  		return true, nil
  1205  	})
  1206  
  1207  	if err != nil {
  1208  		return err
  1209  	}
  1210  
  1211  	vols := make([]map[string]interface{}, len(attachments))
  1212  	for i, attachment := range attachments {
  1213  		vols[i] = make(map[string]interface{})
  1214  		vols[i]["id"] = attachment.ID
  1215  		vols[i]["volume_id"] = attachment.VolumeID
  1216  		vols[i]["device"] = attachment.Device
  1217  	}
  1218  	log.Printf("[INFO] Volume attachments: %v", vols)
  1219  	d.Set("volume", vols)
  1220  
  1221  	return nil
  1222  }
  1223  
  1224  func checkVolumeConfig(d *schema.ResourceData) error {
  1225  	// Although a volume_id is required to attach a volume, in order to be able to report
  1226  	// the attached volumes of an instance, it must be "computed" and thus "optional".
  1227  	// This accounts for situations such as "boot from volume" as well as volumes being
  1228  	// attached to the instance outside of Terraform.
  1229  	if v := d.Get("volume"); v != nil {
  1230  		vols := v.(*schema.Set).List()
  1231  		if len(vols) > 0 {
  1232  			for _, v := range vols {
  1233  				va := v.(map[string]interface{})
  1234  				if va["volume_id"].(string) == "" {
  1235  					return fmt.Errorf("A volume_id must be specified when attaching volumes.")
  1236  				}
  1237  			}
  1238  		}
  1239  	}
  1240  
  1241  	if v, ok := d.GetOk("block_device"); ok {
  1242  		vL := v.(*schema.Set).List()
  1243  		if len(vL) > 1 {
  1244  			return fmt.Errorf("Can only specify one block device to boot from.")
  1245  		}
  1246  	}
  1247  
  1248  	return nil
  1249  }