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