terraform/builtin/providers/azure/resource_azure_instance.go

671 lines
17 KiB
Go

package azure
import (
"bytes"
"encoding/base64"
"fmt"
"log"
"strings"
"github.com/Azure/azure-sdk-for-go/management"
"github.com/Azure/azure-sdk-for-go/management/hostedservice"
"github.com/Azure/azure-sdk-for-go/management/osimage"
"github.com/Azure/azure-sdk-for-go/management/virtualmachine"
"github.com/Azure/azure-sdk-for-go/management/virtualmachineimage"
"github.com/Azure/azure-sdk-for-go/management/vmutils"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
)
const (
linux = "Linux"
windows = "Windows"
osDiskBlobStorageURL = "http://%s.blob.core.windows.net/vhds/%s.vhd"
)
func resourceAzureInstance() *schema.Resource {
return &schema.Resource{
Create: resourceAzureInstanceCreate,
Read: resourceAzureInstanceRead,
Update: resourceAzureInstanceUpdate,
Delete: resourceAzureInstanceDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"description": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"image": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"size": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"subnet": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"virtual_network": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"storage_service_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"reverse_dns": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"location": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"automatic_updates": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
ForceNew: true,
},
"time_zone": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"username": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"password": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"ssh_key_thumbprint": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"endpoint": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"protocol": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "tcp",
},
"public_port": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"private_port": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
},
},
Set: resourceAzureEndpointHash,
},
"security_group": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"ip_address": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"vip_address": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
}
}
func resourceAzureInstanceCreate(d *schema.ResourceData, meta interface{}) (err error) {
mc := meta.(*Client).mgmtClient
name := d.Get("name").(string)
// Compute/set the description
description := d.Get("description").(string)
if description == "" {
description = name
}
// Retrieve the needed details of the image
configureForImage, osType, err := retrieveImageDetails(
mc,
d.Get("image").(string),
name,
d.Get("storage_service_name").(string),
)
if err != nil {
return err
}
// Verify if we have all required parameters
if err := verifyInstanceParameters(d, osType); err != nil {
return err
}
p := hostedservice.CreateHostedServiceParameters{
ServiceName: name,
Label: base64.StdEncoding.EncodeToString([]byte(name)),
Description: fmt.Sprintf("Cloud Service created automatically for instance %s", name),
Location: d.Get("location").(string),
ReverseDNSFqdn: d.Get("reverse_dns").(string),
}
log.Printf("[DEBUG] Creating Cloud Service for instance: %s", name)
err = hostedservice.NewClient(mc).CreateHostedService(p)
if err != nil {
return fmt.Errorf("Error creating Cloud Service for instance %s: %s", name, err)
}
// Put in this defer here, so we are sure to cleanup already created parts
// when we exit with an error
defer func(mc management.Client) {
if err != nil {
req, err := hostedservice.NewClient(mc).DeleteHostedService(name, true)
if err != nil {
log.Printf("[DEBUG] Error cleaning up Cloud Service of instance %s: %s", name, err)
}
// Wait until the Cloud Service is deleted
if err := mc.WaitForOperation(req, nil); err != nil {
log.Printf(
"[DEBUG] Error waiting for Cloud Service of instance %s to be deleted: %s", name, err)
}
}
}(mc)
// Create a new role for the instance
role := vmutils.NewVMConfiguration(name, d.Get("size").(string))
log.Printf("[DEBUG] Configuring deployment from image...")
err = configureForImage(&role)
if err != nil {
return fmt.Errorf("Error configuring the deployment for %s: %s", name, err)
}
if osType == linux {
// This is pretty ugly, but the Azure SDK leaves me no other choice...
if tp, ok := d.GetOk("ssh_key_thumbprint"); ok {
err = vmutils.ConfigureForLinux(
&role,
name,
d.Get("username").(string),
d.Get("password").(string),
tp.(string),
)
} else {
err = vmutils.ConfigureForLinux(
&role,
name,
d.Get("username").(string),
d.Get("password").(string),
)
}
if err != nil {
return fmt.Errorf("Error configuring %s for Linux: %s", name, err)
}
}
if osType == windows {
err = vmutils.ConfigureForWindows(
&role,
name,
d.Get("username").(string),
d.Get("password").(string),
d.Get("automatic_updates").(bool),
d.Get("time_zone").(string),
)
if err != nil {
return fmt.Errorf("Error configuring %s for Windows: %s", name, err)
}
}
if s := d.Get("endpoint").(*schema.Set); s.Len() > 0 {
for _, v := range s.List() {
m := v.(map[string]interface{})
err := vmutils.ConfigureWithExternalPort(
&role,
m["name"].(string),
m["private_port"].(int),
m["public_port"].(int),
endpointProtocol(m["protocol"].(string)),
)
if err != nil {
return fmt.Errorf(
"Error adding endpoint %s for instance %s: %s", m["name"].(string), name, err)
}
}
}
if subnet, ok := d.GetOk("subnet"); ok {
err = vmutils.ConfigureWithSubnet(&role, subnet.(string))
if err != nil {
return fmt.Errorf(
"Error associating subnet %s with instance %s: %s", d.Get("subnet").(string), name, err)
}
}
if sg, ok := d.GetOk("security_group"); ok {
err = vmutils.ConfigureWithSecurityGroup(&role, sg.(string))
if err != nil {
return fmt.Errorf(
"Error associating security group %s with instance %s: %s", sg.(string), name, err)
}
}
options := virtualmachine.CreateDeploymentOptions{
VirtualNetworkName: d.Get("virtual_network").(string),
}
log.Printf("[DEBUG] Creating the new instance...")
req, err := virtualmachine.NewClient(mc).CreateDeployment(role, name, options)
if err != nil {
return fmt.Errorf("Error creating instance %s: %s", name, err)
}
log.Printf("[DEBUG] Waiting for the new instance to be created...")
if err := mc.WaitForOperation(req, nil); err != nil {
return fmt.Errorf(
"Error waiting for instance %s to be created: %s", name, err)
}
d.SetId(name)
return resourceAzureInstanceRead(d, meta)
}
func resourceAzureInstanceRead(d *schema.ResourceData, meta interface{}) error {
mc := meta.(*Client).mgmtClient
log.Printf("[DEBUG] Retrieving Cloud Service for instance: %s", d.Id())
cs, err := hostedservice.NewClient(mc).GetHostedService(d.Id())
if err != nil {
return fmt.Errorf("Error retrieving Cloud Service of instance %s: %s", d.Id(), err)
}
d.Set("reverse_dns", cs.ReverseDNSFqdn)
d.Set("location", cs.Location)
log.Printf("[DEBUG] Retrieving instance: %s", d.Id())
dpmt, err := virtualmachine.NewClient(mc).GetDeployment(d.Id(), d.Id())
if err != nil {
if management.IsResourceNotFoundError(err) {
d.SetId("")
return nil
}
return fmt.Errorf("Error retrieving instance %s: %s", d.Id(), err)
}
if len(dpmt.RoleList) != 1 {
return fmt.Errorf(
"Instance %s has an unexpected number of roles: %d", d.Id(), len(dpmt.RoleList))
}
d.Set("size", dpmt.RoleList[0].RoleSize)
if len(dpmt.RoleInstanceList) != 1 {
return fmt.Errorf(
"Instance %s has an unexpected number of role instances: %d",
d.Id(), len(dpmt.RoleInstanceList))
}
d.Set("ip_address", dpmt.RoleInstanceList[0].IPAddress)
if len(dpmt.RoleInstanceList[0].InstanceEndpoints) > 0 {
d.Set("vip_address", dpmt.RoleInstanceList[0].InstanceEndpoints[0].Vip)
}
// Find the network configuration set
for _, c := range dpmt.RoleList[0].ConfigurationSets {
if c.ConfigurationSetType == virtualmachine.ConfigurationSetTypeNetwork {
// Create a new set to hold all configured endpoints
endpoints := &schema.Set{
F: resourceAzureEndpointHash,
}
// Loop through all endpoints
for _, ep := range c.InputEndpoints {
endpoint := map[string]interface{}{}
// Update the values
endpoint["name"] = ep.Name
endpoint["protocol"] = string(ep.Protocol)
endpoint["public_port"] = ep.Port
endpoint["private_port"] = ep.LocalPort
endpoints.Add(endpoint)
}
d.Set("endpoint", endpoints)
// Update the subnet
switch len(c.SubnetNames) {
case 1:
d.Set("subnet", c.SubnetNames[0])
case 0:
d.Set("subnet", "")
default:
return fmt.Errorf(
"Instance %s has an unexpected number of associated subnets %d",
d.Id(), len(dpmt.RoleInstanceList))
}
// Update the security group
d.Set("security_group", c.NetworkSecurityGroup)
}
}
connType := "ssh"
if dpmt.RoleList[0].OSVirtualHardDisk.OS == windows {
connType = "winrm"
}
// Set the connection info for any configured provisioners
d.SetConnInfo(map[string]string{
"type": connType,
"host": dpmt.VirtualIPs[0].Address,
"user": d.Get("username").(string),
"password": d.Get("password").(string),
})
return nil
}
func resourceAzureInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
mc := meta.(*Client).mgmtClient
// First check if anything we can update changed, and if not just return
if !d.HasChange("size") && !d.HasChange("endpoint") && !d.HasChange("security_group") {
return nil
}
// Get the current role
role, err := virtualmachine.NewClient(mc).GetRole(d.Id(), d.Id(), d.Id())
if err != nil {
return fmt.Errorf("Error retrieving role of instance %s: %s", d.Id(), err)
}
// Verify if we have all required parameters
if err := verifyInstanceParameters(d, role.OSVirtualHardDisk.OS); err != nil {
return err
}
if d.HasChange("size") {
role.RoleSize = d.Get("size").(string)
}
if d.HasChange("endpoint") {
_, n := d.GetChange("endpoint")
// Delete the existing endpoints
for i, c := range role.ConfigurationSets {
if c.ConfigurationSetType == virtualmachine.ConfigurationSetTypeNetwork {
c.InputEndpoints = nil
role.ConfigurationSets[i] = c
}
}
// And add the ones we still want
if s := n.(*schema.Set); s.Len() > 0 {
for _, v := range s.List() {
m := v.(map[string]interface{})
err := vmutils.ConfigureWithExternalPort(
role,
m["name"].(string),
m["private_port"].(int),
m["public_port"].(int),
endpointProtocol(m["protocol"].(string)),
)
if err != nil {
return fmt.Errorf(
"Error adding endpoint %s for instance %s: %s", m["name"].(string), d.Id(), err)
}
}
}
}
if d.HasChange("security_group") {
sg := d.Get("security_group").(string)
err := vmutils.ConfigureWithSecurityGroup(role, sg)
if err != nil {
return fmt.Errorf(
"Error associating security group %s with instance %s: %s", sg, d.Id(), err)
}
}
// Update the adjusted role
req, err := virtualmachine.NewClient(mc).UpdateRole(d.Id(), d.Id(), d.Id(), *role)
if err != nil {
return fmt.Errorf("Error updating role of instance %s: %s", d.Id(), err)
}
if err := mc.WaitForOperation(req, nil); err != nil {
return fmt.Errorf(
"Error waiting for role of instance %s to be updated: %s", d.Id(), err)
}
return resourceAzureInstanceRead(d, meta)
}
func resourceAzureInstanceDelete(d *schema.ResourceData, meta interface{}) error {
mc := meta.(*Client).mgmtClient
log.Printf("[DEBUG] Deleting instance: %s", d.Id())
req, err := hostedservice.NewClient(mc).DeleteHostedService(d.Id(), true)
if err != nil {
return fmt.Errorf("Error deleting instance %s: %s", d.Id(), err)
}
// Wait until the instance is deleted
if err := mc.WaitForOperation(req, nil); err != nil {
return fmt.Errorf(
"Error waiting for instance %s to be deleted: %s", d.Id(), err)
}
d.SetId("")
return nil
}
func resourceAzureEndpointHash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["name"].(string)))
buf.WriteString(fmt.Sprintf("%s-", m["protocol"].(string)))
buf.WriteString(fmt.Sprintf("%d-", m["public_port"].(int)))
buf.WriteString(fmt.Sprintf("%d-", m["private_port"].(int)))
return hashcode.String(buf.String())
}
func retrieveImageDetails(
mc management.Client,
label string,
name string,
storage string) (func(*virtualmachine.Role) error, string, error) {
configureForImage, osType, VMLabels, err := retrieveVMImageDetails(mc, label)
if err == nil {
return configureForImage, osType, nil
}
configureForImage, osType, OSLabels, err := retrieveOSImageDetails(mc, label, name, storage)
if err == nil {
return configureForImage, osType, nil
}
return nil, "", fmt.Errorf("Could not find image with label '%s'. Available images are: %s",
label, strings.Join(append(VMLabels, OSLabels...), ", "))
}
func retrieveVMImageDetails(
mc management.Client,
label string) (func(*virtualmachine.Role) error, string, []string, error) {
imgs, err := virtualmachineimage.NewClient(mc).ListVirtualMachineImages()
if err != nil {
return nil, "", nil, fmt.Errorf("Error retrieving image details: %s", err)
}
var labels []string
for _, img := range imgs.VMImages {
if img.Label == label {
if img.OSDiskConfiguration.OS != linux && img.OSDiskConfiguration.OS != windows {
return nil, "", nil, fmt.Errorf("Unsupported image OS: %s", img.OSDiskConfiguration.OS)
}
configureForImage := func(role *virtualmachine.Role) error {
return vmutils.ConfigureDeploymentFromVMImage(
role,
img.Name,
"",
true,
)
}
return configureForImage, img.OSDiskConfiguration.OS, nil, nil
}
labels = append(labels, img.Label)
}
return nil, "", labels, fmt.Errorf("Could not find image with label '%s'", label)
}
func retrieveOSImageDetails(
mc management.Client,
label string,
name string,
storage string) (func(*virtualmachine.Role) error, string, []string, error) {
imgs, err := osimage.NewClient(mc).ListOSImages()
if err != nil {
return nil, "", nil, fmt.Errorf("Error retrieving image details: %s", err)
}
var labels []string
for _, img := range imgs.OSImages {
if img.Label == label {
if img.OS != linux && img.OS != windows {
return nil, "", nil, fmt.Errorf("Unsupported image OS: %s", img.OS)
}
if img.MediaLink == "" {
if storage == "" {
return nil, "", nil,
fmt.Errorf("When using a platform image, the 'storage' parameter is required")
}
img.MediaLink = fmt.Sprintf(osDiskBlobStorageURL, storage, name)
}
configureForImage := func(role *virtualmachine.Role) error {
return vmutils.ConfigureDeploymentFromPlatformImage(
role,
img.Name,
img.MediaLink,
label,
)
}
return configureForImage, img.OS, nil, nil
}
labels = append(labels, img.Label)
}
return nil, "", labels, fmt.Errorf("Could not find image with label '%s'", label)
}
func endpointProtocol(p string) virtualmachine.InputEndpointProtocol {
if p == "tcp" {
return virtualmachine.InputEndpointProtocolTCP
}
return virtualmachine.InputEndpointProtocolUDP
}
func verifyInstanceParameters(d *schema.ResourceData, osType string) error {
if osType == linux {
_, pass := d.GetOk("password")
_, key := d.GetOk("ssh_key_thumbprint")
if !pass && !key {
return fmt.Errorf(
"You must supply a 'password' and/or a 'ssh_key_thumbprint' when using a Linux image")
}
}
if osType == windows {
if _, ok := d.GetOk("password"); !ok {
return fmt.Errorf("You must supply a 'password' when using a Windows image")
}
if _, ok := d.GetOk("time_zone"); !ok {
return fmt.Errorf("You must supply a 'time_zone' when using a Windows image")
}
}
if _, ok := d.GetOk("subnet"); ok {
if _, ok := d.GetOk("virtual_network"); !ok {
return fmt.Errorf("You must also supply a 'virtual_network' when supplying a 'subnet'")
}
}
if s := d.Get("endpoint").(*schema.Set); s.Len() > 0 {
for _, v := range s.List() {
protocol := v.(map[string]interface{})["protocol"].(string)
if protocol != "tcp" && protocol != "udp" {
return fmt.Errorf(
"Invalid endpoint protocol %s! Valid options are 'tcp' and 'udp'.", protocol)
}
}
}
return nil
}