github.com/jsoriano/terraform@v0.6.7-0.20151026070445-8b70867fdd95/builtin/providers/azure/resource_azure_instance.go (about) 1 package azure 2 3 import ( 4 "bytes" 5 "encoding/base64" 6 "fmt" 7 "log" 8 "strings" 9 10 "github.com/Azure/azure-sdk-for-go/management" 11 "github.com/Azure/azure-sdk-for-go/management/hostedservice" 12 "github.com/Azure/azure-sdk-for-go/management/osimage" 13 "github.com/Azure/azure-sdk-for-go/management/virtualmachine" 14 "github.com/Azure/azure-sdk-for-go/management/virtualmachineimage" 15 "github.com/Azure/azure-sdk-for-go/management/vmutils" 16 "github.com/hashicorp/terraform/helper/hashcode" 17 "github.com/hashicorp/terraform/helper/schema" 18 ) 19 20 const ( 21 linux = "Linux" 22 windows = "Windows" 23 osDiskBlobStorageURL = "http://%s.blob.core.windows.net/vhds/%s.vhd" 24 ) 25 26 func resourceAzureInstance() *schema.Resource { 27 return &schema.Resource{ 28 Create: resourceAzureInstanceCreate, 29 Read: resourceAzureInstanceRead, 30 Update: resourceAzureInstanceUpdate, 31 Delete: resourceAzureInstanceDelete, 32 33 Schema: map[string]*schema.Schema{ 34 "name": &schema.Schema{ 35 Type: schema.TypeString, 36 Required: true, 37 ForceNew: true, 38 }, 39 40 "hosted_service_name": &schema.Schema{ 41 Type: schema.TypeString, 42 Optional: true, 43 Computed: true, 44 ForceNew: true, 45 }, 46 47 "description": &schema.Schema{ 48 Type: schema.TypeString, 49 Optional: true, 50 Computed: true, 51 ForceNew: true, 52 }, 53 54 "image": &schema.Schema{ 55 Type: schema.TypeString, 56 Required: true, 57 ForceNew: true, 58 }, 59 60 "size": &schema.Schema{ 61 Type: schema.TypeString, 62 Required: true, 63 }, 64 65 "subnet": &schema.Schema{ 66 Type: schema.TypeString, 67 Optional: true, 68 Computed: true, 69 ForceNew: true, 70 }, 71 72 "virtual_network": &schema.Schema{ 73 Type: schema.TypeString, 74 Optional: true, 75 ForceNew: true, 76 }, 77 78 "storage_service_name": &schema.Schema{ 79 Type: schema.TypeString, 80 Optional: true, 81 ForceNew: true, 82 }, 83 84 "reverse_dns": &schema.Schema{ 85 Type: schema.TypeString, 86 Optional: true, 87 ForceNew: true, 88 }, 89 90 "location": &schema.Schema{ 91 Type: schema.TypeString, 92 Required: true, 93 ForceNew: true, 94 }, 95 96 "automatic_updates": &schema.Schema{ 97 Type: schema.TypeBool, 98 Optional: true, 99 Default: false, 100 ForceNew: true, 101 }, 102 103 "time_zone": &schema.Schema{ 104 Type: schema.TypeString, 105 Optional: true, 106 ForceNew: true, 107 }, 108 109 "username": &schema.Schema{ 110 Type: schema.TypeString, 111 Required: true, 112 ForceNew: true, 113 }, 114 115 "password": &schema.Schema{ 116 Type: schema.TypeString, 117 Optional: true, 118 ForceNew: true, 119 }, 120 121 "ssh_key_thumbprint": &schema.Schema{ 122 Type: schema.TypeString, 123 Optional: true, 124 ForceNew: true, 125 }, 126 127 "endpoint": &schema.Schema{ 128 Type: schema.TypeSet, 129 Optional: true, 130 Computed: true, 131 Elem: &schema.Resource{ 132 Schema: map[string]*schema.Schema{ 133 "name": &schema.Schema{ 134 Type: schema.TypeString, 135 Required: true, 136 }, 137 138 "protocol": &schema.Schema{ 139 Type: schema.TypeString, 140 Optional: true, 141 Default: "tcp", 142 }, 143 144 "public_port": &schema.Schema{ 145 Type: schema.TypeInt, 146 Required: true, 147 }, 148 149 "private_port": &schema.Schema{ 150 Type: schema.TypeInt, 151 Required: true, 152 }, 153 }, 154 }, 155 Set: resourceAzureEndpointHash, 156 }, 157 158 "security_group": &schema.Schema{ 159 Type: schema.TypeString, 160 Optional: true, 161 Computed: true, 162 }, 163 164 "ip_address": &schema.Schema{ 165 Type: schema.TypeString, 166 Computed: true, 167 }, 168 169 "vip_address": &schema.Schema{ 170 Type: schema.TypeString, 171 Computed: true, 172 }, 173 174 "domain_name": &schema.Schema{ 175 Type: schema.TypeString, 176 Optional: true, 177 ForceNew: true, 178 }, 179 180 "domain_username": &schema.Schema{ 181 Type: schema.TypeString, 182 Optional: true, 183 ForceNew: true, 184 }, 185 186 "domain_password": &schema.Schema{ 187 Type: schema.TypeString, 188 Optional: true, 189 ForceNew: true, 190 }, 191 192 "domain_ou": &schema.Schema{ 193 Type: schema.TypeString, 194 Optional: true, 195 ForceNew: true, 196 }, 197 }, 198 } 199 } 200 201 func resourceAzureInstanceCreate(d *schema.ResourceData, meta interface{}) (err error) { 202 azureClient := meta.(*Client) 203 mc := azureClient.mgmtClient 204 hostedServiceClient := azureClient.hostedServiceClient 205 vmClient := azureClient.vmClient 206 207 name := d.Get("name").(string) 208 209 // Compute/set the description 210 description := d.Get("description").(string) 211 if description == "" { 212 description = name 213 } 214 215 // Retrieve the needed details of the image 216 configureForImage, osType, err := retrieveImageDetails( 217 meta, 218 d.Get("image").(string), 219 name, 220 d.Get("storage_service_name").(string), 221 ) 222 if err != nil { 223 return err 224 } 225 226 // Verify if we have all required parameters 227 if err := verifyInstanceParameters(d, osType); err != nil { 228 return err 229 } 230 231 var hostedServiceName string 232 // check if hosted service name parameter was given: 233 if serviceName, ok := d.GetOk("hosted_service_name"); !ok { 234 // if not provided; just use the name of the instance to create a new one: 235 hostedServiceName = name 236 d.Set("hosted_service_name", hostedServiceName) 237 238 p := hostedservice.CreateHostedServiceParameters{ 239 ServiceName: hostedServiceName, 240 Label: base64.StdEncoding.EncodeToString([]byte(name)), 241 Description: fmt.Sprintf("Cloud Service created automatically for instance %s", name), 242 Location: d.Get("location").(string), 243 ReverseDNSFqdn: d.Get("reverse_dns").(string), 244 } 245 246 log.Printf("[DEBUG] Creating Cloud Service for instance: %s", name) 247 err = hostedServiceClient.CreateHostedService(p) 248 if err != nil { 249 return fmt.Errorf("Error creating Cloud Service for instance %s: %s", name, err) 250 } 251 } else { 252 // else; use the provided hosted service name: 253 hostedServiceName = serviceName.(string) 254 } 255 256 // Create a new role for the instance 257 role := vmutils.NewVMConfiguration(name, d.Get("size").(string)) 258 259 log.Printf("[DEBUG] Configuring deployment from image...") 260 err = configureForImage(&role) 261 if err != nil { 262 return fmt.Errorf("Error configuring the deployment for %s: %s", name, err) 263 } 264 265 if osType == linux { 266 // This is pretty ugly, but the Azure SDK leaves me no other choice... 267 if tp, ok := d.GetOk("ssh_key_thumbprint"); ok { 268 err = vmutils.ConfigureForLinux( 269 &role, 270 name, 271 d.Get("username").(string), 272 d.Get("password").(string), 273 tp.(string), 274 ) 275 } else { 276 err = vmutils.ConfigureForLinux( 277 &role, 278 name, 279 d.Get("username").(string), 280 d.Get("password").(string), 281 ) 282 } 283 if err != nil { 284 return fmt.Errorf("Error configuring %s for Linux: %s", name, err) 285 } 286 } 287 288 if osType == windows { 289 err = vmutils.ConfigureForWindows( 290 &role, 291 name, 292 d.Get("username").(string), 293 d.Get("password").(string), 294 d.Get("automatic_updates").(bool), 295 d.Get("time_zone").(string), 296 ) 297 if err != nil { 298 return fmt.Errorf("Error configuring %s for Windows: %s", name, err) 299 } 300 301 if domain_name, ok := d.GetOk("domain_name"); ok { 302 err = vmutils.ConfigureWindowsToJoinDomain( 303 &role, 304 d.Get("domain_username").(string), 305 d.Get("domain_password").(string), 306 domain_name.(string), 307 d.Get("domain_ou").(string), 308 ) 309 if err != nil { 310 return fmt.Errorf("Error configuring %s for WindowsToJoinDomain: %s", name, err) 311 } 312 } 313 } 314 315 if s := d.Get("endpoint").(*schema.Set); s.Len() > 0 { 316 for _, v := range s.List() { 317 m := v.(map[string]interface{}) 318 err := vmutils.ConfigureWithExternalPort( 319 &role, 320 m["name"].(string), 321 m["private_port"].(int), 322 m["public_port"].(int), 323 endpointProtocol(m["protocol"].(string)), 324 ) 325 if err != nil { 326 return fmt.Errorf( 327 "Error adding endpoint %s for instance %s: %s", m["name"].(string), name, err) 328 } 329 } 330 } 331 332 if subnet, ok := d.GetOk("subnet"); ok { 333 err = vmutils.ConfigureWithSubnet(&role, subnet.(string)) 334 if err != nil { 335 return fmt.Errorf( 336 "Error associating subnet %s with instance %s: %s", d.Get("subnet").(string), name, err) 337 } 338 } 339 340 if sg, ok := d.GetOk("security_group"); ok { 341 err = vmutils.ConfigureWithSecurityGroup(&role, sg.(string)) 342 if err != nil { 343 return fmt.Errorf( 344 "Error associating security group %s with instance %s: %s", sg.(string), name, err) 345 } 346 } 347 348 options := virtualmachine.CreateDeploymentOptions{ 349 VirtualNetworkName: d.Get("virtual_network").(string), 350 } 351 352 log.Printf("[DEBUG] Creating the new instance...") 353 req, err := vmClient.CreateDeployment(role, hostedServiceName, options) 354 if err != nil { 355 return fmt.Errorf("Error creating instance %s: %s", name, err) 356 } 357 358 log.Printf("[DEBUG] Waiting for the new instance to be created...") 359 if err := mc.WaitForOperation(req, nil); err != nil { 360 return fmt.Errorf( 361 "Error waiting for instance %s to be created: %s", name, err) 362 } 363 364 d.SetId(name) 365 366 return resourceAzureInstanceRead(d, meta) 367 } 368 369 func resourceAzureInstanceRead(d *schema.ResourceData, meta interface{}) error { 370 azureClient := meta.(*Client) 371 hostedServiceClient := azureClient.hostedServiceClient 372 vmClient := azureClient.vmClient 373 374 name := d.Get("name").(string) 375 376 // check if the instance belongs to an independent hosted service 377 // or it had one created for it. 378 var hostedServiceName string 379 if serviceName, ok := d.GetOk("hosted_service_name"); ok { 380 // if independent; use that hosted service name: 381 hostedServiceName = serviceName.(string) 382 } else { 383 // else; suppose it's the instance's name: 384 hostedServiceName = name 385 } 386 387 log.Printf("[DEBUG] Retrieving Cloud Service for instance: %s", name) 388 cs, err := hostedServiceClient.GetHostedService(hostedServiceName) 389 if err != nil { 390 return fmt.Errorf("Error retrieving Cloud Service of instance %s (%q): %s", name, hostedServiceName, err) 391 } 392 393 d.Set("reverse_dns", cs.ReverseDNSFqdn) 394 d.Set("location", cs.Location) 395 396 log.Printf("[DEBUG] Retrieving instance: %s", name) 397 dpmt, err := vmClient.GetDeployment(hostedServiceName, name) 398 if err != nil { 399 if management.IsResourceNotFoundError(err) { 400 d.SetId("") 401 return nil 402 } 403 return fmt.Errorf("Error retrieving instance %s: %s", name, err) 404 } 405 406 if len(dpmt.RoleList) != 1 { 407 return fmt.Errorf( 408 "Instance %s has an unexpected number of roles: %d", name, len(dpmt.RoleList)) 409 } 410 411 d.Set("size", dpmt.RoleList[0].RoleSize) 412 413 if len(dpmt.RoleInstanceList) != 1 { 414 return fmt.Errorf( 415 "Instance %s has an unexpected number of role instances: %d", 416 name, len(dpmt.RoleInstanceList)) 417 } 418 d.Set("ip_address", dpmt.RoleInstanceList[0].IPAddress) 419 420 if len(dpmt.RoleInstanceList[0].InstanceEndpoints) > 0 { 421 d.Set("vip_address", dpmt.RoleInstanceList[0].InstanceEndpoints[0].Vip) 422 } 423 424 // Find the network configuration set 425 for _, c := range dpmt.RoleList[0].ConfigurationSets { 426 if c.ConfigurationSetType == virtualmachine.ConfigurationSetTypeNetwork { 427 // Create a new set to hold all configured endpoints 428 endpoints := &schema.Set{ 429 F: resourceAzureEndpointHash, 430 } 431 432 // Loop through all endpoints 433 for _, ep := range c.InputEndpoints { 434 endpoint := map[string]interface{}{} 435 436 // Update the values 437 endpoint["name"] = ep.Name 438 endpoint["protocol"] = string(ep.Protocol) 439 endpoint["public_port"] = ep.Port 440 endpoint["private_port"] = ep.LocalPort 441 endpoints.Add(endpoint) 442 } 443 d.Set("endpoint", endpoints) 444 445 // Update the subnet 446 switch len(c.SubnetNames) { 447 case 1: 448 d.Set("subnet", c.SubnetNames[0]) 449 case 0: 450 d.Set("subnet", "") 451 default: 452 return fmt.Errorf( 453 "Instance %s has an unexpected number of associated subnets %d", 454 name, len(dpmt.RoleInstanceList)) 455 } 456 457 // Update the security group 458 d.Set("security_group", c.NetworkSecurityGroup) 459 } 460 } 461 462 connType := "ssh" 463 if dpmt.RoleList[0].OSVirtualHardDisk.OS == windows { 464 connType = "winrm" 465 } 466 467 // Set the connection info for any configured provisioners 468 d.SetConnInfo(map[string]string{ 469 "type": connType, 470 "host": dpmt.VirtualIPs[0].Address, 471 "user": d.Get("username").(string), 472 "password": d.Get("password").(string), 473 }) 474 475 return nil 476 } 477 478 func resourceAzureInstanceUpdate(d *schema.ResourceData, meta interface{}) error { 479 azureClient := meta.(*Client) 480 mc := azureClient.mgmtClient 481 vmClient := azureClient.vmClient 482 483 // First check if anything we can update changed, and if not just return 484 if !d.HasChange("size") && !d.HasChange("endpoint") && !d.HasChange("security_group") { 485 return nil 486 } 487 488 name := d.Get("name").(string) 489 hostedServiceName := d.Get("hosted_service_name").(string) 490 491 // Get the current role 492 role, err := vmClient.GetRole(hostedServiceName, name, name) 493 if err != nil { 494 return fmt.Errorf("Error retrieving role of instance %s: %s", name, err) 495 } 496 497 // Verify if we have all required parameters 498 if err := verifyInstanceParameters(d, role.OSVirtualHardDisk.OS); err != nil { 499 return err 500 } 501 502 if d.HasChange("size") { 503 role.RoleSize = d.Get("size").(string) 504 } 505 506 if d.HasChange("endpoint") { 507 _, n := d.GetChange("endpoint") 508 509 // Delete the existing endpoints 510 for i, c := range role.ConfigurationSets { 511 if c.ConfigurationSetType == virtualmachine.ConfigurationSetTypeNetwork { 512 c.InputEndpoints = nil 513 role.ConfigurationSets[i] = c 514 } 515 } 516 517 // And add the ones we still want 518 if s := n.(*schema.Set); s.Len() > 0 { 519 for _, v := range s.List() { 520 m := v.(map[string]interface{}) 521 err := vmutils.ConfigureWithExternalPort( 522 role, 523 m["name"].(string), 524 m["private_port"].(int), 525 m["public_port"].(int), 526 endpointProtocol(m["protocol"].(string)), 527 ) 528 if err != nil { 529 return fmt.Errorf( 530 "Error adding endpoint %s for instance %s: %s", m["name"].(string), name, err) 531 } 532 } 533 } 534 } 535 536 if d.HasChange("security_group") { 537 sg := d.Get("security_group").(string) 538 err := vmutils.ConfigureWithSecurityGroup(role, sg) 539 if err != nil { 540 return fmt.Errorf( 541 "Error associating security group %s with instance %s: %s", sg, name, err) 542 } 543 } 544 545 // Update the adjusted role 546 req, err := vmClient.UpdateRole(hostedServiceName, name, name, *role) 547 if err != nil { 548 return fmt.Errorf("Error updating role of instance %s: %s", name, err) 549 } 550 551 if err := mc.WaitForOperation(req, nil); err != nil { 552 return fmt.Errorf( 553 "Error waiting for role of instance %s to be updated: %s", name, err) 554 } 555 556 return resourceAzureInstanceRead(d, meta) 557 } 558 559 func resourceAzureInstanceDelete(d *schema.ResourceData, meta interface{}) error { 560 azureClient := meta.(*Client) 561 mc := azureClient.mgmtClient 562 vmClient := azureClient.vmClient 563 hostedServiceClient := azureClient.hostedServiceClient 564 565 name := d.Get("name").(string) 566 hostedServiceName := d.Get("hosted_service_name").(string) 567 568 log.Printf("[DEBUG] Deleting instance: %s", name) 569 570 // check if the instance had a hosted service created especially for it: 571 if name == hostedServiceName { 572 // if so; we must delete the associated hosted service as well: 573 req, err := hostedServiceClient.DeleteHostedService(name, true) 574 if err != nil { 575 return fmt.Errorf("Error deleting instance and hosted service %s: %s", name, err) 576 } 577 578 // Wait until the hosted service and the instance it contains is deleted: 579 if err := mc.WaitForOperation(req, nil); err != nil { 580 return fmt.Errorf( 581 "Error waiting for instance %s to be deleted: %s", name, err) 582 } 583 } else { 584 // else; just delete the instance: 585 reqID, err := vmClient.DeleteDeployment(hostedServiceName, name) 586 if err != nil { 587 return fmt.Errorf("Error deleting instance %s off hosted service %s: %s", name, hostedServiceName, err) 588 } 589 590 // and wait for the deletion: 591 if err := mc.WaitForOperation(reqID, nil); err != nil { 592 return fmt.Errorf("Error waiting for intance %s to be deleted off the hosted service %s: %s", 593 name, hostedServiceName, err) 594 } 595 } 596 597 return nil 598 } 599 600 func resourceAzureEndpointHash(v interface{}) int { 601 var buf bytes.Buffer 602 m := v.(map[string]interface{}) 603 buf.WriteString(fmt.Sprintf("%s-", m["name"].(string))) 604 buf.WriteString(fmt.Sprintf("%s-", m["protocol"].(string))) 605 buf.WriteString(fmt.Sprintf("%d-", m["public_port"].(int))) 606 buf.WriteString(fmt.Sprintf("%d-", m["private_port"].(int))) 607 608 return hashcode.String(buf.String()) 609 } 610 611 func retrieveImageDetails( 612 meta interface{}, 613 label string, 614 name string, 615 storage string) (func(*virtualmachine.Role) error, string, error) { 616 617 azureClient := meta.(*Client) 618 vmImageClient := azureClient.vmImageClient 619 osImageClient := azureClient.osImageClient 620 621 configureForImage, osType, VMLabels, err := retrieveVMImageDetails(vmImageClient, label) 622 if err == nil { 623 return configureForImage, osType, nil 624 } 625 626 configureForImage, osType, OSLabels, err := retrieveOSImageDetails(osImageClient, label, name, storage) 627 if err == nil { 628 return configureForImage, osType, nil 629 } 630 631 if err == PlatformStorageError { 632 return nil, "", err 633 } 634 635 return nil, "", fmt.Errorf("Could not find image with label '%s'. Available images are: %s", 636 label, strings.Join(append(VMLabels, OSLabels...), ", ")) 637 } 638 639 func retrieveVMImageDetails( 640 vmImageClient virtualmachineimage.Client, 641 label string) (func(*virtualmachine.Role) error, string, []string, error) { 642 imgs, err := vmImageClient.ListVirtualMachineImages() 643 if err != nil { 644 return nil, "", nil, fmt.Errorf("Error retrieving image details: %s", err) 645 } 646 647 var labels []string 648 for _, img := range imgs.VMImages { 649 if img.Label == label { 650 if img.OSDiskConfiguration.OS != linux && img.OSDiskConfiguration.OS != windows { 651 return nil, "", nil, fmt.Errorf("Unsupported image OS: %s", img.OSDiskConfiguration.OS) 652 } 653 654 configureForImage := func(role *virtualmachine.Role) error { 655 return vmutils.ConfigureDeploymentFromVMImage( 656 role, 657 img.Name, 658 "", 659 true, 660 ) 661 } 662 663 return configureForImage, img.OSDiskConfiguration.OS, nil, nil 664 } 665 666 labels = append(labels, img.Label) 667 } 668 669 return nil, "", labels, fmt.Errorf("Could not find image with label '%s'", label) 670 } 671 672 func retrieveOSImageDetails( 673 osImageClient osimage.OSImageClient, 674 label string, 675 name string, 676 storage string) (func(*virtualmachine.Role) error, string, []string, error) { 677 imgs, err := osImageClient.ListOSImages() 678 if err != nil { 679 return nil, "", nil, fmt.Errorf("Error retrieving image details: %s", err) 680 } 681 682 var labels []string 683 for _, img := range imgs.OSImages { 684 if img.Label == label { 685 if img.OS != linux && img.OS != windows { 686 return nil, "", nil, fmt.Errorf("Unsupported image OS: %s", img.OS) 687 } 688 if img.MediaLink == "" { 689 if storage == "" { 690 return nil, "", nil, PlatformStorageError 691 } 692 img.MediaLink = fmt.Sprintf(osDiskBlobStorageURL, storage, name) 693 } 694 695 configureForImage := func(role *virtualmachine.Role) error { 696 return vmutils.ConfigureDeploymentFromPlatformImage( 697 role, 698 img.Name, 699 img.MediaLink, 700 label, 701 ) 702 } 703 704 return configureForImage, img.OS, nil, nil 705 } 706 707 labels = append(labels, img.Label) 708 } 709 710 return nil, "", labels, fmt.Errorf("Could not find image with label '%s'", label) 711 } 712 713 func endpointProtocol(p string) virtualmachine.InputEndpointProtocol { 714 if p == "tcp" { 715 return virtualmachine.InputEndpointProtocolTCP 716 } 717 718 return virtualmachine.InputEndpointProtocolUDP 719 } 720 721 func verifyInstanceParameters(d *schema.ResourceData, osType string) error { 722 if osType == linux { 723 _, pass := d.GetOk("password") 724 _, key := d.GetOk("ssh_key_thumbprint") 725 726 if !pass && !key { 727 return fmt.Errorf( 728 "You must supply a 'password' and/or a 'ssh_key_thumbprint' when using a Linux image") 729 } 730 } 731 732 if osType == windows { 733 if _, ok := d.GetOk("password"); !ok { 734 return fmt.Errorf("You must supply a 'password' when using a Windows image") 735 } 736 737 if _, ok := d.GetOk("time_zone"); !ok { 738 return fmt.Errorf("You must supply a 'time_zone' when using a Windows image") 739 } 740 } 741 742 if _, ok := d.GetOk("subnet"); ok { 743 if _, ok := d.GetOk("virtual_network"); !ok { 744 return fmt.Errorf("You must also supply a 'virtual_network' when supplying a 'subnet'") 745 } 746 } 747 748 if s := d.Get("endpoint").(*schema.Set); s.Len() > 0 { 749 for _, v := range s.List() { 750 protocol := v.(map[string]interface{})["protocol"].(string) 751 752 if protocol != "tcp" && protocol != "udp" { 753 return fmt.Errorf( 754 "Invalid endpoint protocol %s! Valid options are 'tcp' and 'udp'.", protocol) 755 } 756 } 757 } 758 759 return nil 760 }