[MS] provider/azurerm: New resource - Express Route Circuit (#14265)

* Adds ExpressRoute circuit documentation

* Adds tests and doc improvements

* Code for basic Express Route Circuit support

* Use the built-in validation helper

* Added ignoreCaseDiffSuppressFunc to a few fields

* Added more information to docs

* Touchup

* Moving SKU properties into a set.

* Updates doc

* A bit more tweaks

* Switch to Sprintf for test string

* Updating the acceptance test name for consistency
This commit is contained in:
Jay Wang 2017-05-14 11:30:14 -07:00 committed by Tom Harvey
parent 11e386993b
commit 6ca50dd81d
9 changed files with 524 additions and 0 deletions

View File

@ -53,6 +53,7 @@ type ArmClient struct {
appGatewayClient network.ApplicationGatewaysClient
ifaceClient network.InterfacesClient
expressRouteCircuitClient network.ExpressRouteCircuitsClient
loadBalancerClient network.LoadBalancersClient
localNetConnClient network.LocalNetworkGatewaysClient
publicIPClient network.PublicIPAddressesClient
@ -281,6 +282,12 @@ func (c *Config) getArmClient() (*ArmClient, error) {
ifc.Sender = autorest.CreateSender(withRequestLogging())
client.ifaceClient = ifc
erc := network.NewExpressRouteCircuitsClientWithBaseURI(endpoint, c.SubscriptionID)
setUserAgent(&erc.Client)
erc.Authorizer = spt
erc.Sender = autorest.CreateSender(withRequestLogging())
client.expressRouteCircuitClient = erc
lbc := network.NewLoadBalancersClientWithBaseURI(endpoint, c.SubscriptionID)
setUserAgent(&lbc.Client)
lbc.Authorizer = spt

View File

@ -0,0 +1,40 @@
package azurerm
import (
"fmt"
"net/http"
"github.com/Azure/azure-sdk-for-go/arm/network"
"github.com/hashicorp/errwrap"
)
func extractResourceGroupAndErcName(resourceId string) (resourceGroup string, name string, err error) {
id, err := parseAzureResourceID(resourceId)
if err != nil {
return "", "", err
}
resourceGroup = id.ResourceGroup
name = id.Path["expressRouteCircuits"]
return
}
func retrieveErcByResourceId(resourceId string, meta interface{}) (erc *network.ExpressRouteCircuit, resourceGroup string, e error) {
ercClient := meta.(*ArmClient).expressRouteCircuitClient
resGroup, name, err := extractResourceGroupAndErcName(resourceId)
if err != nil {
return nil, "", errwrap.Wrapf("Error Parsing Azure Resource ID - {{err}}", err)
}
resp, err := ercClient.Get(resGroup, name)
if err != nil {
if resp.StatusCode == http.StatusNotFound {
return nil, "", nil
}
return nil, "", errwrap.Wrapf(fmt.Sprintf("Error making Read request on Express Route Circuit %s: {{err}}", name), err)
}
return &resp, resGroup, nil
}

View File

@ -0,0 +1,29 @@
package azurerm
import (
"testing"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
)
func TestAccAzureRMExpressRouteCircuit_importBasic(t *testing.T) {
resourceName := "azurerm_express_route_circuit.test"
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testCheckAzureRMExpressRouteCircuitDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAzureRMExpressRouteCircuit_basic(acctest.RandInt()),
},
resource.TestStep{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}

View File

@ -78,6 +78,8 @@ func Provider() terraform.ResourceProvider {
"azurerm_eventhub_consumer_group": resourceArmEventHubConsumerGroup(),
"azurerm_eventhub_namespace": resourceArmEventHubNamespace(),
"azurerm_express_route_circuit": resourceArmExpressRouteCircuit(),
"azurerm_lb": resourceArmLoadBalancer(),
"azurerm_lb_backend_address_pool": resourceArmLoadBalancerBackendAddressPool(),
"azurerm_lb_nat_rule": resourceArmLoadBalancerNatRule(),

View File

@ -0,0 +1,240 @@
package azurerm
import (
"bytes"
"log"
"strings"
"fmt"
"github.com/Azure/azure-sdk-for-go/arm/network"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
)
func resourceArmExpressRouteCircuit() *schema.Resource {
return &schema.Resource{
Create: resourceArmExpressRouteCircuitCreateOrUpdate,
Read: resourceArmExpressRouteCircuitRead,
Update: resourceArmExpressRouteCircuitCreateOrUpdate,
Delete: resourceArmExpressRouteCircuitDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"resource_group_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"location": locationSchema(),
"service_provider_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
DiffSuppressFunc: ignoreCaseDiffSuppressFunc,
},
"peering_location": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
DiffSuppressFunc: ignoreCaseDiffSuppressFunc,
},
"bandwidth_in_mbps": {
Type: schema.TypeInt,
Required: true,
},
"sku": {
Type: schema.TypeSet,
Required: true,
MinItems: 1,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"tier": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{
string(network.ExpressRouteCircuitSkuTierStandard),
string(network.ExpressRouteCircuitSkuTierPremium),
}, true),
DiffSuppressFunc: ignoreCaseDiffSuppressFunc,
},
"family": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{
string(network.MeteredData),
string(network.UnlimitedData),
}, true),
DiffSuppressFunc: ignoreCaseDiffSuppressFunc,
},
},
},
Set: resourceArmExpressRouteCircuitSkuHash,
},
"allow_classic_operations": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"service_provider_provisioning_state": {
Type: schema.TypeString,
Computed: true,
},
"service_key": {
Type: schema.TypeString,
Computed: true,
},
"tags": tagsSchema(),
},
}
}
func resourceArmExpressRouteCircuitCreateOrUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ArmClient)
ercClient := client.expressRouteCircuitClient
log.Printf("[INFO] preparing arguments for Azure ARM ExpressRouteCircuit creation.")
name := d.Get("name").(string)
resGroup := d.Get("resource_group_name").(string)
location := d.Get("location").(string)
serviceProviderName := d.Get("service_provider_name").(string)
peeringLocation := d.Get("peering_location").(string)
bandwidthInMbps := int32(d.Get("bandwidth_in_mbps").(int))
sku := expandExpressRouteCircuitSku(d)
allowRdfeOps := d.Get("allow_classic_operations").(bool)
tags := d.Get("tags").(map[string]interface{})
expandedTags := expandTags(tags)
erc := network.ExpressRouteCircuit{
Name: &name,
Location: &location,
Sku: sku,
ExpressRouteCircuitPropertiesFormat: &network.ExpressRouteCircuitPropertiesFormat{
AllowClassicOperations: &allowRdfeOps,
ServiceProviderProperties: &network.ExpressRouteCircuitServiceProviderProperties{
ServiceProviderName: &serviceProviderName,
PeeringLocation: &peeringLocation,
BandwidthInMbps: &bandwidthInMbps,
},
},
Tags: expandedTags,
}
_, err := ercClient.CreateOrUpdate(resGroup, name, erc, make(chan struct{}))
if err != nil {
return errwrap.Wrapf("Error Creating/Updating ExpressRouteCircuit {{err}}", err)
}
read, err := ercClient.Get(resGroup, name)
if err != nil {
return errwrap.Wrapf("Error Getting ExpressRouteCircuit {{err}}", err)
}
if read.ID == nil {
return fmt.Errorf("Cannot read ExpressRouteCircuit %s (resource group %s) ID", name, resGroup)
}
d.SetId(*read.ID)
return resourceArmExpressRouteCircuitRead(d, meta)
}
func resourceArmExpressRouteCircuitRead(d *schema.ResourceData, meta interface{}) error {
erc, resGroup, err := retrieveErcByResourceId(d.Id(), meta)
if err != nil {
return err
}
if erc == nil {
d.SetId("")
log.Printf("[INFO] Express Route Circuit %q not found. Removing from state", d.Get("name").(string))
return nil
}
d.Set("name", erc.Name)
d.Set("resource_group_name", resGroup)
d.Set("location", erc.Location)
if erc.ServiceProviderProperties != nil {
d.Set("service_provider_name", erc.ServiceProviderProperties.ServiceProviderName)
d.Set("peering_location", erc.ServiceProviderProperties.PeeringLocation)
d.Set("bandwidth_in_mbps", erc.ServiceProviderProperties.BandwidthInMbps)
}
if erc.Sku != nil {
d.Set("sku", schema.NewSet(resourceArmExpressRouteCircuitSkuHash, flattenExpressRouteCircuitSku(erc.Sku)))
}
d.Set("service_provider_provisioning_state", string(erc.ServiceProviderProvisioningState))
d.Set("service_key", erc.ServiceKey)
d.Set("allow_classic_operations", erc.AllowClassicOperations)
flattenAndSetTags(d, erc.Tags)
return nil
}
func resourceArmExpressRouteCircuitDelete(d *schema.ResourceData, meta interface{}) error {
ercClient := meta.(*ArmClient).expressRouteCircuitClient
resGroup, name, err := extractResourceGroupAndErcName(d.Id())
if err != nil {
return errwrap.Wrapf("Error Parsing Azure Resource ID {{err}}", err)
}
_, err = ercClient.Delete(resGroup, name, make(chan struct{}))
return err
}
func expandExpressRouteCircuitSku(d *schema.ResourceData) *network.ExpressRouteCircuitSku {
skuSettings := d.Get("sku").(*schema.Set)
v := skuSettings.List()[0].(map[string]interface{}) // [0] is guarded by MinItems in schema.
tier := v["tier"].(string)
family := v["family"].(string)
name := fmt.Sprintf("%s_%s", tier, family)
return &network.ExpressRouteCircuitSku{
Name: &name,
Tier: network.ExpressRouteCircuitSkuTier(tier),
Family: network.ExpressRouteCircuitSkuFamily(family),
}
}
func flattenExpressRouteCircuitSku(sku *network.ExpressRouteCircuitSku) []interface{} {
return []interface{}{
map[string]interface{}{
"tier": string(sku.Tier),
"family": string(sku.Family),
},
}
}
func resourceArmExpressRouteCircuitSkuHash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", strings.ToLower(m["tier"].(string))))
buf.WriteString(fmt.Sprintf("%s-", strings.ToLower(m["family"].(string))))
return hashcode.String(buf.String())
}

View File

@ -0,0 +1,113 @@
package azurerm
import (
"fmt"
"net/http"
"testing"
"github.com/Azure/azure-sdk-for-go/arm/network"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAzureRMExpressRouteCircuit_basic(t *testing.T) {
var erc network.ExpressRouteCircuit
ri := acctest.RandInt()
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testCheckAzureRMExpressRouteCircuitDestroy,
Steps: []resource.TestStep{
{
Config: testAccAzureRMExpressRouteCircuit_basic(ri),
Check: resource.ComposeTestCheckFunc(
testCheckAzureRMExpressRouteCircuitExists("azurerm_express_route_circuit.test", &erc),
),
},
},
})
}
func testCheckAzureRMExpressRouteCircuitExists(name string, erc *network.ExpressRouteCircuit) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("Not found: %s", name)
}
expressRouteCircuitName := rs.Primary.Attributes["name"]
resourceGroup, hasResourceGroup := rs.Primary.Attributes["resource_group_name"]
if !hasResourceGroup {
return fmt.Errorf("Bad: no resource group found in state for Express Route Circuit: %s", expressRouteCircuitName)
}
conn := testAccProvider.Meta().(*ArmClient).expressRouteCircuitClient
resp, err := conn.Get(resourceGroup, expressRouteCircuitName)
if err != nil {
if resp.StatusCode == http.StatusNotFound {
return fmt.Errorf("Bad: Express Route Circuit %q (resource group: %q) does not exist", expressRouteCircuitName, resourceGroup)
}
return fmt.Errorf("Bad: Get on expressRouteCircuitClient: %s", err)
}
*erc = resp
return nil
}
}
func testCheckAzureRMExpressRouteCircuitDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*ArmClient).expressRouteCircuitClient
for _, rs := range s.RootModule().Resources {
if rs.Type != "azurerm_express_route_circuit" {
continue
}
name := rs.Primary.Attributes["name"]
resourceGroup := rs.Primary.Attributes["resource_group_name"]
resp, err := conn.Get(resourceGroup, name)
if err != nil {
return nil
}
if resp.StatusCode != http.StatusNotFound {
return fmt.Errorf("Express Route Circuit still exists:\n%#v", resp.ExpressRouteCircuitPropertiesFormat)
}
}
return nil
}
func testAccAzureRMExpressRouteCircuit_basic(rInt int) string {
return fmt.Sprintf(`
resource "azurerm_resource_group" "test" {
name = "acctestrg-%d"
location = "West US"
}
resource "azurerm_express_route_circuit" "test" {
name = "acctest-erc-%[1]d"
location = "West US"
resource_group_name = "${azurerm_resource_group.test.name}"
service_provider_name = "Equinix"
peering_location = "Silicon Valley"
bandwidth_in_mbps = 50
sku {
tier = "Standard"
family = "MeteredData"
}
allow_classic_operations = false
tags {
Environment = "production"
Purpose = "AcceptanceTests"
}
}`, rInt)
}

View File

@ -111,6 +111,7 @@ To make a resource importable, please see the
### Azure (Resource Manager)
* azurerm_availability_set
* azurerm_express_route_circuit
* azurerm_dns_zone
* azurerm_local_network_gateway
* azurerm_network_security_group

View File

@ -0,0 +1,89 @@
---
layout: "azurerm"
page_title: "Azure Resource Manager: azurerm_express_route_circuit"
sidebar_current: "docs-azurerm-resource-express-route-circuit"
description: |-
Creates an ExpressRoute circuit.
---
# azurerm\_express\_route\_circuit
Creates an ExpressRoute circuit.
## Example Usage
```hcl
resource "azurerm_resource_group" "test" {
name = "exprtTest"
location = "West US"
}
resource "azurerm_express_route_circuit" "test" {
name = "expressRoute1"
resource_group_name = "${azurerm_resource_group.test.name}"
location = "West US"
service_provider_name = "Equinix"
peering_location = "Silicon Valley"
bandwidth_in_mbps = 50
sku {
tier = "Standard"
family = "MeteredData"
}
allow_classic_operations = false
tags {
environment = "Production"
}
}
```
## Argument Reference
The following arguments are supported:
* `name` - (Required) The name of the ExpressRoute circuit. Changing this forces a
new resource to be created.
* `resource_group_name` - (Required) The name of the resource group in which to
create the namespace. Changing this forces a new resource to be created.
* `location` - (Required) Specifies the supported Azure location where the resource exists.
Changing this forces a new resource to be created.
* `service_provider_name` - (Required) The name of the ExpressRoute Service Provider.
* `peering_location` - (Required) The name of the peering location and not the ARM resource location.
* `bandwidth_in_mbps` - (Required) The bandwidth in Mbps of the circuit being created. Once you increase your bandwidth,
you will not be able to decrease it to its previous value.
* `sku` - (Required) Chosen SKU of ExpressRoute circuit as documented below.
* `allow_classic_operations` - (Optional) Allow the circuit to interact with classic (RDFE) resources.
The default value is false.
* `tags` - (Optional) A mapping of tags to assign to the resource.
`sku` supports the following:
* `tier` - (Required) The service tier. Value must be either "Premium" or "Standard".
* `family` - (Required) The billing mode. Value must be either "MeteredData" or "UnlimitedData".
Once you set the billing model to "UnlimitedData", you will not be able to switch to "MeteredData".
## Attributes Reference
The following attributes are exported:
* `id` - The Resource ID of the ExpressRoute circuit.
* `service_provider_provisioning_state` - The ExpressRoute circuit provisioning state from your chosen service provider.
Possible values are "NotProvisioned", "Provisioning", "Provisioned", and "Deprovisioning".
* `service_key` - The string needed by the service provider to provision the ExpressRoute circuit.
## Import
ExpressRoute circuits can be imported using the `resource id`, e.g.
```
terraform import azurerm_express_route_circuit.myExpressRoute /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/Microsoft.Network/expressRouteCircuits/myExpressRoute
```

View File

@ -218,6 +218,9 @@
<a href="/docs/providers/azurerm/r/traffic_manager_endpoint.html">azurerm_traffic_manager_endpoint</a>
</li>
<li<%= sidebar_current("docs-azurerm-resource-express-route-circuit") %>>
<a href="/docs/providers/azurerm/r/express_route_circuit.html">azurerm_express_route_circuit</a>
</li>
</ul>
</li>