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