diff --git a/azurerm/data_source_netapp_volume.go b/azurerm/data_source_netapp_volume.go new file mode 100644 index 000000000000..78b5f9e750da --- /dev/null +++ b/azurerm/data_source_netapp_volume.go @@ -0,0 +1,110 @@ +package azurerm + +import ( + "fmt" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + aznetapp "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/netapp" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func dataSourceArmNetAppVolume() *schema.Resource { + return &schema.Resource{ + Read: dataSourceArmNetAppVolumeRead, + + Timeouts: &schema.ResourceTimeout{ + Read: schema.DefaultTimeout(5 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: aznetapp.ValidateNetAppPoolName, + }, + + "resource_group_name": azure.SchemaResourceGroupNameForDataSource(), + + "location": azure.SchemaLocationForDataSource(), + + "account_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: aznetapp.ValidateNetAppAccountName, + }, + + "pool_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: aznetapp.ValidateNetAppPoolName, + }, + + "volume_path": { + Type: schema.TypeString, + Computed: true, + }, + + "service_level": { + Type: schema.TypeString, + Computed: true, + }, + + "subnet_id": { + Type: schema.TypeString, + Computed: true, + }, + + "storage_quota_in_gb": { + Type: schema.TypeInt, + Computed: true, + }, + }, + } +} + +func dataSourceArmNetAppVolumeRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).NetApp.VolumeClient + ctx, cancel := timeouts.ForRead(meta.(*ArmClient).StopContext, d) + defer cancel() + + name := d.Get("name").(string) + accountName := d.Get("account_name").(string) + poolName := d.Get("pool_name").(string) + resourceGroup := d.Get("resource_group_name").(string) + + resp, err := client.Get(ctx, resourceGroup, accountName, poolName, name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Error: NetApp Volume %q (Resource Group %q) was not found", name, resourceGroup) + } + return fmt.Errorf("Error reading NetApp Volume %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + if resp.ID == nil || *resp.ID == "" { + return fmt.Errorf("Error retrieving NetApp Volume %q (Resource Group %q): ID was nil or empty", name, resourceGroup) + } + + d.SetId(*resp.ID) + + d.Set("name", name) + d.Set("resource_group_name", resourceGroup) + d.Set("account_name", accountName) + d.Set("pool_name", poolName) + if location := resp.Location; location != nil { + d.Set("location", azure.NormalizeLocation(*location)) + } + if props := resp.VolumeProperties; props != nil { + d.Set("volume_path", props.CreationToken) + d.Set("service_level", props.ServiceLevel) + d.Set("subnet_id", props.SubnetID) + + if props.UsageThreshold != nil { + d.Set("storage_quota_in_gb", *props.UsageThreshold/1073741824) + } + } + + return nil +} diff --git a/azurerm/data_source_netapp_volume_test.go b/azurerm/data_source_netapp_volume_test.go new file mode 100644 index 000000000000..9ec1513c37c0 --- /dev/null +++ b/azurerm/data_source_netapp_volume_test.go @@ -0,0 +1,45 @@ +package azurerm + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" +) + +func TestAccDataSourceAzureRMNetAppVolume_basic(t *testing.T) { + dataSourceName := "data.azurerm_netapp_volume.test" + ri := tf.AccRandTimeInt() + location := testLocation() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceNetAppVolume_basic(ri, location), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(dataSourceName, "volume_path"), + resource.TestCheckResourceAttrSet(dataSourceName, "service_level"), + resource.TestCheckResourceAttrSet(dataSourceName, "subnet_id"), + resource.TestCheckResourceAttrSet(dataSourceName, "storage_quota_in_gb"), + ), + }, + }, + }) +} + +func testAccDataSourceNetAppVolume_basic(rInt int, location string) string { + config := testAccAzureRMNetAppVolume_basic(rInt, location) + return fmt.Sprintf(` +%s + +data "azurerm_netapp_volume" "test" { + resource_group_name = "${azurerm_netapp_volume.test.resource_group_name}" + account_name = "${azurerm_netapp_volume.test.account_name}" + pool_name = "${azurerm_netapp_volume.test.pool_name}" + name = "${azurerm_netapp_volume.test.name}" +} +`, config) +} diff --git a/azurerm/internal/services/netapp/client/client.go b/azurerm/internal/services/netapp/client/client.go index e2363bae45a1..f2141ff1d023 100644 --- a/azurerm/internal/services/netapp/client/client.go +++ b/azurerm/internal/services/netapp/client/client.go @@ -8,6 +8,7 @@ import ( type Client struct { AccountClient *netapp.AccountsClient PoolClient *netapp.PoolsClient + VolumeClient *netapp.VolumesClient } func NewClient(o *common.ClientOptions) *Client { @@ -17,8 +18,12 @@ func NewClient(o *common.ClientOptions) *Client { poolClient := netapp.NewPoolsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&poolClient.Client, o.ResourceManagerAuthorizer) + volumeClient := netapp.NewVolumesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&volumeClient.Client, o.ResourceManagerAuthorizer) + return &Client{ AccountClient: &accountClient, PoolClient: &poolClient, + VolumeClient: &volumeClient, } } diff --git a/azurerm/internal/services/netapp/validate.go b/azurerm/internal/services/netapp/validation.go similarity index 50% rename from azurerm/internal/services/netapp/validate.go rename to azurerm/internal/services/netapp/validation.go index 9b4847bea85f..24c7f3d78167 100644 --- a/azurerm/internal/services/netapp/validate.go +++ b/azurerm/internal/services/netapp/validation.go @@ -24,3 +24,23 @@ func ValidateNetAppPoolName(v interface{}, k string) (warnings []string, errors return warnings, errors } + +func ValidateNetAppVolumeName(v interface{}, k string) (warnings []string, errors []error) { + value := v.(string) + + if !regexp.MustCompile(`^[a-zA-Z][-_\da-zA-Z]{0,63}$`).MatchString(value) { + errors = append(errors, fmt.Errorf("%q must be between 1 and 64 characters in length and start with letters and contains only letters, numbers, underscore or hyphens.", k)) + } + + return warnings, errors +} + +func ValidateNetAppVolumeVolumePath(v interface{}, k string) (warnings []string, errors []error) { + value := v.(string) + + if !regexp.MustCompile(`^[a-zA-Z][-\da-zA-Z]{0,79}$`).MatchString(value) { + errors = append(errors, fmt.Errorf("%q must be between 1 and 80 characters in length and start with letters and contains only letters, numbers or hyphens.", k)) + } + + return warnings, errors +} diff --git a/azurerm/internal/services/netapp/validation_test.go b/azurerm/internal/services/netapp/validation_test.go new file mode 100644 index 000000000000..7b72f66062f6 --- /dev/null +++ b/azurerm/internal/services/netapp/validation_test.go @@ -0,0 +1,139 @@ +package netapp + +import "testing" + +func TestValidateNetAppVolumeName(t *testing.T) { + testData := []struct { + input string + expected bool + }{ + { + // empty + input: "", + expected: false, + }, + { + // basic example + input: "hello", + expected: true, + }, + { + // can't start with an underscore + input: "_hello", + expected: false, + }, + { + // can't end with a dash + input: "hello-", + expected: true, + }, + { + // can't contain an exclamation mark + input: "hello!", + expected: false, + }, + { + // dash in the middle + input: "malcolm-in-the-middle", + expected: true, + }, + { + // can't end with a period + input: "hello.", + expected: false, + }, + { + // 63 chars + input: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk", + expected: true, + }, + { + // 64 chars + input: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkj", + expected: true, + }, + { + // 65 chars + input: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkja", + expected: false, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q..", v.input) + + _, errors := ValidateNetAppVolumeName(v.input, "name") + actual := len(errors) == 0 + if v.expected != actual { + t.Fatalf("Expected %t but got %t", v.expected, actual) + } + } +} + +func TestValidateNetAppVolumeVolumePath(t *testing.T) { + testData := []struct { + input string + expected bool + }{ + { + // empty + input: "", + expected: false, + }, + { + // basic example + input: "hello", + expected: true, + }, + { + // can't start with an underscore + input: "_hello", + expected: false, + }, + { + // can't end with a dash + input: "hello-", + expected: true, + }, + { + // can't contain an exclamation mark + input: "hello!", + expected: false, + }, + { + // dash in the middle + input: "malcolm-in-the-middle", + expected: true, + }, + { + // can't end with a period + input: "hello.", + expected: false, + }, + { + // 79 chars + input: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijabcdefgheysudciac", + expected: true, + }, + { + // 80 chars + input: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkasbdjdssardwyupac", + expected: true, + }, + { + // 81 chars + input: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkjspoiuytrewqasdfac", + expected: false, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q..", v.input) + + _, errors := ValidateNetAppVolumeVolumePath(v.input, "volume_path") + actual := len(errors) == 0 + if v.expected != actual { + t.Fatalf("Expected %t but got %t", v.expected, actual) + } + } +} diff --git a/azurerm/provider.go b/azurerm/provider.go index 11ab5bb1d55d..c444036b397a 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -107,6 +107,7 @@ func Provider() terraform.ResourceProvider { "azurerm_nat_gateway": dataSourceArmNatGateway(), "azurerm_netapp_account": dataSourceArmNetAppAccount(), "azurerm_netapp_pool": dataSourceArmNetAppPool(), + "azurerm_netapp_volume": dataSourceArmNetAppVolume(), "azurerm_network_ddos_protection_plan": dataSourceNetworkDDoSProtectionPlan(), "azurerm_network_interface": dataSourceArmNetworkInterface(), "azurerm_network_security_group": dataSourceArmNetworkSecurityGroup(), @@ -387,6 +388,7 @@ func Provider() terraform.ResourceProvider { "azurerm_network_watcher": resourceArmNetworkWatcher(), "azurerm_netapp_account": resourceArmNetAppAccount(), "azurerm_netapp_pool": resourceArmNetAppPool(), + "azurerm_netapp_volume": resourceArmNetAppVolume(), "azurerm_notification_hub_authorization_rule": resourceArmNotificationHubAuthorizationRule(), "azurerm_notification_hub_namespace": resourceArmNotificationHubNamespace(), "azurerm_notification_hub": resourceArmNotificationHub(), diff --git a/azurerm/resource_arm_netapp_account_test.go b/azurerm/resource_arm_netapp_account_test.go index a4ca72cc47d6..7b18089e4594 100644 --- a/azurerm/resource_arm_netapp_account_test.go +++ b/azurerm/resource_arm_netapp_account_test.go @@ -227,7 +227,6 @@ resource "azurerm_netapp_account" "import" { location = "${azurerm_netapp_account.test.location}" resource_group_name = "${azurerm_netapp_account.test.name}" } -} `, testAccAzureRMNetAppAccount_basicConfig(rInt, location)) } diff --git a/azurerm/resource_arm_netapp_pool.go b/azurerm/resource_arm_netapp_pool.go index cebdac178c5f..83083b7019f2 100644 --- a/azurerm/resource_arm_netapp_pool.go +++ b/azurerm/resource_arm_netapp_pool.go @@ -52,6 +52,7 @@ func resourceArmNetAppPool() *schema.Resource { "account_name": { Type: schema.TypeString, Required: true, + ForceNew: true, ValidateFunc: aznetapp.ValidateNetAppAccountName, }, diff --git a/azurerm/resource_arm_netapp_pool_test.go b/azurerm/resource_arm_netapp_pool_test.go index 4501a775a87d..af00d4754625 100644 --- a/azurerm/resource_arm_netapp_pool_test.go +++ b/azurerm/resource_arm_netapp_pool_test.go @@ -200,7 +200,7 @@ resource "azurerm_netapp_pool" "test" { location = "${azurerm_resource_group.test.location}" resource_group_name = "${azurerm_resource_group.test.name}" service_level = "Premium" - size_in_tb = "4" + size_in_tb = 4 } `, rInt, location, rInt, rInt) } @@ -236,7 +236,7 @@ resource "azurerm_netapp_pool" "test" { location = "${azurerm_resource_group.test.location}" resource_group_name = "${azurerm_resource_group.test.name}" service_level = "Standard" - size_in_tb = "15" + size_in_tb = 15 } `, rInt, location, rInt, rInt) } diff --git a/azurerm/resource_arm_netapp_volume.go b/azurerm/resource_arm_netapp_volume.go new file mode 100644 index 000000000000..496eabc74308 --- /dev/null +++ b/azurerm/resource_arm_netapp_volume.go @@ -0,0 +1,390 @@ +package azurerm + +import ( + "context" + "fmt" + "log" + "strconv" + "strings" + "time" + + "github.com/Azure/azure-sdk-for-go/services/netapp/mgmt/2019-06-01/netapp" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features" + aznetapp "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/netapp" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmNetAppVolume() *schema.Resource { + return &schema.Resource{ + Create: resourceArmNetAppVolumeCreateUpdate, + Read: resourceArmNetAppVolumeRead, + Update: resourceArmNetAppVolumeCreateUpdate, + Delete: resourceArmNetAppVolumeDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(30 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: aznetapp.ValidateNetAppVolumeName, + }, + + "resource_group_name": azure.SchemaResourceGroupName(), + + "location": azure.SchemaLocation(), + + "account_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: aznetapp.ValidateNetAppAccountName, + }, + + "pool_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: aznetapp.ValidateNetAppPoolName, + }, + + "volume_path": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: aznetapp.ValidateNetAppVolumeVolumePath, + }, + + "service_level": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + string(netapp.Premium), + string(netapp.Standard), + string(netapp.Ultra), + }, false), + }, + + "subnet_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: azure.ValidateResourceID, + }, + + "storage_quota_in_gb": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(100, 4096), + }, + + "export_policy_rule": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 5, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "rule_index": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(1, 5), + }, + "allowed_clients": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validate.CIDR, + }, + }, + "cifs_enabled": { + Type: schema.TypeBool, + Required: true, + }, + "nfsv3_enabled": { + Type: schema.TypeBool, + Required: true, + }, + "nfsv4_enabled": { + Type: schema.TypeBool, + Required: true, + }, + "unix_read_only": { + Type: schema.TypeBool, + Optional: true, + }, + "unix_read_write": { + Type: schema.TypeBool, + Optional: true, + }, + }, + }, + }, + }, + } +} + +func resourceArmNetAppVolumeCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).NetApp.VolumeClient + ctx, cancel := timeouts.ForCreateUpdate(meta.(*ArmClient).StopContext, d) + defer cancel() + + name := d.Get("name").(string) + resourceGroup := d.Get("resource_group_name").(string) + accountName := d.Get("account_name").(string) + poolName := d.Get("pool_name").(string) + + if features.ShouldResourcesBeImported() && d.IsNewResource() { + existing, err := client.Get(ctx, resourceGroup, accountName, poolName, name) + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("Error checking for present of existing NetApp Volume %q (Resource Group %q): %+v", name, resourceGroup, err) + } + } + if existing.ID != nil && *existing.ID != "" { + return tf.ImportAsExistsError("azurerm_netapp_volume", *existing.ID) + } + } + + location := azure.NormalizeLocation(d.Get("location").(string)) + volumePath := d.Get("volume_path").(string) + serviceLevel := d.Get("service_level").(string) + subnetId := d.Get("subnet_id").(string) + storageQuotaInGB := int64(d.Get("storage_quota_in_gb").(int) * 1073741824) + exportPolicyRule := d.Get("export_policy_rule").(*schema.Set).List() + + parameters := netapp.Volume{ + Location: utils.String(location), + VolumeProperties: &netapp.VolumeProperties{ + CreationToken: utils.String(volumePath), + ServiceLevel: netapp.ServiceLevel(serviceLevel), + SubnetID: utils.String(subnetId), + UsageThreshold: utils.Int64(storageQuotaInGB), + ExportPolicy: expandArmNetAppVolumeExportPolicyRule(exportPolicyRule), + }, + } + + future, err := client.CreateOrUpdate(ctx, parameters, resourceGroup, accountName, poolName, name) + if err != nil { + return fmt.Errorf("Error creating NetApp Volume %q (Resource Group %q): %+v", name, resourceGroup, err) + } + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("Error waiting for creation of NetApp Volume %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + resp, err := client.Get(ctx, resourceGroup, accountName, poolName, name) + if err != nil { + return fmt.Errorf("Error retrieving NetApp Volume %q (Resource Group %q): %+v", name, resourceGroup, err) + } + if resp.ID == nil || *resp.ID == "" { + return fmt.Errorf("Cannot read NetApp Volume %q (Resource Group %q) ID", name, resourceGroup) + } + d.SetId(*resp.ID) + + return resourceArmNetAppVolumeRead(d, meta) +} + +func resourceArmNetAppVolumeRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).NetApp.VolumeClient + ctx, cancel := timeouts.ForRead(meta.(*ArmClient).StopContext, d) + defer cancel() + + id, err := azure.ParseAzureResourceID(d.Id()) + if err != nil { + return err + } + resourceGroup := id.ResourceGroup + accountName := id.Path["netAppAccounts"] + poolName := id.Path["capacityPools"] + name := id.Path["volumes"] + + resp, err := client.Get(ctx, resourceGroup, accountName, poolName, name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[INFO] NetApp Volumes %q does not exist - removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("Error reading NetApp Volumes %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + d.Set("name", name) + d.Set("resource_group_name", resourceGroup) + d.Set("account_name", accountName) + d.Set("pool_name", poolName) + if location := resp.Location; location != nil { + d.Set("location", azure.NormalizeLocation(*location)) + } + if props := resp.VolumeProperties; props != nil { + d.Set("volume_path", props.CreationToken) + d.Set("service_level", props.ServiceLevel) + d.Set("subnet_id", props.SubnetID) + + if props.UsageThreshold != nil { + d.Set("storage_quota_in_gb", *props.UsageThreshold/1073741824) + } + if err := d.Set("export_policy_rule", flattenArmNetAppVolumeExportPolicyRule(props.ExportPolicy)); err != nil { + return fmt.Errorf("Error setting `export_policy_rule`: %+v", err) + } + } + + return nil +} + +func resourceArmNetAppVolumeDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).NetApp.VolumeClient + ctx, cancel := timeouts.ForDelete(meta.(*ArmClient).StopContext, d) + defer cancel() + + id, err := azure.ParseAzureResourceID(d.Id()) + if err != nil { + return err + } + resourceGroup := id.ResourceGroup + accountName := id.Path["netAppAccounts"] + poolName := id.Path["capacityPools"] + name := id.Path["volumes"] + + if _, err = client.Delete(ctx, resourceGroup, accountName, poolName, name); err != nil { + return fmt.Errorf("Error deleting NetApp Volume %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + return waitForNetAppVolumeToBeDeleted(ctx, client, resourceGroup, accountName, poolName, name) +} + +func waitForNetAppVolumeToBeDeleted(ctx context.Context, client *netapp.VolumesClient, resourceGroup, accountName, poolName, name string) error { + // The resource NetApp Volume depends on the resource NetApp Pool. + // Although the delete API returns 404 which means the NetApp Volume resource has been deleted. + // Then it tries to immediately delete NetApp Pool but it still throws error `Can not delete resource before nested resources are deleted.` + // In this case we're going to try triggering the Deletion again, in-case it didn't work prior to this attempt. + // For more details, see related Bug: https://github.com/Azure/azure-sdk-for-go/issues/6485 + + log.Printf("[DEBUG] Waiting for NetApp Volume Provisioning Service %q (Resource Group %q) to be deleted", name, resourceGroup) + stateConf := &resource.StateChangeConf{ + Pending: []string{"200", "202"}, + Target: []string{"404"}, + Refresh: netappVolumeDeleteStateRefreshFunc(ctx, client, resourceGroup, accountName, poolName, name), + Timeout: 20 * time.Minute, + } + if _, err := stateConf.WaitForState(); err != nil { + return fmt.Errorf("Error waiting for NetApp Volume Provisioning Service %q (Resource Group %q) to be deleted: %+v", name, resourceGroup, err) + } + + return nil +} + +func netappVolumeDeleteStateRefreshFunc(ctx context.Context, client *netapp.VolumesClient, resourceGroupName string, accountName string, poolName string, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + res, err := client.Get(ctx, resourceGroupName, accountName, poolName, name) + if err != nil { + if !utils.ResponseWasNotFound(res.Response) { + return nil, "", fmt.Errorf("Error retrieving NetApp Volume %q (Resource Group %q): %s", name, resourceGroupName, err) + } + } + + if _, err := client.Delete(ctx, resourceGroupName, accountName, poolName, name); err != nil { + log.Printf("Error reissuing NetApp Volume %q delete request (Resource Group %q): %+v", name, resourceGroupName, err) + } + + return res, strconv.Itoa(res.StatusCode), nil + } +} + +func expandArmNetAppVolumeExportPolicyRule(input []interface{}) *netapp.VolumePropertiesExportPolicy { + results := make([]netapp.ExportPolicyRule, 0) + for _, item := range input { + if item != nil { + v := item.(map[string]interface{}) + ruleIndex := int32(v["rule_index"].(int)) + allowedClients := strings.Join(*utils.ExpandStringSlice(v["allowed_clients"].(*schema.Set).List()), ",") + cifsEnabled := v["cifs_enabled"].(bool) + nfsv3Enabled := v["nfsv3_enabled"].(bool) + nfsv4Enabled := v["nfsv4_enabled"].(bool) + unixReadOnly := v["unix_read_only"].(bool) + unixReadWrite := v["unix_read_write"].(bool) + + result := netapp.ExportPolicyRule{ + AllowedClients: utils.String(allowedClients), + Cifs: utils.Bool(cifsEnabled), + Nfsv3: utils.Bool(nfsv3Enabled), + Nfsv4: utils.Bool(nfsv4Enabled), + RuleIndex: utils.Int32(ruleIndex), + UnixReadOnly: utils.Bool(unixReadOnly), + UnixReadWrite: utils.Bool(unixReadWrite), + } + + results = append(results, result) + } + } + + return &netapp.VolumePropertiesExportPolicy{ + Rules: &results, + } +} + +func flattenArmNetAppVolumeExportPolicyRule(input *netapp.VolumePropertiesExportPolicy) []interface{} { + results := make([]interface{}, 0) + if input == nil || input.Rules == nil { + return results + } + + for _, item := range *input.Rules { + ruleIndex := int32(0) + if v := item.RuleIndex; v != nil { + ruleIndex = *v + } + allowedClients := []string{} + if v := item.AllowedClients; v != nil { + allowedClients = strings.Split(*v, ",") + } + cifsEnabled := false + if v := item.Cifs; v != nil { + cifsEnabled = *v + } + nfsv3Enabled := false + if v := item.Nfsv3; v != nil { + nfsv3Enabled = *v + } + nfsv4Enabled := false + if v := item.Nfsv4; v != nil { + nfsv4Enabled = *v + } + unixReadOnly := false + if v := item.UnixReadOnly; v != nil { + unixReadOnly = *v + } + unixReadWrite := false + if v := item.UnixReadWrite; v != nil { + unixReadWrite = *v + } + + results = append(results, map[string]interface{}{ + "rule_index": ruleIndex, + "allowed_clients": utils.FlattenStringSlice(&allowedClients), + "cifs_enabled": cifsEnabled, + "nfsv3_enabled": nfsv3Enabled, + "nfsv4_enabled": nfsv4Enabled, + "unix_read_only": unixReadOnly, + "unix_read_write": unixReadWrite, + }) + } + + return results +} diff --git a/azurerm/resource_arm_netapp_volume_test.go b/azurerm/resource_arm_netapp_volume_test.go new file mode 100644 index 000000000000..76457790726b --- /dev/null +++ b/azurerm/resource_arm_netapp_volume_test.go @@ -0,0 +1,473 @@ +package azurerm + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMNetAppVolume_basic(t *testing.T) { + resourceName := "azurerm_netapp_volume.test" + ri := tf.AccRandTimeInt() + location := testLocation() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMNetAppVolumeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMNetAppVolume_basic(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMNetAppVolumeExists(resourceName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureRMNetAppVolume_requiresImport(t *testing.T) { + if !features.ShouldResourcesBeImported() { + t.Skip("Skipping since resources aren't required to be imported") + return + } + + resourceName := "azurerm_netapp_volume.test" + ri := tf.AccRandTimeInt() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMNetAppVolumeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMNetAppVolume_basic(ri, testLocation()), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMNetAppVolumeExists(resourceName), + ), + }, + { + Config: testAccAzureRMNetAppVolume_requiresImport(ri, testLocation()), + ExpectError: testRequiresImportError("azurerm_netapp_volume"), + }, + }, + }) +} + +func TestAccAzureRMNetAppVolume_complete(t *testing.T) { + resourceName := "azurerm_netapp_volume.test" + ri := tf.AccRandTimeInt() + location := testLocation() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMNetAppVolumeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMNetAppVolume_complete(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMNetAppVolumeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "service_level", "Premium"), + resource.TestCheckResourceAttr(resourceName, "storage_quota_in_gb", "101"), + resource.TestCheckResourceAttr(resourceName, "export_policy_rule.#", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureRMNetAppVolume_update(t *testing.T) { + resourceName := "azurerm_netapp_volume.test" + ri := tf.AccRandTimeInt() + location := testLocation() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMNetAppVolumeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMNetAppVolume_basic(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMNetAppVolumeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "storage_quota_in_gb", "100"), + resource.TestCheckResourceAttr(resourceName, "export_policy_rule.#", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAzureRMNetAppVolume_complete(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMNetAppVolumeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "storage_quota_in_gb", "101"), + resource.TestCheckResourceAttr(resourceName, "export_policy_rule.#", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAzureRMNetAppVolume_basic(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMNetAppVolumeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "storage_quota_in_gb", "100"), + resource.TestCheckResourceAttr(resourceName, "export_policy_rule.#", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureRMNetAppVolume_updateSubnet(t *testing.T) { + resourceName := "azurerm_netapp_volume.test" + ri := tf.AccRandTimeInt() + location := testLocation() + resourceGroupName := fmt.Sprintf("acctestRG-netapp-%d", ri) + oldVNetName := fmt.Sprintf("acctest-VirtualNetwork-%d", ri) + oldSubnetName := fmt.Sprintf("acctest-Subnet-%d", ri) + newVNetName := fmt.Sprintf("acctest-updated-VirtualNetwork-%d", ri) + newSubnetName := fmt.Sprintf("acctest-updated-Subnet-%d", ri) + uriTemplate := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/virtualNetworks/%s/subnets/%s" + + subscriptionID := os.Getenv("ARM_SUBSCRIPTION_ID") + oldSubnetId := fmt.Sprintf(uriTemplate, subscriptionID, resourceGroupName, oldVNetName, oldSubnetName) + newSubnetId := fmt.Sprintf(uriTemplate, subscriptionID, resourceGroupName, newVNetName, newSubnetName) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMNetAppVolumeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMNetAppVolume_basic(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMNetAppVolumeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "subnet_id", oldSubnetId), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAzureRMNetAppVolume_updateSubnet(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMNetAppVolumeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "subnet_id", newSubnetId), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureRMNetAppVolume_updateExportPolicyRule(t *testing.T) { + resourceName := "azurerm_netapp_volume.test" + ri := tf.AccRandTimeInt() + location := testLocation() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMNetAppVolumeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMNetAppVolume_complete(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMNetAppVolumeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "export_policy_rule.#", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAzureRMNetAppVolume_updateExportPolicyRule(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMNetAppVolumeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "export_policy_rule.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testCheckAzureRMNetAppVolumeExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("NetApp Volume not found: %s", resourceName) + } + + name := rs.Primary.Attributes["name"] + accountName := rs.Primary.Attributes["account_name"] + poolName := rs.Primary.Attributes["pool_name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + client := testAccProvider.Meta().(*ArmClient).NetApp.VolumeClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + if resp, err := client.Get(ctx, resourceGroup, accountName, poolName, name); err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: NetApp Volume %q (Resource Group %q) does not exist", name, resourceGroup) + } + return fmt.Errorf("Bad: Get on netapp.VolumeClient: %+v", err) + } + + return nil + } +} + +func testCheckAzureRMNetAppVolumeDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*ArmClient).NetApp.VolumeClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_netapp_volume" { + continue + } + + name := rs.Primary.Attributes["name"] + accountName := rs.Primary.Attributes["account_name"] + poolName := rs.Primary.Attributes["pool_name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + if resp, err := client.Get(ctx, resourceGroup, accountName, poolName, name); err != nil { + if !utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: Get on netapp.VolumeClient: %+v", err) + } + } + + return nil + } + + return nil +} + +func testAccAzureRMNetAppVolume_basic(rInt int, location string) string { + template := testAccAzureRMNetAppVolume_template(rInt, location) + return fmt.Sprintf(` +%s + +resource "azurerm_netapp_volume" "test" { + name = "acctest-NetAppVolume-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + account_name = "${azurerm_netapp_account.test.name}" + pool_name = "${azurerm_netapp_pool.test.name}" + volume_path = "my-unique-file-path-%d" + service_level = "Premium" + subnet_id = "${azurerm_subnet.test.id}" + storage_quota_in_gb = 100 +} +`, template, rInt, rInt) +} + +func testAccAzureRMNetAppVolume_requiresImport(rInt int, location string) string { + return fmt.Sprintf(` +%s + +resource "azurerm_netapp_volume" "import" { + name = "${azurerm_netapp_volume.test.name}" + location = "${azurerm_netapp_volume.test.location}" + resource_group_name = "${azurerm_netapp_volume.test.name}" +} +`, testAccAzureRMNetAppVolume_basic(rInt, location)) +} + +func testAccAzureRMNetAppVolume_complete(rInt int, location string) string { + template := testAccAzureRMNetAppVolume_template(rInt, location) + return fmt.Sprintf(` +%s + +resource "azurerm_netapp_volume" "test" { + name = "acctest-NetAppVolume-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + account_name = "${azurerm_netapp_account.test.name}" + pool_name = "${azurerm_netapp_pool.test.name}" + service_level = "Premium" + volume_path = "my-unique-file-path-%d" + subnet_id = "${azurerm_subnet.test.id}" + storage_quota_in_gb = 101 + + export_policy_rule { + rule_index = 1 + allowed_clients = ["1.2.3.0/24"] + cifs_enabled = false + nfsv3_enabled = true + nfsv4_enabled = false + unix_read_only = false + unix_read_write = true + } + + export_policy_rule { + rule_index = 2 + allowed_clients = ["1.2.5.0"] + cifs_enabled = false + nfsv3_enabled = true + nfsv4_enabled = false + unix_read_only = true + unix_read_write = false + } +} +`, template, rInt, rInt) +} + +func testAccAzureRMNetAppVolume_updateSubnet(rInt int, location string) string { + template := testAccAzureRMNetAppVolume_template(rInt, location) + return fmt.Sprintf(` +%s + +resource "azurerm_virtual_network" "updated" { + name = "acctest-updated-VirtualNetwork-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + address_space = ["10.1.0.0/16"] +} + +resource "azurerm_subnet" "updated" { + name = "acctest-updated-Subnet-%d" + resource_group_name = "${azurerm_resource_group.test.name}" + virtual_network_name = "${azurerm_virtual_network.updated.name}" + address_prefix = "10.1.3.0/24" + + delegation { + name = "testdelegation2" + + service_delegation { + name = "Microsoft.Netapp/volumes" + actions = ["Microsoft.Network/networkinterfaces/*", "Microsoft.Network/virtualNetworks/subnets/join/action"] + } + } +} + +resource "azurerm_netapp_volume" "test" { + name = "acctest-updated-NetAppVolume-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + account_name = "${azurerm_netapp_account.test.name}" + pool_name = "${azurerm_netapp_pool.test.name}" + volume_path = "my-updated-unique-file-path-%d" + service_level = "Premium" + subnet_id = "${azurerm_subnet.updated.id}" + storage_quota_in_gb = 100 +} +`, template, rInt, rInt, rInt, rInt) +} + +func testAccAzureRMNetAppVolume_updateExportPolicyRule(rInt int, location string) string { + template := testAccAzureRMNetAppVolume_template(rInt, location) + return fmt.Sprintf(` +%s + +resource "azurerm_netapp_volume" "test" { + name = "acctest-NetAppVolume-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + account_name = "${azurerm_netapp_account.test.name}" + pool_name = "${azurerm_netapp_pool.test.name}" + service_level = "Premium" + volume_path = "my-unique-file-path-%d" + subnet_id = "${azurerm_subnet.test.id}" + storage_quota_in_gb = 101 + + export_policy_rule { + rule_index = 1 + allowed_clients = ["1.2.4.0/24", "1.3.4.0"] + cifs_enabled = false + nfsv3_enabled = true + nfsv4_enabled = false + unix_read_only = false + unix_read_write = true + } +} +`, template, rInt, rInt) +} + +func testAccAzureRMNetAppVolume_template(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-netapp-%d" + location = "%s" +} + +resource "azurerm_virtual_network" "test" { + name = "acctest-VirtualNetwork-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + address_space = ["10.0.0.0/16"] +} + +resource "azurerm_subnet" "test" { + name = "acctest-Subnet-%d" + resource_group_name = "${azurerm_resource_group.test.name}" + virtual_network_name = "${azurerm_virtual_network.test.name}" + address_prefix = "10.0.2.0/24" + + delegation { + name = "testdelegation" + + service_delegation { + name = "Microsoft.Netapp/volumes" + actions = ["Microsoft.Network/networkinterfaces/*", "Microsoft.Network/virtualNetworks/subnets/join/action"] + } + } +} + +resource "azurerm_netapp_account" "test" { + name = "acctest-NetAppAccount-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" +} + +resource "azurerm_netapp_pool" "test" { + name = "acctest-NetAppPool-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + account_name = "${azurerm_netapp_account.test.name}" + service_level = "Premium" + size_in_tb = 4 +} +`, rInt, location, rInt, rInt, rInt, rInt) +} diff --git a/examples/netapp/pool/main.tf b/examples/netapp/pool/main.tf index 0871f61dc38c..a1c773af94d7 100644 --- a/examples/netapp/pool/main.tf +++ b/examples/netapp/pool/main.tf @@ -15,5 +15,5 @@ resource "azurerm_netapp_pool" "example" { location = "${azurerm_resource_group.example.location}" resource_group_name = "${azurerm_resource_group.example.name}" service_level = "Premium" - size_in_4_tb = "1" + size_in_tb = 4 } diff --git a/examples/netapp/volume/main.tf b/examples/netapp/volume/main.tf new file mode 100644 index 000000000000..7a4a1f54cec8 --- /dev/null +++ b/examples/netapp/volume/main.tf @@ -0,0 +1,54 @@ +resource "azurerm_resource_group" "example" { + name = "${var.prefix}-resources" + location = "${var.location}" +} + +resource "azurerm_virtual_network" "example" { + name = "${var.prefix}-virtualnetwork" + location = "${azurerm_resource_group.example.location}" + resource_group_name = "${azurerm_resource_group.example.name}" + address_space = ["10.0.0.0/16"] +} + +resource "azurerm_subnet" "example" { + name = "${var.prefix}-subnet" + resource_group_name = "${azurerm_resource_group.example.name}" + virtual_network_name = "${azurerm_virtual_network.example.name}" + address_prefix = "10.0.2.0/24" + + delegation { + name = "testdelegation" + + service_delegation { + name = "Microsoft.Netapp/volumes" + actions = ["Microsoft.Network/networkinterfaces/*", "Microsoft.Network/virtualNetworks/subnets/join/action"] + } + } +} + +resource "azurerm_netapp_account" "example" { + name = "${var.prefix}-netappaccount" + location = "${azurerm_resource_group.example.location}" + resource_group_name = "${azurerm_resource_group.example.name}" +} + +resource "azurerm_netapp_pool" "example" { + name = "${var.prefix}-netapppool" + location = "${azurerm_resource_group.example.location}" + resource_group_name = "${azurerm_resource_group.example.name}" + account_name = "${azurerm_netapp_account.example.name}" + service_level = "Premium" + size_in_tb = 4 +} + +resource "azurerm_netapp_volume" "example" { + name = "${var.prefix}-netappvolume" + location = "${azurerm_resource_group.example.location}" + resource_group_name = "${azurerm_resource_group.example.name}" + account_name = "${azurerm_netapp_account.example.name}" + pool_name = "${azurerm_netapp_pool.example.name}" + volume_path = "my-unique-file-path" + service_level = "Premium" + subnet_id = "${azurerm_subnet.example.id}" + storage_quota_in_gb = 100 +} diff --git a/examples/netapp/volume/variables.tf b/examples/netapp/volume/variables.tf new file mode 100644 index 000000000000..31dcff302353 --- /dev/null +++ b/examples/netapp/volume/variables.tf @@ -0,0 +1,7 @@ +variable "location" { + description = "The Azure location where all resources in this example should be created." +} + +variable "prefix" { + description = "The prefix used for all resources used by this NetApp Volume" +} diff --git a/website/azurerm.erb b/website/azurerm.erb index 78e9c40c704c..9d6aee2ca68f 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -306,6 +306,10 @@ azurerm_netapp_pool +