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