github.com/koding/terraform@v0.6.4-0.20170608090606-5d7e0339779d/builtin/providers/azure/resource_azure_instance.go (about) 1 package azure 2 3 import ( 4 "bytes" 5 "crypto/sha1" 6 "encoding/base64" 7 "encoding/hex" 8 "fmt" 9 "log" 10 "strings" 11 "time" 12 13 "github.com/Azure/azure-sdk-for-go/management/hostedservice" 14 "github.com/Azure/azure-sdk-for-go/management/osimage" 15 "github.com/Azure/azure-sdk-for-go/management/virtualmachine" 16 "github.com/Azure/azure-sdk-for-go/management/virtualmachineimage" 17 "github.com/Azure/azure-sdk-for-go/management/vmutils" 18 "github.com/hashicorp/terraform/helper/hashcode" 19 "github.com/hashicorp/terraform/helper/resource" 20 "github.com/hashicorp/terraform/helper/schema" 21 ) 22 23 const ( 24 linux = "Linux" 25 windows = "Windows" 26 storageContainterName = "vhds" 27 osDiskBlobNameFormat = "%s.vhd" 28 osDiskBlobStorageURL = "http://%s.blob.core.windows.net/" + storageContainterName + "/" + osDiskBlobNameFormat 29 ) 30 31 func resourceAzureInstance() *schema.Resource { 32 return &schema.Resource{ 33 Create: resourceAzureInstanceCreate, 34 Read: resourceAzureInstanceRead, 35 Update: resourceAzureInstanceUpdate, 36 Delete: resourceAzureInstanceDelete, 37 38 Schema: map[string]*schema.Schema{ 39 "name": &schema.Schema{ 40 Type: schema.TypeString, 41 Required: true, 42 ForceNew: true, 43 }, 44 45 "hosted_service_name": &schema.Schema{ 46 Type: schema.TypeString, 47 Optional: true, 48 Computed: true, 49 ForceNew: true, 50 }, 51 52 // in order to prevent an unintentional delete of a containing 53 // hosted service in the case the same name are given to both the 54 // service and the instance despite their being created separately, 55 // we must maintain a flag to definitively denote whether this 56 // instance had a hosted service created for it or not: 57 "has_dedicated_service": &schema.Schema{ 58 Type: schema.TypeBool, 59 Computed: true, 60 }, 61 62 "description": &schema.Schema{ 63 Type: schema.TypeString, 64 Optional: true, 65 Computed: true, 66 ForceNew: true, 67 }, 68 69 "image": &schema.Schema{ 70 Type: schema.TypeString, 71 Required: true, 72 ForceNew: true, 73 }, 74 75 "size": &schema.Schema{ 76 Type: schema.TypeString, 77 Required: true, 78 }, 79 80 "subnet": &schema.Schema{ 81 Type: schema.TypeString, 82 Optional: true, 83 Computed: true, 84 ForceNew: true, 85 }, 86 87 "virtual_network": &schema.Schema{ 88 Type: schema.TypeString, 89 Optional: true, 90 ForceNew: true, 91 }, 92 93 "storage_service_name": &schema.Schema{ 94 Type: schema.TypeString, 95 Optional: true, 96 ForceNew: true, 97 }, 98 99 "reverse_dns": &schema.Schema{ 100 Type: schema.TypeString, 101 Optional: true, 102 ForceNew: true, 103 }, 104 105 "location": &schema.Schema{ 106 Type: schema.TypeString, 107 Required: true, 108 ForceNew: true, 109 }, 110 111 "automatic_updates": &schema.Schema{ 112 Type: schema.TypeBool, 113 Optional: true, 114 Default: false, 115 ForceNew: true, 116 }, 117 118 "time_zone": &schema.Schema{ 119 Type: schema.TypeString, 120 Optional: true, 121 ForceNew: true, 122 }, 123 124 "username": &schema.Schema{ 125 Type: schema.TypeString, 126 Required: true, 127 ForceNew: true, 128 }, 129 130 "password": &schema.Schema{ 131 Type: schema.TypeString, 132 Optional: true, 133 ForceNew: true, 134 }, 135 136 "ssh_key_thumbprint": &schema.Schema{ 137 Type: schema.TypeString, 138 Optional: true, 139 ForceNew: true, 140 }, 141 142 "endpoint": &schema.Schema{ 143 Type: schema.TypeSet, 144 Optional: true, 145 Computed: true, 146 Elem: &schema.Resource{ 147 Schema: map[string]*schema.Schema{ 148 "name": &schema.Schema{ 149 Type: schema.TypeString, 150 Required: true, 151 }, 152 153 "protocol": &schema.Schema{ 154 Type: schema.TypeString, 155 Optional: true, 156 Default: "tcp", 157 }, 158 159 "public_port": &schema.Schema{ 160 Type: schema.TypeInt, 161 Required: true, 162 }, 163 164 "private_port": &schema.Schema{ 165 Type: schema.TypeInt, 166 Required: true, 167 }, 168 }, 169 }, 170 Set: resourceAzureEndpointHash, 171 }, 172 173 "security_group": &schema.Schema{ 174 Type: schema.TypeString, 175 Optional: true, 176 Computed: true, 177 }, 178 179 "ip_address": &schema.Schema{ 180 Type: schema.TypeString, 181 Computed: true, 182 }, 183 184 "vip_address": &schema.Schema{ 185 Type: schema.TypeString, 186 Computed: true, 187 }, 188 189 "domain_name": &schema.Schema{ 190 Type: schema.TypeString, 191 Optional: true, 192 ForceNew: true, 193 }, 194 195 "domain_username": &schema.Schema{ 196 Type: schema.TypeString, 197 Optional: true, 198 ForceNew: true, 199 }, 200 201 "domain_password": &schema.Schema{ 202 Type: schema.TypeString, 203 Optional: true, 204 ForceNew: true, 205 }, 206 207 "domain_ou": &schema.Schema{ 208 Type: schema.TypeString, 209 Optional: true, 210 ForceNew: true, 211 }, 212 213 "custom_data": &schema.Schema{ 214 Type: schema.TypeString, 215 Optional: true, 216 ForceNew: true, 217 StateFunc: func(v interface{}) string { 218 if s, ok := v.(string); ok && s != "" { 219 hash := sha1.Sum([]byte(s)) 220 return hex.EncodeToString(hash[:]) 221 } 222 return "" 223 }, 224 }, 225 }, 226 } 227 } 228 229 func resourceAzureInstanceCreate(d *schema.ResourceData, meta interface{}) (err error) { 230 azureClient := meta.(*Client) 231 mc := azureClient.mgmtClient 232 hostedServiceClient := azureClient.hostedServiceClient 233 vmClient := azureClient.vmClient 234 235 name := d.Get("name").(string) 236 237 // Compute/set the description 238 description := d.Get("description").(string) 239 if description == "" { 240 description = name 241 } 242 243 // Retrieve the needed details of the image 244 configureForImage, osType, err := retrieveImageDetails( 245 meta, 246 d.Get("image").(string), 247 name, 248 d.Get("storage_service_name").(string), 249 ) 250 if err != nil { 251 return err 252 } 253 254 // Verify if we have all required parameters 255 if err := verifyInstanceParameters(d, osType); err != nil { 256 return err 257 } 258 259 var hostedServiceName string 260 // check if hosted service name parameter was given: 261 if serviceName, ok := d.GetOk("hosted_service_name"); !ok { 262 // if not provided; just use the name of the instance to create a new one: 263 hostedServiceName = name 264 d.Set("hosted_service_name", hostedServiceName) 265 d.Set("has_dedicated_service", true) 266 267 p := hostedservice.CreateHostedServiceParameters{ 268 ServiceName: hostedServiceName, 269 Label: base64.StdEncoding.EncodeToString([]byte(name)), 270 Description: fmt.Sprintf("Cloud Service created automatically for instance %s", name), 271 Location: d.Get("location").(string), 272 ReverseDNSFqdn: d.Get("reverse_dns").(string), 273 } 274 275 log.Printf("[DEBUG] Creating Cloud Service for instance: %s", name) 276 err = hostedServiceClient.CreateHostedService(p) 277 if err != nil { 278 return fmt.Errorf("Error creating Cloud Service for instance %s: %s", name, err) 279 } 280 } else { 281 // else; use the provided hosted service name: 282 hostedServiceName = serviceName.(string) 283 } 284 285 // Create a new role for the instance 286 role := vmutils.NewVMConfiguration(name, d.Get("size").(string)) 287 288 log.Printf("[DEBUG] Configuring deployment from image...") 289 err = configureForImage(&role) 290 if err != nil { 291 return fmt.Errorf("Error configuring the deployment for %s: %s", name, err) 292 } 293 294 var customData string 295 if data, ok := d.GetOk("custom_data"); ok { 296 data := data.(string) 297 298 // Ensure the custom_data is not double-encoded. 299 if _, err := base64.StdEncoding.DecodeString(data); err != nil { 300 customData = base64.StdEncoding.EncodeToString([]byte(data)) 301 } else { 302 customData = data 303 } 304 } 305 306 if osType == linux { 307 // This is pretty ugly, but the Azure SDK leaves me no other choice... 308 if tp, ok := d.GetOk("ssh_key_thumbprint"); ok { 309 err = vmutils.ConfigureForLinux( 310 &role, 311 name, 312 d.Get("username").(string), 313 d.Get("password").(string), 314 tp.(string), 315 ) 316 } else { 317 err = vmutils.ConfigureForLinux( 318 &role, 319 name, 320 d.Get("username").(string), 321 d.Get("password").(string), 322 ) 323 } 324 if err != nil { 325 return fmt.Errorf("Error configuring %s for Linux: %s", name, err) 326 } 327 328 if customData != "" { 329 err = vmutils.ConfigureWithCustomDataForLinux(&role, customData) 330 if err != nil { 331 return fmt.Errorf("Error configuring custom data for %s: %s", name, err) 332 } 333 } 334 } 335 336 if osType == windows { 337 err = vmutils.ConfigureForWindows( 338 &role, 339 name, 340 d.Get("username").(string), 341 d.Get("password").(string), 342 d.Get("automatic_updates").(bool), 343 d.Get("time_zone").(string), 344 ) 345 if err != nil { 346 return fmt.Errorf("Error configuring %s for Windows: %s", name, err) 347 } 348 349 if domain_name, ok := d.GetOk("domain_name"); ok { 350 err = vmutils.ConfigureWindowsToJoinDomain( 351 &role, 352 d.Get("domain_username").(string), 353 d.Get("domain_password").(string), 354 domain_name.(string), 355 d.Get("domain_ou").(string), 356 ) 357 if err != nil { 358 return fmt.Errorf("Error configuring %s for WindowsToJoinDomain: %s", name, err) 359 } 360 } 361 362 if customData != "" { 363 err = vmutils.ConfigureWithCustomDataForWindows(&role, customData) 364 if err != nil { 365 return fmt.Errorf("Error configuring custom data for %s: %s", name, err) 366 } 367 } 368 } 369 370 if s := d.Get("endpoint").(*schema.Set); s.Len() > 0 { 371 for _, v := range s.List() { 372 m := v.(map[string]interface{}) 373 err := vmutils.ConfigureWithExternalPort( 374 &role, 375 m["name"].(string), 376 m["private_port"].(int), 377 m["public_port"].(int), 378 endpointProtocol(m["protocol"].(string)), 379 ) 380 if err != nil { 381 return fmt.Errorf( 382 "Error adding endpoint %s for instance %s: %s", m["name"].(string), name, err) 383 } 384 } 385 } 386 387 if subnet, ok := d.GetOk("subnet"); ok { 388 err = vmutils.ConfigureWithSubnet(&role, subnet.(string)) 389 if err != nil { 390 return fmt.Errorf( 391 "Error associating subnet %s with instance %s: %s", d.Get("subnet").(string), name, err) 392 } 393 } 394 395 if sg, ok := d.GetOk("security_group"); ok { 396 err = vmutils.ConfigureWithSecurityGroup(&role, sg.(string)) 397 if err != nil { 398 return fmt.Errorf( 399 "Error associating security group %s with instance %s: %s", sg.(string), name, err) 400 } 401 } 402 403 log.Printf("[DEBUG] Checking if this cloud service already has a deployment...") 404 existingDeploymentName, err := vmClient.GetDeploymentName(hostedServiceName) 405 if err != nil { 406 return fmt.Errorf("Error creating instance %s while checking whether cloud service %s has existing deployments: %s", name, hostedServiceName, err) 407 } 408 409 // existingDeploymentName empty means this is the first VM being attached to this cloud service 410 // we have to call the CreateDeployment(...) method 411 if existingDeploymentName == "" { 412 options := virtualmachine.CreateDeploymentOptions{ 413 VirtualNetworkName: d.Get("virtual_network").(string), 414 } 415 416 log.Printf("[DEBUG] Creating the new VM instance along with a new deployment...") 417 req, err := vmClient.CreateDeployment(role, hostedServiceName, options) 418 if err != nil { 419 return fmt.Errorf("Error creating instance %s: %s", name, err) 420 } 421 422 log.Printf("[DEBUG] Waiting for the new instance to be created...") 423 if err := mc.WaitForOperation(req, nil); err != nil { 424 return fmt.Errorf("Error waiting for instance %s to be created: %s", name, err) 425 } 426 427 // existingDeploymentName has a value means this cloud service already has other VM-s, we will have 428 // to call AddRole(...) to add this VM to the existing deployment under this cloud service 429 } else { 430 log.Printf("[DEBUG] Adding the new VM instance to an existing deployment...") 431 addRoleOpId, err := vmClient.AddRole(hostedServiceName, existingDeploymentName, role) 432 if err != nil { 433 return fmt.Errorf("Error creating and adding new instance %s to cloud service %s with existing deployment %s: %s", name, hostedServiceName, existingDeploymentName, err) 434 } 435 436 log.Printf("[DEBUG] Waiting for the new instance to be created and added to the existing cloud service and deployment...") 437 if err := mc.WaitForOperation(addRoleOpId, nil); err != nil { 438 return fmt.Errorf("Error waiting for instance %s to be created and added to cloud service %s with existing deployment %s: %s", name, hostedServiceName, existingDeploymentName, err) 439 } 440 } 441 442 d.SetId(name) 443 444 return resourceAzureInstanceRead(d, meta) 445 } 446 447 func resourceAzureInstanceRead(d *schema.ResourceData, meta interface{}) error { 448 azureClient := meta.(*Client) 449 hostedServiceClient := azureClient.hostedServiceClient 450 vmClient := azureClient.vmClient 451 452 name := d.Get("name").(string) 453 454 // check if the instance belongs to an independent hosted service 455 // or it had one created for it. 456 var hostedServiceName string 457 if serviceName, ok := d.GetOk("hosted_service_name"); ok { 458 // if independent; use that hosted service name: 459 hostedServiceName = serviceName.(string) 460 } else { 461 // else; suppose it's the instance's name: 462 hostedServiceName = name 463 } 464 465 log.Printf("[DEBUG] Retrieving Cloud Service for instance: %s", name) 466 cs, err := hostedServiceClient.GetHostedService(hostedServiceName) 467 if err != nil { 468 return fmt.Errorf("Error retrieving Cloud Service of instance %s (%q): %s", name, hostedServiceName, err) 469 } 470 471 d.Set("reverse_dns", cs.ReverseDNSFqdn) 472 d.Set("location", cs.Location) 473 474 log.Printf("[DEBUG] Retrieving instance: %s", name) 475 deploymentName, err := vmClient.GetDeploymentName(hostedServiceName) 476 if err != nil { 477 return fmt.Errorf("Error retrieving deployment from cloud service %s while trying to read instance %s: %s", hostedServiceName, name, err) 478 } 479 if deploymentName == "" { 480 return fmt.Errorf("Error retrieving deployment from cloud service %s while trying to read instance %s: No deployment exists!", hostedServiceName, name) 481 } 482 dpmt, err := vmClient.GetDeployment(hostedServiceName, deploymentName) 483 if err != nil { 484 return fmt.Errorf("Error retrieving deployment %s while trying to read instance %s: %s", deploymentName, name, err) 485 } 486 487 // A cloud service has one or more deployments(in the case of terraform, we will support 488 // just one deployment, that in the "Production" deployment slot) 489 // Each deployment has one or more Roles. Each Role has one or more Role Instances 490 // However, both the RoleList array and RoleInstanceList array are contained as part of DeploymentResponse struct 491 // see here: https://msdn.microsoft.com/en-us/library/azure/ee460804.aspx 492 // Also notable is that terraform is, until now, for IAAS infrastructure only (as opposed to PAAS web and worker roles) 493 // Therefore, the Role-s we create here will have a RoleType field set to "PersistentVMRole" 494 495 if len(dpmt.RoleList) < 1 { 496 return fmt.Errorf("Error reading instance %s: RoleList for deployment %s is empty", name, deploymentName) 497 } 498 if len(dpmt.RoleInstanceList) < 1 { 499 return fmt.Errorf("Error reading instance %s: RoleInstanceList for deployment %s is empty", name, deploymentName) 500 } 501 502 //roleInst is a pointer that will point to the correct element in the dpmt.RoleInstanceList array 503 var roleInst *virtualmachine.RoleInstance = nil 504 for i := range dpmt.RoleInstanceList { 505 if dpmt.RoleInstanceList[i].InstanceName == name { 506 roleInst = &dpmt.RoleInstanceList[i] 507 break 508 } 509 } 510 if roleInst == nil { 511 return fmt.Errorf("Error reading instance %s: RoleInstanceList does not contain any VM by that name", name) 512 } 513 514 //role is a pointer that will point to the correct element in the dpmt.RoleList array 515 var role *virtualmachine.Role = nil 516 for j := range dpmt.RoleList { 517 if dpmt.RoleList[j].RoleName == roleInst.RoleName { 518 role = &dpmt.RoleList[j] 519 break 520 } 521 } 522 if role == nil { 523 return fmt.Errorf("Error reading instance %s: RoleList does not contain any Role by the name %s", name, roleInst.RoleName) 524 } 525 526 //Now populate various fields in d from either role or roleInst, whichever makes sense 527 d.Set("size", role.RoleSize) 528 d.Set("ip_address", roleInst.IPAddress) 529 if len(roleInst.InstanceEndpoints) > 0 { 530 d.Set("vip_address", roleInst.InstanceEndpoints[0].Vip) 531 } 532 533 // Find the network configuration set 534 for _, c := range role.ConfigurationSets { 535 if c.ConfigurationSetType == virtualmachine.ConfigurationSetTypeNetwork { 536 // Create a new set to hold all configured endpoints 537 endpoints := &schema.Set{ 538 F: resourceAzureEndpointHash, 539 } 540 541 // Loop through all endpoints 542 for _, ep := range c.InputEndpoints { 543 endpoint := map[string]interface{}{} 544 545 // Update the values 546 endpoint["name"] = ep.Name 547 endpoint["protocol"] = string(ep.Protocol) 548 endpoint["public_port"] = ep.Port 549 endpoint["private_port"] = ep.LocalPort 550 endpoints.Add(endpoint) 551 } 552 d.Set("endpoint", endpoints) 553 554 // Update the subnet 555 switch len(c.SubnetNames) { 556 case 1: 557 d.Set("subnet", c.SubnetNames[0]) 558 case 0: 559 d.Set("subnet", "") 560 default: 561 return fmt.Errorf("Instance %s has an unexpected number of associated subnets %d", name, len(c.SubnetNames)) 562 } 563 564 // Update the security group 565 d.Set("security_group", c.NetworkSecurityGroup) 566 } 567 } 568 569 connType := "ssh" 570 if role.OSVirtualHardDisk.OS == windows { 571 connType = "winrm" 572 } 573 574 // Set the connection info for any configured provisioners 575 d.SetConnInfo(map[string]string{ 576 "type": connType, 577 "host": dpmt.VirtualIPs[0].Address, 578 "user": d.Get("username").(string), 579 "password": d.Get("password").(string), 580 }) 581 582 return nil 583 } 584 585 func resourceAzureInstanceUpdate(d *schema.ResourceData, meta interface{}) error { 586 azureClient := meta.(*Client) 587 mc := azureClient.mgmtClient 588 vmClient := azureClient.vmClient 589 590 // First check if anything we can update changed, and if not just return 591 if !d.HasChange("size") && !d.HasChange("endpoint") && !d.HasChange("security_group") { 592 return nil 593 } 594 595 name := d.Get("name").(string) 596 597 // check if the instance belongs to an independent hosted service 598 // or it had one created for it. 599 var hostedServiceName string 600 if serviceName, ok := d.GetOk("hosted_service_name"); ok { 601 // if independent; use that hosted service name: 602 hostedServiceName = serviceName.(string) 603 } else { 604 // else; suppose it's the instance's name: 605 hostedServiceName = name 606 } 607 608 deploymentName, err := vmClient.GetDeploymentName(hostedServiceName) 609 if err != nil { 610 return fmt.Errorf("Error retrieving deployment from cloud service %s while trying to update instance %s: %s", hostedServiceName, name, err) 611 } 612 if deploymentName == "" { 613 return fmt.Errorf("Error retrieving deployment from cloud service %s while trying to update instance %s: No deployment exists!", hostedServiceName, name) 614 } 615 616 // Get the current role 617 role, err := vmClient.GetRole(hostedServiceName, deploymentName, name) 618 if err != nil { 619 return fmt.Errorf("Error retrieving role of instance %s: %s", name, err) 620 } 621 622 // Verify if we have all required parameters 623 if err := verifyInstanceParameters(d, role.OSVirtualHardDisk.OS); err != nil { 624 return err 625 } 626 627 if d.HasChange("size") { 628 role.RoleSize = d.Get("size").(string) 629 } 630 631 if d.HasChange("endpoint") { 632 _, n := d.GetChange("endpoint") 633 634 // Delete the existing endpoints 635 for i, c := range role.ConfigurationSets { 636 if c.ConfigurationSetType == virtualmachine.ConfigurationSetTypeNetwork { 637 c.InputEndpoints = nil 638 role.ConfigurationSets[i] = c 639 } 640 } 641 642 // And add the ones we still want 643 if s := n.(*schema.Set); s.Len() > 0 { 644 for _, v := range s.List() { 645 m := v.(map[string]interface{}) 646 err := vmutils.ConfigureWithExternalPort( 647 role, 648 m["name"].(string), 649 m["private_port"].(int), 650 m["public_port"].(int), 651 endpointProtocol(m["protocol"].(string)), 652 ) 653 if err != nil { 654 return fmt.Errorf( 655 "Error adding endpoint %s for instance %s: %s", m["name"].(string), name, err) 656 } 657 } 658 } 659 } 660 661 if d.HasChange("security_group") { 662 sg := d.Get("security_group").(string) 663 err := vmutils.ConfigureWithSecurityGroup(role, sg) 664 if err != nil { 665 return fmt.Errorf( 666 "Error associating security group %s with instance %s: %s", sg, name, err) 667 } 668 } 669 670 // Update the adjusted role 671 req, err := vmClient.UpdateRole(hostedServiceName, deploymentName, name, *role) 672 if err != nil { 673 return fmt.Errorf("Error updating role of instance %s: %s", name, err) 674 } 675 676 if err := mc.WaitForOperation(req, nil); err != nil { 677 return fmt.Errorf( 678 "Error waiting for role of instance %s to be updated: %s", name, err) 679 } 680 681 return resourceAzureInstanceRead(d, meta) 682 } 683 684 func resourceAzureInstanceDelete(d *schema.ResourceData, meta interface{}) error { 685 azureClient := meta.(*Client) 686 mc := azureClient.mgmtClient 687 vmClient := azureClient.vmClient 688 689 name := d.Get("name").(string) 690 691 // check if the instance belongs to an independent hosted service 692 // or it had one created for it. 693 var hostedServiceName string 694 if serviceName, ok := d.GetOk("hosted_service_name"); ok { 695 // if independent; use that hosted service name: 696 hostedServiceName = serviceName.(string) 697 } else { 698 // else; suppose it's the instance's name: 699 hostedServiceName = name 700 } 701 702 log.Printf("[DEBUG] Deleting instance: %s", name) 703 704 //Note: The logic around "has_dedicated_service" works well in spite of the fact that it was added before the code 705 //changes necessray to handle multiple VM-s per cloud service. That is because this internal flag is set ONLY when 706 //the cloud service was created along with the VM creation. Therefore, no matter what happened afterwards, it should 707 //get destroyed when the VM is getting destroyed. 708 709 // check if the instance had a hosted service created especially for it: 710 if d.Get("has_dedicated_service").(bool) { 711 // if so; we must delete the associated hosted service as well: 712 hostedServiceClient := azureClient.hostedServiceClient 713 req, err := hostedServiceClient.DeleteHostedService(hostedServiceName, true) 714 if err != nil { 715 return fmt.Errorf("Error deleting instance %s: Error deleting hosted service %s: %s", name, hostedServiceName, err) 716 } 717 718 // Wait until the hosted service and the instance it contains is deleted: 719 if err := mc.WaitForOperation(req, nil); err != nil { 720 return fmt.Errorf( 721 "Error waiting for instance %s to be deleted: %s", name, err) 722 } 723 } else { 724 // Here, the idea is that we do not delete the entire deployment if there are other 725 // VM-s still existing (*after* we delete the current VM). So, first, we call GetDeployment 726 // on the deployment name (which we obtain by calling GetDeploymentName) to check if this 727 // is the last VM. If yes, we blow away the whole deployment. Else we just call DeleteRole 728 729 // First, call GetDeploymentName so that we can call GetDeployment() with that deployment name 730 deploymentName, err := vmClient.GetDeploymentName(hostedServiceName) 731 if err != nil { 732 return fmt.Errorf("Error retrieving deployment from cloud service %s while trying to delete instance %s: %s", hostedServiceName, name, err) 733 } 734 if deploymentName == "" { 735 return fmt.Errorf("Error retrieving deployment from cloud service %s while trying to delete instance %s: No deployment exists!", hostedServiceName, name) 736 } 737 738 // Second, call GetDeployment() to retrieve the whole DeploymentResponse struct, so that we can check RoleInstanceList array's length 739 dpmt, err := vmClient.GetDeployment(hostedServiceName, deploymentName) 740 if err != nil { 741 return fmt.Errorf("Error retrieving deployment %s while trying to delete instance %s: %s", deploymentName, name, err) 742 } 743 744 // Third, check RoleInstanceList array's length to determine if this is the last VM in this deployment. 745 // If yes, remove the whole deployment. Else, remove just the VM using DeleteRole(...) 746 if len(dpmt.RoleInstanceList) == 1 { 747 reqID, err := vmClient.DeleteDeployment(hostedServiceName, name) 748 if err != nil { 749 return fmt.Errorf("Error deleting instance %s off hosted service %s: %s", name, hostedServiceName, err) 750 } 751 752 // and wait for the deletion: 753 if err := mc.WaitForOperation(reqID, nil); err != nil { 754 return fmt.Errorf("Error waiting for intance %s to be deleted off the hosted service %s: %s", 755 name, hostedServiceName, err) 756 } 757 } else { 758 // This is not the last VM in the deployment, call DeleteRole 759 delRoleOpId, err := vmClient.DeleteRole(hostedServiceName, deploymentName, name, true) 760 if err != nil { 761 return fmt.Errorf("Error trying to delete instance %s: %s", name, err) 762 } 763 764 // Wait for that call to complete 765 if err := mc.WaitForOperation(delRoleOpId, nil); err != nil { 766 return fmt.Errorf("Error waiting for instance %s to be deleted: %s", name, err) 767 } 768 } 769 } 770 771 log.Printf("[INFO] Waiting for the deletion of instance '%s''s disk blob.", name) 772 773 // in order to avoid `terraform taint`-like scenarios in which the instance 774 // is deleted and re-created so fast the previous storage blob which held 775 // the image doesn't manage to get deleted (despite it being in a 776 // 'deleting' state) and a lease conflict occurs over it, we must ensure 777 // the blob got completely deleted as well: 778 storName := d.Get("storage_service_name").(string) 779 blobClient, err := azureClient.getStorageServiceBlobClient(storName) 780 if err != nil { 781 return err 782 } 783 784 err = resource.Retry(15*time.Minute, func() *resource.RetryError { 785 container := blobClient.GetContainerReference(storageContainterName) 786 blobName := fmt.Sprintf(osDiskBlobNameFormat, name) 787 blob := container.GetBlobReference(blobName) 788 exists, err := blob.Exists() 789 if err != nil { 790 return resource.NonRetryableError(err) 791 } 792 793 if exists { 794 return resource.RetryableError( 795 fmt.Errorf("Instance '%s''s disk storage blob still exists.", name)) 796 } 797 798 return nil 799 }) 800 801 return err 802 } 803 804 func resourceAzureEndpointHash(v interface{}) int { 805 var buf bytes.Buffer 806 m := v.(map[string]interface{}) 807 buf.WriteString(fmt.Sprintf("%s-", m["name"].(string))) 808 buf.WriteString(fmt.Sprintf("%s-", m["protocol"].(string))) 809 buf.WriteString(fmt.Sprintf("%d-", m["public_port"].(int))) 810 buf.WriteString(fmt.Sprintf("%d-", m["private_port"].(int))) 811 812 return hashcode.String(buf.String()) 813 } 814 815 func retrieveImageDetails( 816 meta interface{}, 817 label string, 818 name string, 819 storage string) (func(*virtualmachine.Role) error, string, error) { 820 821 azureClient := meta.(*Client) 822 vmImageClient := azureClient.vmImageClient 823 osImageClient := azureClient.osImageClient 824 825 configureForImage, osType, VMLabels, err := retrieveVMImageDetails(vmImageClient, label) 826 if err == nil { 827 return configureForImage, osType, nil 828 } 829 830 configureForImage, osType, OSLabels, err := retrieveOSImageDetails(osImageClient, label, name, storage) 831 if err == nil { 832 return configureForImage, osType, nil 833 } 834 835 if err == PlatformStorageError { 836 return nil, "", err 837 } 838 839 return nil, "", fmt.Errorf("Could not find image with label '%s'. Available images are: %s", 840 label, strings.Join(append(VMLabels, OSLabels...), ", ")) 841 } 842 843 func retrieveVMImageDetails( 844 vmImageClient virtualmachineimage.Client, 845 label string) (func(*virtualmachine.Role) error, string, []string, error) { 846 imgs, err := vmImageClient.ListVirtualMachineImages(virtualmachineimage.ListParameters{}) 847 if err != nil { 848 return nil, "", nil, fmt.Errorf("Error retrieving image details: %s", err) 849 } 850 851 var labels []string 852 for _, img := range imgs.VMImages { 853 if img.Label == label { 854 if img.OSDiskConfiguration.OS != linux && img.OSDiskConfiguration.OS != windows { 855 return nil, "", nil, fmt.Errorf("Unsupported image OS: %s", img.OSDiskConfiguration.OS) 856 } 857 858 configureForImage := func(role *virtualmachine.Role) error { 859 return vmutils.ConfigureDeploymentFromPublishedVMImage( 860 role, 861 img.Name, 862 "", 863 true, 864 ) 865 } 866 867 return configureForImage, img.OSDiskConfiguration.OS, nil, nil 868 } 869 870 labels = append(labels, img.Label) 871 } 872 873 return nil, "", labels, fmt.Errorf("Could not find image with label '%s'", label) 874 } 875 876 func retrieveOSImageDetails( 877 osImageClient osimage.OSImageClient, 878 label string, 879 name string, 880 storage string) (func(*virtualmachine.Role) error, string, []string, error) { 881 882 imgs, err := osImageClient.ListOSImages() 883 if err != nil { 884 return nil, "", nil, fmt.Errorf("Error retrieving image details: %s", err) 885 } 886 887 var labels []string 888 for _, img := range imgs.OSImages { 889 if img.Label == label { 890 if img.OS != linux && img.OS != windows { 891 return nil, "", nil, fmt.Errorf("Unsupported image OS: %s", img.OS) 892 } 893 if img.MediaLink == "" { 894 if storage == "" { 895 return nil, "", nil, PlatformStorageError 896 } 897 img.MediaLink = fmt.Sprintf(osDiskBlobStorageURL, storage, name) 898 } 899 900 configureForImage := func(role *virtualmachine.Role) error { 901 return vmutils.ConfigureDeploymentFromPlatformImage( 902 role, 903 img.Name, 904 img.MediaLink, 905 label, 906 ) 907 } 908 909 return configureForImage, img.OS, nil, nil 910 } 911 912 labels = append(labels, img.Label) 913 } 914 915 return nil, "", labels, fmt.Errorf("Could not find image with label '%s'", label) 916 } 917 918 func endpointProtocol(p string) virtualmachine.InputEndpointProtocol { 919 if p == "tcp" { 920 return virtualmachine.InputEndpointProtocolTCP 921 } 922 923 return virtualmachine.InputEndpointProtocolUDP 924 } 925 926 func verifyInstanceParameters(d *schema.ResourceData, osType string) error { 927 if osType == linux { 928 _, pass := d.GetOk("password") 929 _, key := d.GetOk("ssh_key_thumbprint") 930 931 if !pass && !key { 932 return fmt.Errorf( 933 "You must supply a 'password' and/or a 'ssh_key_thumbprint' when using a Linux image") 934 } 935 } 936 937 if osType == windows { 938 if _, ok := d.GetOk("password"); !ok { 939 return fmt.Errorf("You must supply a 'password' when using a Windows image") 940 } 941 942 if _, ok := d.GetOk("time_zone"); !ok { 943 return fmt.Errorf("You must supply a 'time_zone' when using a Windows image") 944 } 945 } 946 947 if _, ok := d.GetOk("subnet"); ok { 948 if _, ok := d.GetOk("virtual_network"); !ok { 949 return fmt.Errorf("You must also supply a 'virtual_network' when supplying a 'subnet'") 950 } 951 } 952 953 if s := d.Get("endpoint").(*schema.Set); s.Len() > 0 { 954 for _, v := range s.List() { 955 protocol := v.(map[string]interface{})["protocol"].(string) 956 957 if protocol != "tcp" && protocol != "udp" { 958 return fmt.Errorf( 959 "Invalid endpoint protocol %s! Valid options are 'tcp' and 'udp'.", protocol) 960 } 961 } 962 } 963 964 return nil 965 }