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