provider/azurerm: Initial commit.

This commit brings some of the work over from #3808, but rearchitects to
use a separate provider for Azure Resource Manager. This is in line with
the decisions made by the Azure Powershell Cmdlets, and is important for
usability since the sets of required fields change between the ASM and
ARM APIs.

Currently `azurerm_resource_group` and `azurerm_virtual_network` are
implemented, more resources will follow.
This commit is contained in:
Nashwan Azhari 2015-12-08 17:30:12 -05:00 committed by James Nugent
parent 294d59cee4
commit c279adfc55
7 changed files with 953 additions and 0 deletions

View File

@ -0,0 +1,12 @@
package main
import (
"github.com/hashicorp/terraform/builtin/providers/azurerm"
"github.com/hashicorp/terraform/plugin"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: azurerm.Provider,
})
}

View File

@ -0,0 +1 @@
package main

View File

@ -0,0 +1,227 @@
package azurerm
import (
"encoding/json"
"fmt"
"github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/azure-sdk-for-go/arm/compute"
"github.com/Azure/azure-sdk-for-go/arm/network"
"github.com/Azure/azure-sdk-for-go/arm/resources"
"github.com/Azure/azure-sdk-for-go/arm/scheduler"
"github.com/Azure/azure-sdk-for-go/arm/storage"
"github.com/hashicorp/terraform/helper/pathorcontents"
)
// ArmClient contains the handles to all the specific Azure Resource Manager
// resource classes' respective clients.
type ArmClient struct {
availSetClient compute.AvailabilitySetsClient
usageOpsClient compute.UsageOperationsClient
vmExtensionImageClient compute.VirtualMachineExtensionImagesClient
vmExtensionClient compute.VirtualMachineExtensionsClient
vmImageClient compute.VirtualMachineImagesClient
vmClient compute.VirtualMachinesClient
appGatewayClient network.ApplicationGatewaysClient
ifaceClient network.InterfacesClient
loadBalancerClient network.LoadBalancersClient
localNetConnClient network.LocalNetworkGatewaysClient
publicIPClient network.PublicIPAddressesClient
secGroupClient network.SecurityGroupsClient
secRuleClient network.SecurityRulesClient
subnetClient network.SubnetsClient
netUsageClient network.UsagesClient
vnetGatewayConnectionsClient network.VirtualNetworkGatewayConnectionsClient
vnetGatewayClient network.VirtualNetworkGatewaysClient
vnetClient network.VirtualNetworksClient
resourceGroupClient resources.GroupsClient
tagsClient resources.TagsClient
jobsClient scheduler.JobsClient
jobsCollectionsClient scheduler.JobCollectionsClient
storageServiceClient storage.AccountsClient
storageUsageClient storage.UsageOperationsClient
}
// getArmClient is a helper method which returns a fully instantiated
// *ArmClient based on the Config's current settings.
func (c *Config) getArmClient() (*ArmClient, error) {
// first; check that all the necessary credentials were provided:
if !c._armCredentialsProvided() {
return nil, fmt.Errorf("Not all ARM-required fields have been provided.")
}
spt, err := azure.NewServicePrincipalToken(c.ClientID, c.ClientSecret, c.TenantID, azure.AzureResourceManagerScope)
if err != nil {
return nil, err
}
// client declarations:
client := ArmClient{}
// NOTE: these declarations should be left separate for clarity should the
// clients be wished to be configured with custom Responders/PollingModess etc...
asc := compute.NewAvailabilitySetsClient(c.SubscriptionID)
asc.Authorizer = spt
client.availSetClient = asc
uoc := compute.NewUsageOperationsClient(c.SubscriptionID)
uoc.Authorizer = spt
client.usageOpsClient = uoc
vmeic := compute.NewVirtualMachineExtensionImagesClient(c.SubscriptionID)
vmeic.Authorizer = spt
client.vmExtensionImageClient = vmeic
vmec := compute.NewVirtualMachineExtensionsClient(c.SubscriptionID)
vmec.Authorizer = spt
client.vmExtensionClient = vmec
vmic := compute.NewVirtualMachineImagesClient(c.SubscriptionID)
vmic.Authorizer = spt
client.vmImageClient = vmic
vmc := compute.NewVirtualMachinesClient(c.SubscriptionID)
vmc.Authorizer = spt
client.vmClient = vmc
agc := network.NewApplicationGatewaysClient(c.SubscriptionID)
agc.Authorizer = spt
client.appGatewayClient = agc
ifc := network.NewInterfacesClient(c.SubscriptionID)
ifc.Authorizer = spt
client.ifaceClient = ifc
lbc := network.NewLoadBalancersClient(c.SubscriptionID)
lbc.Authorizer = spt
client.loadBalancerClient = lbc
lgc := network.NewLocalNetworkGatewaysClient(c.SubscriptionID)
lgc.Authorizer = spt
client.localNetConnClient = lgc
pipc := network.NewPublicIPAddressesClient(c.SubscriptionID)
pipc.Authorizer = spt
client.publicIPClient = pipc
sgc := network.NewSecurityGroupsClient(c.SubscriptionID)
sgc.Authorizer = spt
client.secGroupClient = sgc
src := network.NewSecurityRulesClient(c.SubscriptionID)
src.Authorizer = spt
client.secRuleClient = src
snc := network.NewSubnetsClient(c.SubscriptionID)
snc.Authorizer = spt
client.subnetClient = snc
vgcc := network.NewVirtualNetworkGatewayConnectionsClient(c.SubscriptionID)
vgcc.Authorizer = spt
client.vnetGatewayConnectionsClient = vgcc
vgc := network.NewVirtualNetworkGatewaysClient(c.SubscriptionID)
vgc.Authorizer = spt
client.vnetGatewayClient = vgc
vnc := network.NewVirtualNetworksClient(c.SubscriptionID)
vnc.Authorizer = spt
client.vnetClient = vnc
rgc := resources.NewGroupsClient(c.SubscriptionID)
rgc.Authorizer = spt
client.resourceGroupClient = rgc
tc := resources.NewTagsClient(c.SubscriptionID)
tc.Authorizer = spt
client.tagsClient = tc
jc := scheduler.NewJobsClient(c.SubscriptionID)
jc.Authorizer = spt
client.jobsClient = jc
jcc := scheduler.NewJobCollectionsClient(c.SubscriptionID)
jcc.Authorizer = spt
client.jobsCollectionsClient = jcc
ssc := storage.NewAccountsClient(c.SubscriptionID)
ssc.Authorizer = spt
client.storageServiceClient = ssc
suc := storage.NewUsageOperationsClient(c.SubscriptionID)
suc.Authorizer = spt
client.storageUsageClient = suc
return &client, nil
}
// armCredentialsProvided is a helper method which indicates whether or not the
// credentials required for authenticating against the ARM APIs were provided.
func (c *Config) armCredentialsProvided() bool {
return c.ArmConfig != "" || c._armCredentialsProvided()
}
func (c *Config) _armCredentialsProvided() bool {
return !(c.SubscriptionID == "" || c.ClientID == "" || c.ClientSecret == "" || c.TenantID == "")
}
// readArmSettings is a helper method which; given the contents of the ARM
// credentials file, loads all the data into the Config.
func (c *Config) readArmSettings(contents string) error {
data := &armConfigData{}
err := json.Unmarshal([]byte(contents), data)
c.SubscriptionID = data.SubscriptionID
c.ClientID = data.ClientID
c.ClientSecret = data.ClientSecret
c.TenantID = data.TenantID
return err
}
// configFileContentsWarning represents the warning message returned when the
// path to the 'arm_config_file' is provided instead of its sourced contents.
var configFileContentsWarning = `
The path to the 'arm_config_file' was provided instead of its contents.
Support for accepting filepaths instead of their contents will be removed
in the near future. Do please consider switching over to using
'${file("/path/to/config.arm")}' instead.
`[1:]
// validateArmConfigFile is a helper function which verifies that
// the provided ARM configuration file is valid.
func validateArmConfigFile(v interface{}, _ string) (ws []string, es []error) {
value := v.(string)
if value == "" {
return nil, nil
}
pathOrContents, wasPath, err := pathorcontents.Read(v.(string))
if err != nil {
es = append(es, fmt.Errorf("Error reading 'arm_config_file': %s", err))
}
if wasPath {
ws = append(ws, configFileContentsWarning)
}
data := armConfigData{}
err = json.Unmarshal([]byte(pathOrContents), &data)
if err != nil {
es = append(es, fmt.Errorf("Error unmarshalling the provided 'arm_config_file': %s", err))
}
return
}
// armConfigData is a private struct which represents the expected layout of
// an ARM configuration file. It is used for unmarshalling purposes.
type armConfigData struct {
ClientID string `json:"clientID"`
ClientSecret string `json:"clientSecret"`
SubscriptionID string `json:"subscriptionID"`
TenantID string `json:"tenantID"`
}

View File

@ -0,0 +1,108 @@
package azurerm
import (
"fmt"
"strings"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
// Provider returns a terraform.ResourceProvider.
func Provider() terraform.ResourceProvider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
"arm_config_file": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "",
DefaultFunc: schema.EnvDefaultFunc("ARM_CONFIG_FILE", nil),
ValidateFunc: validateArmConfigFile,
},
"subscription_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("AZURE_SUBSCRIPTION_ID", ""),
},
"client_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_ID", ""),
},
"client_secret": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_SECRET", ""),
},
"tenant_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("ARM_TENANT_ID", ""),
},
},
ResourcesMap: map[string]*schema.Resource{
"azurerm_resource_group": resourceArmResourceGroup(),
"azurerm_virtual_network": resourceArmVirtualNetwork(),
},
ConfigureFunc: providerConfigure,
}
}
// Config is the configuration structure used to instantiate a
// new Azure management client.
type Config struct {
ManagementURL string
ArmConfig string
SubscriptionID string
ClientID string
ClientSecret string
TenantID string
}
const noConfigError = `Credentials must be provided either via arm_config_file, or via
subscription_id, client_id, client_secret and tenant_id. Please see
the provider documentation for more information on how to obtain these
credentials.`
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config := Config{
SubscriptionID: d.Get("subscription_id").(string),
ClientID: d.Get("client_id").(string),
ClientSecret: d.Get("client_secret").(string),
TenantID: d.Get("tenant_id").(string),
}
// check if credentials file is provided:
armConfig := d.Get("arm_config_file").(string)
if armConfig != "" {
// then, load the settings from that:
if err := config.readArmSettings(armConfig); err != nil {
return nil, err
}
}
// then; check whether the ARM credentials were provided:
if !config.armCredentialsProvided() {
return nil, fmt.Errorf(noConfigError)
}
client, err := config.getArmClient()
if err != nil {
return nil, err
}
return client, nil
}
func azureRMNormalizeLocation(location interface{}) string {
input := location.(string)
return strings.Replace(strings.ToLower(input), " ", "", -1)
}

View File

@ -0,0 +1,195 @@
package azurerm
import (
"io"
"io/ioutil"
"math/rand"
"os"
"strings"
"testing"
"time"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/go-homedir"
)
var testAccProviders map[string]terraform.ResourceProvider
var testAccProvider *schema.Provider
const (
testAccSecurityGroupName = "terraform-security-group"
testAccHostedServiceName = "terraform-testing-service"
)
// testAccStorageServiceName is used as the name for the Storage Service
// created in all storage-related tests.
// It is much more convenient to provide a Storage Service which
// has been created beforehand as the creation of one takes a lot
// and would greatly impede the multitude of tests which rely on one.
// NOTE: the storage container should be located in `West US`.
var testAccStorageServiceName = os.Getenv("AZURE_STORAGE")
const testAccStorageContainerName = "terraform-testing-container"
func init() {
testAccProvider = Provider().(*schema.Provider)
testAccProviders = map[string]terraform.ResourceProvider{
"azure": testAccProvider,
}
}
func TestProvider(t *testing.T) {
if err := Provider().(*schema.Provider).InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = Provider()
}
func testAccPreCheck(t *testing.T) {
if v := os.Getenv("AZURE_PUBLISH_SETTINGS"); v == "" {
subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID")
certificate := os.Getenv("AZURE_CERTIFICATE")
if subscriptionID == "" || certificate == "" {
t.Fatal("either AZURE_PUBLISH_SETTINGS, or AZURE_SUBSCRIPTION_ID " +
"and AZURE_CERTIFICATE must be set for acceptance tests")
}
}
if v := os.Getenv("AZURE_STORAGE"); v == "" {
t.Fatal("AZURE_STORAGE must be set for acceptance tests")
}
}
func TestAzure_validateSettingsFile(t *testing.T) {
f, err := ioutil.TempFile("", "tf-test")
if err != nil {
t.Fatalf("Error creating temporary file in TestAzure_validateSettingsFile: %s", err)
}
defer os.Remove(f.Name())
fx, err := ioutil.TempFile("", "tf-test-xml")
if err != nil {
t.Fatalf("Error creating temporary file with XML in TestAzure_validateSettingsFile: %s", err)
}
defer os.Remove(fx.Name())
_, err = io.WriteString(fx, "<PublishData></PublishData>")
if err != nil {
t.Fatalf("Error writing XML File: %s", err)
}
fx.Close()
home, err := homedir.Dir()
if err != nil {
t.Fatalf("Error fetching homedir: %s", err)
}
fh, err := ioutil.TempFile(home, "tf-test-home")
if err != nil {
t.Fatalf("Error creating homedir-based temporary file: %s", err)
}
defer os.Remove(fh.Name())
_, err = io.WriteString(fh, "<PublishData></PublishData>")
if err != nil {
t.Fatalf("Error writing XML File: %s", err)
}
fh.Close()
r := strings.NewReplacer(home, "~")
homePath := r.Replace(fh.Name())
cases := []struct {
Input string // String of XML or a path to an XML file
W int // expected count of warnings
E int // expected count of errors
}{
{"test", 0, 1},
{f.Name(), 1, 1},
{fx.Name(), 1, 0},
{homePath, 1, 0},
{"<PublishData></PublishData>", 0, 0},
}
for _, tc := range cases {
w, e := validateSettingsFile(tc.Input, "")
if len(w) != tc.W {
t.Errorf("Error in TestAzureValidateSettingsFile: input: %s , warnings: %v, errors: %v", tc.Input, w, e)
}
if len(e) != tc.E {
t.Errorf("Error in TestAzureValidateSettingsFile: input: %s , warnings: %v, errors: %v", tc.Input, w, e)
}
}
}
func TestAzure_providerConfigure(t *testing.T) {
home, err := homedir.Dir()
if err != nil {
t.Fatalf("Error fetching homedir: %s", err)
}
fh, err := ioutil.TempFile(home, "tf-test-home")
if err != nil {
t.Fatalf("Error creating homedir-based temporary file: %s", err)
}
defer os.Remove(fh.Name())
_, err = io.WriteString(fh, testAzurePublishSettingsStr)
if err != nil {
t.Fatalf("err: %s", err)
}
fh.Close()
r := strings.NewReplacer(home, "~")
homePath := r.Replace(fh.Name())
cases := []struct {
SettingsFile string // String of XML or a path to an XML file
NilMeta bool // whether meta is expected to be nil
}{
{testAzurePublishSettingsStr, false},
{homePath, false},
}
for _, tc := range cases {
rp := Provider()
raw := map[string]interface{}{
"settings_file": tc.SettingsFile,
}
rawConfig, err := config.NewRawConfig(raw)
if err != nil {
t.Fatalf("err: %s", err)
}
err = rp.Configure(terraform.NewResourceConfig(rawConfig))
meta := rp.(*schema.Provider).Meta()
if (meta == nil) != tc.NilMeta {
t.Fatalf("expected NilMeta: %t, got meta: %#v, settings_file: %q",
tc.NilMeta, meta, tc.SettingsFile)
}
}
}
func genRandInt() int {
return rand.New(rand.NewSource(time.Now().UnixNano())).Int() % 100000
}
// testAzurePublishSettingsStr is a revoked publishsettings file
const testAzurePublishSettingsStr = `
<?xml version="1.0" encoding="utf-8"?>
<PublishData>
<PublishProfile
SchemaVersion="2.0"
PublishMethod="AzureServiceManagementAPI">
<Subscription
ServiceManagementUrl="https://management.core.windows.net"
Id="a65bf94f-26b3-4fb1-9d50-6e27c6096df1"
Name="terraform-testing"
ManagementCertificate="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" />
</PublishProfile>
</PublishData>
`

View File

@ -0,0 +1,140 @@
package azurerm
import (
"fmt"
"log"
"time"
"github.com/Azure/azure-sdk-for-go/arm/resources"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)
// resourceArmResourceGroup returns the *schema.Resource
// associated to resource group resources on ARM.
func resourceArmResourceGroup() *schema.Resource {
return &schema.Resource{
Create: resourceArmResourceGroupCreate,
Read: resourceArmResourceGroupRead,
Exists: resourceArmResourceGroupExists,
Delete: resourceArmResourceGroupDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
//TODO(jen20) - implement validation func: {resource-group-name} must uniquely identify the resource group within the subscription. It must be no longer than 80 characters long. It can only contain alphanumeric characters, dash, underscore, opening parenthesis, closing parenthesis or period. The name cannot end with a period.
},
"location": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
StateFunc: azureRMNormalizeLocation,
},
},
}
}
// resourceArmResourceGroupCreate goes ahead and creates the specified ARM resource group.
func resourceArmResourceGroupCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ArmClient)
resGroupClient := client.resourceGroupClient
name := d.Get("name").(string)
location := d.Get("location").(string)
log.Printf("[INFO] Issuing Azure ARM creation request for resource group '%s'.", name)
rg := resources.ResourceGroup{
Name: &name,
Location: &location,
}
_, err := resGroupClient.CreateOrUpdate(name, rg)
if err != nil {
return fmt.Errorf("Error issuing Azure ARM create request for resource group '%s': %s", name, err)
}
d.SetId(*rg.Name)
// Wait for the resource group to become available
// TODO(jen20): Is there any need for this?
log.Printf("[DEBUG] Waiting for Resource Group (%s) to become available", d.Id())
stateConf := &resource.StateChangeConf{
Pending: []string{"Accepted"},
Target: "Succeeded",
Refresh: resourceGroupStateRefreshFunc(client, d.Id()),
Timeout: 10 * time.Minute,
}
if _, err := stateConf.WaitForState(); err != nil {
return fmt.Errorf("Error waiting for Resource Group (%s) to become available: %s", d.Id(), err)
}
return resourceArmResourceGroupRead(d, meta)
}
// resourceArmResourceGroupRead goes ahead and reads the state of the corresponding ARM resource group.
func resourceArmResourceGroupRead(d *schema.ResourceData, meta interface{}) error {
resGroupClient := meta.(*ArmClient).resourceGroupClient
name := d.Id()
log.Printf("[INFO] Issuing read request to Azure ARM for resource group '%s'.", name)
res, err := resGroupClient.Get(name)
if err != nil {
return fmt.Errorf("Error issuing read request to Azure ARM for resource group '%s': %s", name, err)
}
d.Set("name", *res.Name)
d.Set("location", *res.Location)
return nil
}
// resourceArmResourceGroupExists goes ahead and checks for the existence of the correspoding ARM resource group.
func resourceArmResourceGroupExists(d *schema.ResourceData, meta interface{}) (bool, error) {
resGroupClient := meta.(*ArmClient).resourceGroupClient
name := d.Id()
resp, err := resGroupClient.CheckExistence(name)
if err != nil {
// TODO(aznashwan): implement some error switching helpers in the SDK
// to avoid HTTP error checks such as the below:
if resp.StatusCode != 200 {
return false, err
}
return true, nil
}
return true, nil
}
// resourceArmResourceGroupDelete deletes the specified ARM resource group.
func resourceArmResourceGroupDelete(d *schema.ResourceData, meta interface{}) error {
resGroupClient := meta.(*ArmClient).resourceGroupClient
name := d.Id()
_, err := resGroupClient.Delete(name)
if err != nil {
return err
}
return nil
}
// resourceGroupStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
// a resource group.
func resourceGroupStateRefreshFunc(client *ArmClient, id string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
res, err := client.resourceGroupClient.Get(id)
if err != nil {
return nil, "", fmt.Errorf("Error issuing read request in resourceGroupStateRefreshFunc to Azure ARM for resource group '%s': %s", id, err)
}
return res, *res.Properties.ProvisioningState, nil
}
}

View File

@ -0,0 +1,270 @@
package azurerm
import (
"fmt"
"log"
"net/http"
"time"
"github.com/Azure/azure-sdk-for-go/arm/network"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceArmVirtualNetwork() *schema.Resource {
return &schema.Resource{
Create: resourceArmVirtualNetworkCreate,
Read: resourceArmVirtualNetworkRead,
Update: resourceArmVirtualNetworkUpdate,
Delete: resourceArmVirtualNetworkDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"address_space": &schema.Schema{
Type: schema.TypeList,
Required: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"dns_servers_names": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"subnet": &schema.Schema{
Type: schema.TypeSet,
Required: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"address_prefix": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"security_group": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
},
Set: resourceAzureSubnetHash,
},
"location": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
StateFunc: azureRMNormalizeLocation,
},
"resource_group_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
},
}
}
// resourceArmVirtualNetworkCreate creates the specified ARM virtual network.
func resourceArmVirtualNetworkCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ArmClient)
vnetClient := client.vnetClient
log.Printf("[INFO] preparing arguments for Azure ARM virtual network creation.")
name := d.Get("name").(string)
location := d.Get("location").(string)
resGroup := d.Get("resource_group_name").(string)
vnet := network.VirtualNetwork{
Name: &name,
Location: &location,
Properties: getVirtualNetworkProperties(d),
}
log.Printf("[INFO] Sending virtual network create request to ARM.")
_, err := vnetClient.CreateOrUpdate(resGroup, name, vnet)
if err != nil {
return err
}
// if res.Response.StatusCode != http.StatusAccepted {
// return fmt.Errorf("Creation request was denies: code: %d", res.Response.StatusCode)
// }
d.SetId(name)
d.Set("resGroup", resGroup)
// Wait for the resource group to become available
// TODO(jen20): Is there any need for this?
log.Printf("[DEBUG] Waiting for Virtual Network (%s) to become available", d.Id())
stateConf := &resource.StateChangeConf{
Pending: []string{"Accepted", "Updating"},
Target: "Succeeded",
Refresh: virtualNetworkStateRefreshFunc(client, resGroup, name),
Timeout: 10 * time.Minute,
}
if _, err := stateConf.WaitForState(); err != nil {
return fmt.Errorf("Error waiting for Virtual Network (%s) to become available: %s", d.Id(), err)
}
return resourceArmVirtualNetworkRead(d, meta)
}
// resourceArmVirtualNetworkRead goes ahead and reads the state of the corresponding ARM virtual network.
func resourceArmVirtualNetworkRead(d *schema.ResourceData, meta interface{}) error {
vnetClient := meta.(*ArmClient).vnetClient
name := d.Get("name").(string)
resGroup := d.Get("resource_group_name").(string)
log.Printf("[INFO] Sending virtual network read request to ARM.")
resp, err := vnetClient.Get(resGroup, name)
if resp.StatusCode == http.StatusNotFound {
// it means the virtual network has been deleted in the meantime;
// so we must go ahead and remove it here:
d.SetId("")
return nil
}
if err != nil {
return fmt.Errorf("Error making Read request on Azure virtual network %s: %s", name, err)
}
vnet := *resp.Properties
// update all the appropriate values:
d.Set("address_space", vnet.AddressSpace.AddressPrefixes)
// read state of subnets:
subnets := &schema.Set{
F: resourceAzureSubnetHash,
}
for _, subnet := range *vnet.Subnets {
s := map[string]interface{}{}
s["name"] = *subnet.Name
s["address_prefix"] = *subnet.Properties.AddressPrefix
// NOTE(aznashwan): ID's necessary?
if subnet.Properties.NetworkSecurityGroup != nil {
s["security_group"] = *subnet.Properties.NetworkSecurityGroup.ID
}
subnets.Add(s)
}
d.Set("subnet", subnets)
// now; dns servers:
dnses := []string{}
for _, dns := range *vnet.DhcpOptions.DNSServers {
dnses = append(dnses, dns)
}
d.Set("dns_servers_names", dnses)
return nil
}
// resourceArmVirtualNetworkUpdate goes ahead and updates the corresponding ARM virtual network.
func resourceArmVirtualNetworkUpdate(d *schema.ResourceData, meta interface{}) error {
// considering Create's idempotency, Update is simply a proxy for it...
// Update has been left as a separate function here for utmost clarity:
return resourceArmVirtualNetworkCreate(d, meta)
}
// resourceArmVirtualNetworkDelete deletes the specified ARM virtual network.
func resourceArmVirtualNetworkDelete(d *schema.ResourceData, meta interface{}) error {
vnetClient := meta.(*ArmClient).vnetClient
name := d.Get("name").(string)
resGroup := d.Get("resource_group_name").(string)
_, err := vnetClient.Delete(resGroup, name)
return err
}
// getVirtualNetworkProperties is a helper function which returns the
// VirtualNetworkPropertiesFormat of the network resource.
func getVirtualNetworkProperties(d *schema.ResourceData) *network.VirtualNetworkPropertiesFormat {
// first; get address space prefixes:
prefixes := []string{}
for _, prefix := range d.Get("address_space").([]interface{}) {
prefixes = append(prefixes, prefix.(string))
}
// then; the dns servers:
dnses := []string{}
for _, dns := range d.Get("dns_servers_names").([]interface{}) {
dnses = append(dnses, dns.(string))
}
// then; the subnets:
subnets := []network.Subnet{}
if subs := d.Get("subnet").(*schema.Set); subs.Len() > 0 {
for _, subnet := range subs.List() {
subnet := subnet.(map[string]interface{})
name := subnet["name"].(string)
prefix := subnet["address_prefix"].(string)
secGroup := subnet["security_group"].(string)
var subnetObj network.Subnet
subnetObj.Name = &name
subnetObj.Properties = &network.SubnetPropertiesFormat{}
subnetObj.Properties.AddressPrefix = &prefix
if secGroup != "" {
subnetObj.Properties.NetworkSecurityGroup = &network.SubResource{
ID: &secGroup,
}
}
subnets = append(subnets, subnetObj)
}
}
// finally; return the struct:
return &network.VirtualNetworkPropertiesFormat{
AddressSpace: &network.AddressSpace{
AddressPrefixes: &prefixes,
},
DhcpOptions: &network.DhcpOptions{
DNSServers: &dnses,
},
Subnets: &subnets,
}
}
func resourceAzureSubnetHash(v interface{}) int {
m := v.(map[string]interface{})
subnet := m["name"].(string) + m["address_prefix"].(string)
if securityGroup, present := m["security_group"]; present {
subnet = subnet + securityGroup.(string)
}
return hashcode.String(subnet)
}
// virtualNetworkStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
// a virtual network.
func virtualNetworkStateRefreshFunc(client *ArmClient, resourceGroupName string, networkName string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
res, err := client.vnetClient.Get(resourceGroupName, networkName)
if err != nil {
return nil, "", fmt.Errorf("Error issuing read request in virtualNetworkStateRefreshFunc to Azure ARM for virtual network '%s' (RG: '%s'): %s", networkName, resourceGroupName, err)
}
return res, *res.Properties.ProvisioningState, nil
}
}