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