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