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