github.com/anuaimi/terraform@v0.6.4-0.20150904235404-2bf9aec61da8/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 } 175 } 176 177 func resourceAzureInstanceCreate(d *schema.ResourceData, meta interface{}) (err error) { 178 azureClient := meta.(*Client) 179 mc := azureClient.mgmtClient 180 hostedServiceClient := azureClient.hostedServiceClient 181 vmClient := azureClient.vmClient 182 183 name := d.Get("name").(string) 184 185 // Compute/set the description 186 description := d.Get("description").(string) 187 if description == "" { 188 description = name 189 } 190 191 // Retrieve the needed details of the image 192 configureForImage, osType, err := retrieveImageDetails( 193 meta, 194 d.Get("image").(string), 195 name, 196 d.Get("storage_service_name").(string), 197 ) 198 if err != nil { 199 return err 200 } 201 202 // Verify if we have all required parameters 203 if err := verifyInstanceParameters(d, osType); err != nil { 204 return err 205 } 206 207 var hostedServiceName string 208 // check if hosted service name parameter was given: 209 if serviceName, ok := d.GetOk("hosted_service_name"); !ok { 210 // if not provided; just use the name of the instance to create a new one: 211 hostedServiceName = name 212 d.Set("hosted_service_name", hostedServiceName) 213 214 p := hostedservice.CreateHostedServiceParameters{ 215 ServiceName: hostedServiceName, 216 Label: base64.StdEncoding.EncodeToString([]byte(name)), 217 Description: fmt.Sprintf("Cloud Service created automatically for instance %s", name), 218 Location: d.Get("location").(string), 219 ReverseDNSFqdn: d.Get("reverse_dns").(string), 220 } 221 222 log.Printf("[DEBUG] Creating Cloud Service for instance: %s", name) 223 err = hostedServiceClient.CreateHostedService(p) 224 if err != nil { 225 return fmt.Errorf("Error creating Cloud Service for instance %s: %s", name, err) 226 } 227 } else { 228 // else; use the provided hosted service name: 229 hostedServiceName = serviceName.(string) 230 } 231 232 // Create a new role for the instance 233 role := vmutils.NewVMConfiguration(name, d.Get("size").(string)) 234 235 log.Printf("[DEBUG] Configuring deployment from image...") 236 err = configureForImage(&role) 237 if err != nil { 238 return fmt.Errorf("Error configuring the deployment for %s: %s", name, err) 239 } 240 241 if osType == linux { 242 // This is pretty ugly, but the Azure SDK leaves me no other choice... 243 if tp, ok := d.GetOk("ssh_key_thumbprint"); ok { 244 err = vmutils.ConfigureForLinux( 245 &role, 246 name, 247 d.Get("username").(string), 248 d.Get("password").(string), 249 tp.(string), 250 ) 251 } else { 252 err = vmutils.ConfigureForLinux( 253 &role, 254 name, 255 d.Get("username").(string), 256 d.Get("password").(string), 257 ) 258 } 259 if err != nil { 260 return fmt.Errorf("Error configuring %s for Linux: %s", name, err) 261 } 262 } 263 264 if osType == windows { 265 err = vmutils.ConfigureForWindows( 266 &role, 267 name, 268 d.Get("username").(string), 269 d.Get("password").(string), 270 d.Get("automatic_updates").(bool), 271 d.Get("time_zone").(string), 272 ) 273 if err != nil { 274 return fmt.Errorf("Error configuring %s for Windows: %s", name, err) 275 } 276 } 277 278 if s := d.Get("endpoint").(*schema.Set); s.Len() > 0 { 279 for _, v := range s.List() { 280 m := v.(map[string]interface{}) 281 err := vmutils.ConfigureWithExternalPort( 282 &role, 283 m["name"].(string), 284 m["private_port"].(int), 285 m["public_port"].(int), 286 endpointProtocol(m["protocol"].(string)), 287 ) 288 if err != nil { 289 return fmt.Errorf( 290 "Error adding endpoint %s for instance %s: %s", m["name"].(string), name, err) 291 } 292 } 293 } 294 295 if subnet, ok := d.GetOk("subnet"); ok { 296 err = vmutils.ConfigureWithSubnet(&role, subnet.(string)) 297 if err != nil { 298 return fmt.Errorf( 299 "Error associating subnet %s with instance %s: %s", d.Get("subnet").(string), name, err) 300 } 301 } 302 303 if sg, ok := d.GetOk("security_group"); ok { 304 err = vmutils.ConfigureWithSecurityGroup(&role, sg.(string)) 305 if err != nil { 306 return fmt.Errorf( 307 "Error associating security group %s with instance %s: %s", sg.(string), name, err) 308 } 309 } 310 311 options := virtualmachine.CreateDeploymentOptions{ 312 VirtualNetworkName: d.Get("virtual_network").(string), 313 } 314 315 log.Printf("[DEBUG] Creating the new instance...") 316 req, err := vmClient.CreateDeployment(role, hostedServiceName, options) 317 if err != nil { 318 return fmt.Errorf("Error creating instance %s: %s", name, err) 319 } 320 321 log.Printf("[DEBUG] Waiting for the new instance to be created...") 322 if err := mc.WaitForOperation(req, nil); err != nil { 323 return fmt.Errorf( 324 "Error waiting for instance %s to be created: %s", name, err) 325 } 326 327 d.SetId(name) 328 329 return resourceAzureInstanceRead(d, meta) 330 } 331 332 func resourceAzureInstanceRead(d *schema.ResourceData, meta interface{}) error { 333 azureClient := meta.(*Client) 334 hostedServiceClient := azureClient.hostedServiceClient 335 vmClient := azureClient.vmClient 336 337 name := d.Get("name").(string) 338 339 // check if the instance belongs to an independent hosted service 340 // or it had one created for it. 341 var hostedServiceName string 342 if serviceName, ok := d.GetOk("hosted_service_name"); ok { 343 // if independent; use that hosted service name: 344 hostedServiceName = serviceName.(string) 345 } else { 346 // else; suppose it's the instance's name: 347 hostedServiceName = name 348 } 349 350 log.Printf("[DEBUG] Retrieving Cloud Service for instance: %s", name) 351 cs, err := hostedServiceClient.GetHostedService(hostedServiceName) 352 if err != nil { 353 return fmt.Errorf("Error retrieving Cloud Service of instance %s (%q): %s", name, hostedServiceName, err) 354 } 355 356 d.Set("reverse_dns", cs.ReverseDNSFqdn) 357 d.Set("location", cs.Location) 358 359 log.Printf("[DEBUG] Retrieving instance: %s", name) 360 dpmt, err := vmClient.GetDeployment(hostedServiceName, name) 361 if err != nil { 362 if management.IsResourceNotFoundError(err) { 363 d.SetId("") 364 return nil 365 } 366 return fmt.Errorf("Error retrieving instance %s: %s", name, err) 367 } 368 369 if len(dpmt.RoleList) != 1 { 370 return fmt.Errorf( 371 "Instance %s has an unexpected number of roles: %d", name, len(dpmt.RoleList)) 372 } 373 374 d.Set("size", dpmt.RoleList[0].RoleSize) 375 376 if len(dpmt.RoleInstanceList) != 1 { 377 return fmt.Errorf( 378 "Instance %s has an unexpected number of role instances: %d", 379 name, len(dpmt.RoleInstanceList)) 380 } 381 d.Set("ip_address", dpmt.RoleInstanceList[0].IPAddress) 382 383 if len(dpmt.RoleInstanceList[0].InstanceEndpoints) > 0 { 384 d.Set("vip_address", dpmt.RoleInstanceList[0].InstanceEndpoints[0].Vip) 385 } 386 387 // Find the network configuration set 388 for _, c := range dpmt.RoleList[0].ConfigurationSets { 389 if c.ConfigurationSetType == virtualmachine.ConfigurationSetTypeNetwork { 390 // Create a new set to hold all configured endpoints 391 endpoints := &schema.Set{ 392 F: resourceAzureEndpointHash, 393 } 394 395 // Loop through all endpoints 396 for _, ep := range c.InputEndpoints { 397 endpoint := map[string]interface{}{} 398 399 // Update the values 400 endpoint["name"] = ep.Name 401 endpoint["protocol"] = string(ep.Protocol) 402 endpoint["public_port"] = ep.Port 403 endpoint["private_port"] = ep.LocalPort 404 endpoints.Add(endpoint) 405 } 406 d.Set("endpoint", endpoints) 407 408 // Update the subnet 409 switch len(c.SubnetNames) { 410 case 1: 411 d.Set("subnet", c.SubnetNames[0]) 412 case 0: 413 d.Set("subnet", "") 414 default: 415 return fmt.Errorf( 416 "Instance %s has an unexpected number of associated subnets %d", 417 name, len(dpmt.RoleInstanceList)) 418 } 419 420 // Update the security group 421 d.Set("security_group", c.NetworkSecurityGroup) 422 } 423 } 424 425 connType := "ssh" 426 if dpmt.RoleList[0].OSVirtualHardDisk.OS == windows { 427 connType = "winrm" 428 } 429 430 // Set the connection info for any configured provisioners 431 d.SetConnInfo(map[string]string{ 432 "type": connType, 433 "host": dpmt.VirtualIPs[0].Address, 434 "user": d.Get("username").(string), 435 "password": d.Get("password").(string), 436 }) 437 438 return nil 439 } 440 441 func resourceAzureInstanceUpdate(d *schema.ResourceData, meta interface{}) error { 442 azureClient := meta.(*Client) 443 mc := azureClient.mgmtClient 444 vmClient := azureClient.vmClient 445 446 // First check if anything we can update changed, and if not just return 447 if !d.HasChange("size") && !d.HasChange("endpoint") && !d.HasChange("security_group") { 448 return nil 449 } 450 451 name := d.Get("name").(string) 452 hostedServiceName := d.Get("hosted_service_name").(string) 453 454 // Get the current role 455 role, err := vmClient.GetRole(hostedServiceName, name, name) 456 if err != nil { 457 return fmt.Errorf("Error retrieving role of instance %s: %s", name, err) 458 } 459 460 // Verify if we have all required parameters 461 if err := verifyInstanceParameters(d, role.OSVirtualHardDisk.OS); err != nil { 462 return err 463 } 464 465 if d.HasChange("size") { 466 role.RoleSize = d.Get("size").(string) 467 } 468 469 if d.HasChange("endpoint") { 470 _, n := d.GetChange("endpoint") 471 472 // Delete the existing endpoints 473 for i, c := range role.ConfigurationSets { 474 if c.ConfigurationSetType == virtualmachine.ConfigurationSetTypeNetwork { 475 c.InputEndpoints = nil 476 role.ConfigurationSets[i] = c 477 } 478 } 479 480 // And add the ones we still want 481 if s := n.(*schema.Set); s.Len() > 0 { 482 for _, v := range s.List() { 483 m := v.(map[string]interface{}) 484 err := vmutils.ConfigureWithExternalPort( 485 role, 486 m["name"].(string), 487 m["private_port"].(int), 488 m["public_port"].(int), 489 endpointProtocol(m["protocol"].(string)), 490 ) 491 if err != nil { 492 return fmt.Errorf( 493 "Error adding endpoint %s for instance %s: %s", m["name"].(string), name, err) 494 } 495 } 496 } 497 } 498 499 if d.HasChange("security_group") { 500 sg := d.Get("security_group").(string) 501 err := vmutils.ConfigureWithSecurityGroup(role, sg) 502 if err != nil { 503 return fmt.Errorf( 504 "Error associating security group %s with instance %s: %s", sg, name, err) 505 } 506 } 507 508 // Update the adjusted role 509 req, err := vmClient.UpdateRole(hostedServiceName, name, name, *role) 510 if err != nil { 511 return fmt.Errorf("Error updating role of instance %s: %s", name, err) 512 } 513 514 if err := mc.WaitForOperation(req, nil); err != nil { 515 return fmt.Errorf( 516 "Error waiting for role of instance %s to be updated: %s", name, err) 517 } 518 519 return resourceAzureInstanceRead(d, meta) 520 } 521 522 func resourceAzureInstanceDelete(d *schema.ResourceData, meta interface{}) error { 523 azureClient := meta.(*Client) 524 mc := azureClient.mgmtClient 525 vmClient := azureClient.vmClient 526 hostedServiceClient := azureClient.hostedServiceClient 527 528 name := d.Get("name").(string) 529 hostedServiceName := d.Get("hosted_service_name").(string) 530 531 log.Printf("[DEBUG] Deleting instance: %s", name) 532 533 // check if the instance had a hosted service created especially for it: 534 if name == hostedServiceName { 535 // if so; we must delete the associated hosted service as well: 536 req, err := hostedServiceClient.DeleteHostedService(name, true) 537 if err != nil { 538 return fmt.Errorf("Error deleting instance and hosted service %s: %s", name, err) 539 } 540 541 // Wait until the hosted service and the instance it contains is deleted: 542 if err := mc.WaitForOperation(req, nil); err != nil { 543 return fmt.Errorf( 544 "Error waiting for instance %s to be deleted: %s", name, err) 545 } 546 } else { 547 // else; just delete the instance: 548 reqID, err := vmClient.DeleteDeployment(hostedServiceName, name) 549 if err != nil { 550 return fmt.Errorf("Error deleting instance %s off hosted service %s: %s", name, hostedServiceName, err) 551 } 552 553 // and wait for the deletion: 554 if err := mc.WaitForOperation(reqID, nil); err != nil { 555 return fmt.Errorf("Error waiting for intance %s to be deleted off the hosted service %s: %s", 556 name, hostedServiceName, err) 557 } 558 } 559 560 return nil 561 } 562 563 func resourceAzureEndpointHash(v interface{}) int { 564 var buf bytes.Buffer 565 m := v.(map[string]interface{}) 566 buf.WriteString(fmt.Sprintf("%s-", m["name"].(string))) 567 buf.WriteString(fmt.Sprintf("%s-", m["protocol"].(string))) 568 buf.WriteString(fmt.Sprintf("%d-", m["public_port"].(int))) 569 buf.WriteString(fmt.Sprintf("%d-", m["private_port"].(int))) 570 571 return hashcode.String(buf.String()) 572 } 573 574 func retrieveImageDetails( 575 meta interface{}, 576 label string, 577 name string, 578 storage string) (func(*virtualmachine.Role) error, string, error) { 579 580 azureClient := meta.(*Client) 581 vmImageClient := azureClient.vmImageClient 582 osImageClient := azureClient.osImageClient 583 584 configureForImage, osType, VMLabels, err := retrieveVMImageDetails(vmImageClient, label) 585 if err == nil { 586 return configureForImage, osType, nil 587 } 588 589 configureForImage, osType, OSLabels, err := retrieveOSImageDetails(osImageClient, label, name, storage) 590 if err == nil { 591 return configureForImage, osType, nil 592 } 593 594 if err == PlatformStorageError { 595 return nil, "", err 596 } 597 598 return nil, "", fmt.Errorf("Could not find image with label '%s'. Available images are: %s", 599 label, strings.Join(append(VMLabels, OSLabels...), ", ")) 600 } 601 602 func retrieveVMImageDetails( 603 vmImageClient virtualmachineimage.Client, 604 label string) (func(*virtualmachine.Role) error, string, []string, error) { 605 imgs, err := vmImageClient.ListVirtualMachineImages() 606 if err != nil { 607 return nil, "", nil, fmt.Errorf("Error retrieving image details: %s", err) 608 } 609 610 var labels []string 611 for _, img := range imgs.VMImages { 612 if img.Label == label { 613 if img.OSDiskConfiguration.OS != linux && img.OSDiskConfiguration.OS != windows { 614 return nil, "", nil, fmt.Errorf("Unsupported image OS: %s", img.OSDiskConfiguration.OS) 615 } 616 617 configureForImage := func(role *virtualmachine.Role) error { 618 return vmutils.ConfigureDeploymentFromVMImage( 619 role, 620 img.Name, 621 "", 622 true, 623 ) 624 } 625 626 return configureForImage, img.OSDiskConfiguration.OS, nil, nil 627 } 628 629 labels = append(labels, img.Label) 630 } 631 632 return nil, "", labels, fmt.Errorf("Could not find image with label '%s'", label) 633 } 634 635 func retrieveOSImageDetails( 636 osImageClient osimage.OSImageClient, 637 label string, 638 name string, 639 storage string) (func(*virtualmachine.Role) error, string, []string, error) { 640 imgs, err := osImageClient.ListOSImages() 641 if err != nil { 642 return nil, "", nil, fmt.Errorf("Error retrieving image details: %s", err) 643 } 644 645 var labels []string 646 for _, img := range imgs.OSImages { 647 if img.Label == label { 648 if img.OS != linux && img.OS != windows { 649 return nil, "", nil, fmt.Errorf("Unsupported image OS: %s", img.OS) 650 } 651 if img.MediaLink == "" { 652 if storage == "" { 653 return nil, "", nil, PlatformStorageError 654 } 655 img.MediaLink = fmt.Sprintf(osDiskBlobStorageURL, storage, name) 656 } 657 658 configureForImage := func(role *virtualmachine.Role) error { 659 return vmutils.ConfigureDeploymentFromPlatformImage( 660 role, 661 img.Name, 662 img.MediaLink, 663 label, 664 ) 665 } 666 667 return configureForImage, img.OS, nil, nil 668 } 669 670 labels = append(labels, img.Label) 671 } 672 673 return nil, "", labels, fmt.Errorf("Could not find image with label '%s'", label) 674 } 675 676 func endpointProtocol(p string) virtualmachine.InputEndpointProtocol { 677 if p == "tcp" { 678 return virtualmachine.InputEndpointProtocolTCP 679 } 680 681 return virtualmachine.InputEndpointProtocolUDP 682 } 683 684 func verifyInstanceParameters(d *schema.ResourceData, osType string) error { 685 if osType == linux { 686 _, pass := d.GetOk("password") 687 _, key := d.GetOk("ssh_key_thumbprint") 688 689 if !pass && !key { 690 return fmt.Errorf( 691 "You must supply a 'password' and/or a 'ssh_key_thumbprint' when using a Linux image") 692 } 693 } 694 695 if osType == windows { 696 if _, ok := d.GetOk("password"); !ok { 697 return fmt.Errorf("You must supply a 'password' when using a Windows image") 698 } 699 700 if _, ok := d.GetOk("time_zone"); !ok { 701 return fmt.Errorf("You must supply a 'time_zone' when using a Windows image") 702 } 703 } 704 705 if _, ok := d.GetOk("subnet"); ok { 706 if _, ok := d.GetOk("virtual_network"); !ok { 707 return fmt.Errorf("You must also supply a 'virtual_network' when supplying a 'subnet'") 708 } 709 } 710 711 if s := d.Get("endpoint").(*schema.Set); s.Len() > 0 { 712 for _, v := range s.List() { 713 protocol := v.(map[string]interface{})["protocol"].(string) 714 715 if protocol != "tcp" && protocol != "udp" { 716 return fmt.Errorf( 717 "Invalid endpoint protocol %s! Valid options are 'tcp' and 'udp'.", protocol) 718 } 719 } 720 } 721 722 return nil 723 }