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