diff --git a/azurerm/internal/services/netapp/netapp_volume_resource.go b/azurerm/internal/services/netapp/netapp_volume_resource.go index 5dcd742f11c2..c4cdc2db28bc 100644 --- a/azurerm/internal/services/netapp/netapp_volume_resource.go +++ b/azurerm/internal/services/netapp/netapp_volume_resource.go @@ -92,6 +92,14 @@ func resourceNetAppVolume() *schema.Resource { ValidateFunc: azure.ValidateResourceID, }, + "create_from_snapshot_resource_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: azure.ValidateResourceID, + }, + "protocols": { Type: schema.TypeSet, ForceNew: true, @@ -281,6 +289,70 @@ func resourceNetAppVolumeCreateUpdate(d *schema.ResourceData, meta interface{}) volumeType = "DataProtection" } + // Handling volume creation from snapshot case + snapshotResourceID := d.Get("create_from_snapshot_resource_id").(string) + snapshotID := "" + if snapshotResourceID != "" { + // Get snapshot ID GUID value + parsedSnapshotResourceID, err := parse.SnapshotID(snapshotResourceID) + if err != nil { + return fmt.Errorf("Error parsing snapshotResourceID %q: %+v", snapshotResourceID, err) + } + + snapshotClient := meta.(*clients.Client).NetApp.SnapshotClient + snapshotResponse, err := snapshotClient.Get( + ctx, + parsedSnapshotResourceID.ResourceGroup, + parsedSnapshotResourceID.NetAppAccountName, + parsedSnapshotResourceID.CapacityPoolName, + parsedSnapshotResourceID.VolumeName, + parsedSnapshotResourceID.Name, + ) + if err != nil { + return fmt.Errorf("Error getting snapshot from NetApp Volume %q (Resource Group %q): %+v", parsedSnapshotResourceID.VolumeName, parsedSnapshotResourceID.ResourceGroup, err) + } + snapshotID = *snapshotResponse.SnapshotID + + // Validate if properties that cannot be changed matches (protocols, subnet_id, location, resource group, account_name, pool_name, service_level) + sourceVolume, err := client.Get( + ctx, + parsedSnapshotResourceID.ResourceGroup, + parsedSnapshotResourceID.NetAppAccountName, + parsedSnapshotResourceID.CapacityPoolName, + parsedSnapshotResourceID.VolumeName, + ) + if err != nil { + return fmt.Errorf("Error getting source NetApp Volume (snapshot's parent resource) %q (Resource Group %q): %+v", parsedSnapshotResourceID.VolumeName, parsedSnapshotResourceID.ResourceGroup, err) + } + + parsedVolumeID, _ := parse.VolumeID(*sourceVolume.ID) + propertyMismatch := []string{} + if !ValidateSlicesEquality(*sourceVolume.ProtocolTypes, *utils.ExpandStringSlice(protocols), false) { + propertyMismatch = append(propertyMismatch, "protocols") + } + if !strings.EqualFold(*sourceVolume.SubnetID, subnetID) { + propertyMismatch = append(propertyMismatch, "subnet_id") + } + if !strings.EqualFold(*sourceVolume.Location, location) { + propertyMismatch = append(propertyMismatch, "location") + } + if !strings.EqualFold(string(sourceVolume.ServiceLevel), serviceLevel) { + propertyMismatch = append(propertyMismatch, "service_level") + } + if !strings.EqualFold(parsedVolumeID.ResourceGroup, resourceGroup) { + propertyMismatch = append(propertyMismatch, "resource_group_name") + } + if !strings.EqualFold(parsedVolumeID.NetAppAccountName, accountName) { + propertyMismatch = append(propertyMismatch, "account_name") + } + if !strings.EqualFold(parsedVolumeID.CapacityPoolName, poolName) { + propertyMismatch = append(propertyMismatch, "pool_name") + } + if len(propertyMismatch) > 0 { + return fmt.Errorf("Following NetApp Volume properties on new Volume from Snapshot does not match Snapshot's source Volume %q (Resource Group %q): %+v", name, resourceGroup, propertyMismatch) + } + } + parameters := netapp.Volume{ Location: utils.String(location), VolumeProperties: &netapp.VolumeProperties{ @@ -291,6 +363,7 @@ func resourceNetAppVolumeCreateUpdate(d *schema.ResourceData, meta interface{}) UsageThreshold: utils.Int64(storageQuotaInGB), ExportPolicy: exportPolicyRule, VolumeType: utils.String(volumeType), + SnapshotID: utils.String(snapshotID), DataProtection: dataProtectionReplication, }, Tags: tags.Expand(d.Get("tags").(map[string]interface{})), diff --git a/azurerm/internal/services/netapp/netapp_volume_resource_test.go b/azurerm/internal/services/netapp/netapp_volume_resource_test.go index e71eaff1b9ed..28aa5cce33ed 100644 --- a/azurerm/internal/services/netapp/netapp_volume_resource_test.go +++ b/azurerm/internal/services/netapp/netapp_volume_resource_test.go @@ -67,6 +67,21 @@ func TestAccNetAppVolume_crossRegionReplication(t *testing.T) { }) } +func TestAccNetAppVolume_nfsv3FromSnapshot(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_netapp_volume", "test_snapshot_vol") + r := NetAppVolumeResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.nfsv3FromSnapshot(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("create_from_snapshot_resource_id"), + }) +} + func TestAccNetAppVolume_requiresImport(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_netapp_volume", "test") r := NetAppVolumeResource{} @@ -320,6 +335,64 @@ resource "azurerm_netapp_volume" "test_secondary" { `, template, data.RandomInteger, "northeurope") } +func (NetAppVolumeResource) nfsv3FromSnapshot(data acceptance.TestData) string { + template := NetAppVolumeResource{}.template(data) + return fmt.Sprintf(` +%[1]s + +resource "azurerm_netapp_volume" "test" { + name = "acctest-NetAppVolume-%[2]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-%[2]d" + service_level = "Standard" + subnet_id = azurerm_subnet.test.id + protocols = ["NFSv3"] + storage_quota_in_gb = 100 + + export_policy_rule { + rule_index = 1 + allowed_clients = ["1.2.3.0/24"] + protocols_enabled = ["NFSv3"] + unix_read_only = false + unix_read_write = true + } +} + +resource "azurerm_netapp_snapshot" "test" { + name = "acctest-Snapshot-%[2]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_name = azurerm_netapp_volume.test.name +} + +resource "azurerm_netapp_volume" "test_snapshot_vol" { + name = "acctest-NetAppVolume-NewFromSnapshot-%[2]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-snapshot-%[2]d" + service_level = "Standard" + subnet_id = azurerm_subnet.test.id + protocols = ["NFSv3"] + storage_quota_in_gb = 200 + create_from_snapshot_resource_id = azurerm_netapp_snapshot.test.id + + export_policy_rule { + rule_index = 1 + allowed_clients = ["0.0.0.0/0"] + protocols_enabled = ["NFSv3"] + unix_read_write = true + } +} +`, template, data.RandomInteger) +} + func (r NetAppVolumeResource) requiresImport(data acceptance.TestData) string { return fmt.Sprintf(` %s diff --git a/azurerm/internal/services/netapp/validation.go b/azurerm/internal/services/netapp/validation.go index 0f845c9165d0..71fb510d4de8 100644 --- a/azurerm/internal/services/netapp/validation.go +++ b/azurerm/internal/services/netapp/validation.go @@ -2,7 +2,9 @@ package netapp import ( "fmt" + "reflect" "regexp" + "strings" ) func ValidateNetAppAccountName(v interface{}, k string) (warnings []string, errors []error) { @@ -54,3 +56,52 @@ func ValidateNetAppSnapshotName(v interface{}, k string) (warnings []string, err return warnings, errors } + +func ValidateSlicesEquality(source, new []string, caseSensitive bool) bool { + // Fast path + if len(source) != len(new) { + return false + } + + if reflect.DeepEqual(source, new) { + return true + } + + // Slow path + // Source -> New direction + sourceNewValidatedCount := 0 + for _, sourceItem := range source { + for _, newItem := range new { + if caseSensitive { + if sourceItem == newItem { + sourceNewValidatedCount++ + } + } else { + if strings.EqualFold(sourceItem, newItem) { + sourceNewValidatedCount++ + } + } + } + } + + // New -> Source direction + newSourceValidatedCount := 0 + for _, newItem := range source { + for _, sourceItem := range new { + if caseSensitive { + if newItem == sourceItem { + newSourceValidatedCount++ + } + } else { + if strings.EqualFold(newItem, sourceItem) { + newSourceValidatedCount++ + } + } + } + } + + lengthValidation := sourceNewValidatedCount == len(source) && newSourceValidatedCount == len(source) && sourceNewValidatedCount == len(new) && newSourceValidatedCount == len(new) + countValidation := sourceNewValidatedCount == newSourceValidatedCount + + return lengthValidation && countValidation +} diff --git a/azurerm/internal/services/netapp/validation_test.go b/azurerm/internal/services/netapp/validation_test.go index 4359a04a159b..8edb069a4c58 100644 --- a/azurerm/internal/services/netapp/validation_test.go +++ b/azurerm/internal/services/netapp/validation_test.go @@ -205,3 +205,93 @@ func TestValidateNetAppSnapshotName(t *testing.T) { } } } + +func TestValidateSlicesEquality(t *testing.T) { + testData := []struct { + input1 []string + input2 []string + input3 bool + expected bool + }{ + { + // Same order, case sensitive + input1: []string{"CIFS", "NFSv3"}, + input2: []string{"CIFS", "NFSv3"}, + input3: true, + expected: true, + }, + { + // Same order, case insensitive + input1: []string{"CIFS", "NFSv3"}, + input2: []string{"cifs", "nfsv3"}, + input3: false, + expected: true, + }, + { + // Reversed order, case sensitive + input1: []string{"CIFS", "NFSv3"}, + input2: []string{"NFSv3", "CIFS"}, + input3: true, + expected: true, + }, + { + // Reversed order, case insensitive + input1: []string{"cifs", "nfsv3"}, + input2: []string{"NFSv3", "CIFS"}, + input3: false, + expected: true, + }, + + { + // Different, case sensitive + input1: []string{"CIFS", "NFSv3"}, + input2: []string{"NFSv3"}, + input3: true, + expected: false, + }, + { + // Different, case insensitive + input1: []string{"CIFS", "NFSv3"}, + input2: []string{"nfsv3"}, + input3: false, + expected: false, + }, + { + // Different, single slices, case sensitive + input1: []string{"CIFS"}, + input2: []string{"NFSv3"}, + input3: true, + expected: false, + }, + { + // Different, single slices, case insensitive + input1: []string{"cifs"}, + input2: []string{"NFSv3"}, + input3: false, + expected: false, + }, + { + // Same, single slices, case sensitive + input1: []string{"CIFS"}, + input2: []string{"CIFS"}, + input3: true, + expected: true, + }, + { + // Different, single slices, case insensitive + input1: []string{"cifs"}, + input2: []string{"CIFS"}, + input3: false, + expected: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %+v and %+v for %v where 'caseSensitive' = %v result..", v.input1, v.input2, v.expected, v.input3) + + actual := ValidateSlicesEquality(v.input1, v.input2, v.input3) + if v.expected != actual { + t.Fatalf("Expected %t but got %t", v.expected, actual) + } + } +} diff --git a/examples/netapp/volume_from_snapshot/README.md b/examples/netapp/volume_from_snapshot/README.md new file mode 100644 index 000000000000..db917c29d693 --- /dev/null +++ b/examples/netapp/volume_from_snapshot/README.md @@ -0,0 +1,5 @@ +## Example: NetApp Files Volume creation from Snapshot + +This example shows how to create an Azure NetApp Files Volume from a Snapshot. + +For more information, please refer to [How Azure NetApp Files snapshots work](https://docs.microsoft.com/en-us/azure/azure-netapp-files/snapshots-introduction). \ No newline at end of file diff --git a/examples/netapp/volume_from_snapshot/main.tf b/examples/netapp/volume_from_snapshot/main.tf new file mode 100644 index 000000000000..ec690886672c --- /dev/null +++ b/examples/netapp/volume_from_snapshot/main.tf @@ -0,0 +1,108 @@ +provider "azurerm" { + features {} +} + +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_prefixes = ["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 = "Standard" + size_in_tb = 4 +} + +resource "azurerm_netapp_volume" "example" { + lifecycle { + prevent_destroy = true + } + + 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 = "${var.prefix}-netappvolume" + service_level = "Standard" + protocols = ["NFSv3"] + subnet_id = azurerm_subnet.example.id + storage_quota_in_gb = 100 + + export_policy_rule { + rule_index = 1 + allowed_clients = ["0.0.0.0/0"] + protocols_enabled = ["NFSv3"] + unix_read_write = true + } +} + +resource "azurerm_netapp_snapshot" "example" { + lifecycle { + prevent_destroy = true + } + + name = "${var.prefix}-netappsnapshot" + 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_name = azurerm_netapp_volume.example.name +} + +resource "azurerm_netapp_volume" "example-snapshot" { + lifecycle { + prevent_destroy = true + } + + name = "${var.prefix}-netappvolume-snapshot" + 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 = "${var.prefix}-netappvolume-snapshot" + service_level = "Standard" + protocols = ["NFSv3"] + subnet_id = azurerm_subnet.example.id + storage_quota_in_gb = 100 + snapshot_resource_id = azurerm_netapp_snapshot.example.id + + export_policy_rule { + rule_index = 1 + allowed_clients = ["0.0.0.0/0"] + protocols_enabled = ["NFSv3"] + unix_read_write = true + } +} diff --git a/examples/netapp/volume_from_snapshot/variables.tf b/examples/netapp/volume_from_snapshot/variables.tf new file mode 100644 index 000000000000..d20d1b0ec47a --- /dev/null +++ b/examples/netapp/volume_from_snapshot/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 example" +} diff --git a/website/docs/r/netapp_volume.html.markdown b/website/docs/r/netapp_volume.html.markdown index 8389836d9316..9880c5a5d41e 100644 --- a/website/docs/r/netapp_volume.html.markdown +++ b/website/docs/r/netapp_volume.html.markdown @@ -72,6 +72,9 @@ resource "azurerm_netapp_volume" "example" { protocols = ["NFSv4.1"] storage_quota_in_gb = 100 + # When creating volume from a snapshot + create_from_snapshot_resource_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.NetApp/netAppAccounts/account1/capacityPools/pool1/volumes/volume1/snapshots/snapshot1" + # Following section is only required if deploying a data protection volume (secondary) # to enable Cross-Region Replication feature data_protection_replication { @@ -107,6 +110,8 @@ The following arguments are supported: * `storage_quota_in_gb` - (Required) The maximum Storage Quota allowed for a file system in Gigabytes. +* `create_from_snapshot_resource_id` - (Optional) Creates volume from snapshot. Following properties must be the same as the original volume where the snapshot was taken from: `protocols`, `subnet_id`, `location`, `service_level`, `resource_group_name`, `account_name` and `pool_name`. + * `export_policy_rule` - (Optional) One or more `export_policy_rule` block defined below. * `tags` - (Optional) A mapping of tags to assign to the resource.