From 6ca50dd81da228b5f85df9c1621503cb1076d0a7 Mon Sep 17 00:00:00 2001 From: Jay Wang Date: Sun, 14 May 2017 11:30:14 -0700 Subject: [PATCH] [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 --- builtin/providers/azurerm/config.go | 7 + .../azurerm/express_route_circuit.go | 40 +++ .../import_arm_express_route_circuit_test.go | 29 +++ builtin/providers/azurerm/provider.go | 2 + .../resource_arm_express_route_circuit.go | 240 ++++++++++++++++++ ...resource_arm_express_route_circuit_test.go | 113 +++++++++ .../source/docs/import/importability.html.md | 1 + .../r/express_route_circuit.html.markdown | 89 +++++++ website/source/layouts/azurerm.erb | 3 + 9 files changed, 524 insertions(+) create mode 100644 builtin/providers/azurerm/express_route_circuit.go create mode 100644 builtin/providers/azurerm/import_arm_express_route_circuit_test.go create mode 100644 builtin/providers/azurerm/resource_arm_express_route_circuit.go create mode 100644 builtin/providers/azurerm/resource_arm_express_route_circuit_test.go create mode 100644 website/source/docs/providers/azurerm/r/express_route_circuit.html.markdown diff --git a/builtin/providers/azurerm/config.go b/builtin/providers/azurerm/config.go index ff6e6c51a..80e2d5bb8 100644 --- a/builtin/providers/azurerm/config.go +++ b/builtin/providers/azurerm/config.go @@ -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 diff --git a/builtin/providers/azurerm/express_route_circuit.go b/builtin/providers/azurerm/express_route_circuit.go new file mode 100644 index 000000000..297b55f56 --- /dev/null +++ b/builtin/providers/azurerm/express_route_circuit.go @@ -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 +} diff --git a/builtin/providers/azurerm/import_arm_express_route_circuit_test.go b/builtin/providers/azurerm/import_arm_express_route_circuit_test.go new file mode 100644 index 000000000..3e887c44f --- /dev/null +++ b/builtin/providers/azurerm/import_arm_express_route_circuit_test.go @@ -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, + }, + }, + }) +} diff --git a/builtin/providers/azurerm/provider.go b/builtin/providers/azurerm/provider.go index e7875650e..220b34d99 100644 --- a/builtin/providers/azurerm/provider.go +++ b/builtin/providers/azurerm/provider.go @@ -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(), diff --git a/builtin/providers/azurerm/resource_arm_express_route_circuit.go b/builtin/providers/azurerm/resource_arm_express_route_circuit.go new file mode 100644 index 000000000..3f17fff79 --- /dev/null +++ b/builtin/providers/azurerm/resource_arm_express_route_circuit.go @@ -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()) +} diff --git a/builtin/providers/azurerm/resource_arm_express_route_circuit_test.go b/builtin/providers/azurerm/resource_arm_express_route_circuit_test.go new file mode 100644 index 000000000..24c6c7fae --- /dev/null +++ b/builtin/providers/azurerm/resource_arm_express_route_circuit_test.go @@ -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) +} diff --git a/website/source/docs/import/importability.html.md b/website/source/docs/import/importability.html.md index 628881886..1adb0134c 100644 --- a/website/source/docs/import/importability.html.md +++ b/website/source/docs/import/importability.html.md @@ -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 diff --git a/website/source/docs/providers/azurerm/r/express_route_circuit.html.markdown b/website/source/docs/providers/azurerm/r/express_route_circuit.html.markdown new file mode 100644 index 000000000..fb9a159c2 --- /dev/null +++ b/website/source/docs/providers/azurerm/r/express_route_circuit.html.markdown @@ -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 +``` diff --git a/website/source/layouts/azurerm.erb b/website/source/layouts/azurerm.erb index 80b38a275..3c8a1b6db 100644 --- a/website/source/layouts/azurerm.erb +++ b/website/source/layouts/azurerm.erb @@ -218,6 +218,9 @@ azurerm_traffic_manager_endpoint + > + azurerm_express_route_circuit +