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