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