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  }