Skip to content

Commit

Permalink
Support for Azure File Share backup (#5213)
Browse files Browse the repository at this point in the history
Addresses #5026

An additional resource beyond the suggested resources in the associated issue was required in order to first register a storage account with a recovery services vault. I've also used an updated naming scheme based on my proposal in #5089 and associated PR #5170 in order to better distinguish Azure Backup resources from Site Recovery (DR) resources. That PR does not technically block this one, but it would make the naming more consistent with the existing Azure Backup resources.
  • Loading branch information
sean-nixon authored and katbyte committed Dec 19, 2019
1 parent 1099853 commit 42b5c34
Show file tree
Hide file tree
Showing 11 changed files with 1,919 additions and 18 deletions.
46 changes: 28 additions & 18 deletions azurerm/internal/services/recoveryservices/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ import (
)

type Client struct {
ProtectedItemsClient *backup.ProtectedItemsClient
ProtectionPoliciesClient *backup.ProtectionPoliciesClient
VaultsClient *recoveryservices.VaultsClient
FabricClient func(resourceGroupName string, vaultName string) siterecovery.ReplicationFabricsClient
ProtectionContainerClient func(resourceGroupName string, vaultName string) siterecovery.ReplicationProtectionContainersClient
ReplicationPoliciesClient func(resourceGroupName string, vaultName string) siterecovery.ReplicationPoliciesClient
ContainerMappingClient func(resourceGroupName string, vaultName string) siterecovery.ReplicationProtectionContainerMappingsClient
NetworkMappingClient func(resourceGroupName string, vaultName string) siterecovery.ReplicationNetworkMappingsClient
ReplicationMigrationItemsClient func(resourceGroupName string, vaultName string) siterecovery.ReplicationProtectedItemsClient
ProtectedItemsClient *backup.ProtectedItemsClient
ProtectionPoliciesClient *backup.ProtectionPoliciesClient
BackupProtectionContainersClient *backup.ProtectionContainersClient
BackupOperationStatusesClient *backup.OperationStatusesClient
VaultsClient *recoveryservices.VaultsClient
FabricClient func(resourceGroupName string, vaultName string) siterecovery.ReplicationFabricsClient
ProtectionContainerClient func(resourceGroupName string, vaultName string) siterecovery.ReplicationProtectionContainersClient
ReplicationPoliciesClient func(resourceGroupName string, vaultName string) siterecovery.ReplicationPoliciesClient
ContainerMappingClient func(resourceGroupName string, vaultName string) siterecovery.ReplicationProtectionContainerMappingsClient
NetworkMappingClient func(resourceGroupName string, vaultName string) siterecovery.ReplicationNetworkMappingsClient
ReplicationMigrationItemsClient func(resourceGroupName string, vaultName string) siterecovery.ReplicationProtectedItemsClient
}

func NewClient(o *common.ClientOptions) *Client {
Expand All @@ -29,6 +31,12 @@ func NewClient(o *common.ClientOptions) *Client {
protectionPoliciesClient := backup.NewProtectionPoliciesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId)
o.ConfigureClient(&protectionPoliciesClient.Client, o.ResourceManagerAuthorizer)

backupProtectionContainersClient := backup.NewProtectionContainersClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId)
o.ConfigureClient(&backupProtectionContainersClient.Client, o.ResourceManagerAuthorizer)

backupOperationStatusesClient := backup.NewOperationStatusesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId)
o.ConfigureClient(&backupOperationStatusesClient.Client, o.ResourceManagerAuthorizer)

fabricClient := func(resourceGroupName string, vaultName string) siterecovery.ReplicationFabricsClient {
client := siterecovery.NewReplicationFabricsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId, resourceGroupName, vaultName)
o.ConfigureClient(&client.Client, o.ResourceManagerAuthorizer)
Expand Down Expand Up @@ -66,14 +74,16 @@ func NewClient(o *common.ClientOptions) *Client {
}

return &Client{
ProtectedItemsClient: &protectedItemsClient,
ProtectionPoliciesClient: &protectionPoliciesClient,
VaultsClient: &vaultsClient,
FabricClient: fabricClient,
ProtectionContainerClient: protectionContainerClient,
ReplicationPoliciesClient: replicationPoliciesClient,
ContainerMappingClient: containerMappingClient,
NetworkMappingClient: networkMappingClient,
ReplicationMigrationItemsClient: replicationMigrationItemsClient,
ProtectedItemsClient: &protectedItemsClient,
ProtectionPoliciesClient: &protectionPoliciesClient,
BackupProtectionContainersClient: &backupProtectionContainersClient,
BackupOperationStatusesClient: &backupOperationStatusesClient,
VaultsClient: &vaultsClient,
FabricClient: fabricClient,
ProtectionContainerClient: protectionContainerClient,
ReplicationPoliciesClient: replicationPoliciesClient,
ContainerMappingClient: containerMappingClient,
NetworkMappingClient: networkMappingClient,
ReplicationMigrationItemsClient: replicationMigrationItemsClient,
}
}
3 changes: 3 additions & 0 deletions azurerm/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@ func Provider() terraform.ResourceProvider {
"azurerm_azuread_application": resourceArmActiveDirectoryApplication(),
"azurerm_azuread_service_principal_password": resourceArmActiveDirectoryServicePrincipalPassword(),
"azurerm_azuread_service_principal": resourceArmActiveDirectoryServicePrincipal(),
"azurerm_backup_container_storage_account": resourceArmBackupProtectionContainerStorageAccount(),
"azurerm_backup_policy_file_share": resourceArmBackupProtectionPolicyFileShare(),
"azurerm_backup_protected_file_share": resourceArmBackupProtectedFileShare(),
"azurerm_backup_protected_vm": resourceArmRecoveryServicesBackupProtectedVM(),
"azurerm_backup_policy_vm": resourceArmBackupProtectionPolicyVM(),
"azurerm_bastion_host": resourceArmBastionHost(),
Expand Down
250 changes: 250 additions & 0 deletions azurerm/resource_arm_backup_container_storage_account.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
package azurerm

import (
"context"
"fmt"
"log"
"time"

"github.com/Azure/azure-sdk-for-go/services/recoveryservices/mgmt/2017-07-01/backup"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"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/internal/features"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils"
)

func resourceArmBackupProtectionContainerStorageAccount() *schema.Resource {
return &schema.Resource{
Create: resourceArmBackupProtectionContainerStorageAccountCreate,
Read: resourceArmBackupProtectionContainerStorageAccountRead,
Update: nil,
Delete: resourceArmBackupProtectionContainerStorageAccountDelete,
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{
"resource_group_name": azure.SchemaResourceGroupName(),

"recovery_vault_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: azure.ValidateRecoveryServicesVaultName,
},
"storage_account_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: azure.ValidateResourceID,
},
},
}
}

func resourceArmBackupProtectionContainerStorageAccountCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ArmClient).RecoveryServices.BackupProtectionContainersClient
opStatusClient := meta.(*ArmClient).RecoveryServices.BackupOperationStatusesClient
ctx, cancel := timeouts.ForRead(meta.(*ArmClient).StopContext, d)
defer cancel()

resGroup := d.Get("resource_group_name").(string)
vaultName := d.Get("recovery_vault_name").(string)
storageAccountID := d.Get("storage_account_id").(string)

parsedStorageAccountID, err := azure.ParseAzureResourceID(storageAccountID)
if err != nil {
return fmt.Errorf("[ERROR] Unable to parse storage_account_id '%s': %+v", storageAccountID, err)
}
accountName, hasName := parsedStorageAccountID.Path["storageAccounts"]
if !hasName {
return fmt.Errorf("[ERROR] parsed storage_account_id '%s' doesn't contain 'storageAccounts'", storageAccountID)
}

containerName := fmt.Sprintf("StorageContainer;storage;%s;%s", parsedStorageAccountID.ResourceGroup, accountName)

if features.ShouldResourcesBeImported() && d.IsNewResource() {
existing, err := client.Get(ctx, vaultName, resGroup, "Azure", containerName)
if err != nil {
if !utils.ResponseWasNotFound(existing.Response) {
return fmt.Errorf("Error checking for presence of existing recovery services protection container %s (Vault %s): %+v", containerName, vaultName, err)
}
}

if existing.ID != nil && *existing.ID != "" {
return tf.ImportAsExistsError("azurerm_backup_protection_container_storage", azure.HandleAzureSdkForGoBug2824(*existing.ID))
}
}

parameters := backup.ProtectionContainerResource{
Properties: &backup.AzureStorageContainer{
SourceResourceID: &storageAccountID,
FriendlyName: &accountName,
BackupManagementType: backup.ManagementTypeAzureStorage,
ContainerType: backup.ContainerTypeStorageContainer1,
},
}

resp, err := client.Register(ctx, vaultName, resGroup, "Azure", containerName, parameters)
if err != nil {
return fmt.Errorf("Error registering backup protection container %s (Vault %s): %+v", containerName, vaultName, err)
}

locationURL, err := resp.Response.Location() // Operation ID found in the Location header
if locationURL == nil || err != nil {
return fmt.Errorf("Unable to determine operation URL for protection container registration status for %s. (Vault %s): Location header missing or empty", containerName, vaultName)
}

opResourceID := azure.HandleAzureSdkForGoBug2824(locationURL.Path)

parsedLocation, err := azure.ParseAzureResourceID(opResourceID)
if err != nil {
return err
}

operationID := parsedLocation.Path["operationResults"]
if _, err = resourceArmBackupProtectionContainerStorageAccountWaitForOperation(ctx, opStatusClient, vaultName, resGroup, operationID, d); err != nil {
return err
}

resp, err = client.Get(ctx, vaultName, resGroup, "Azure", containerName)
if err != nil {
return fmt.Errorf("Error retrieving site recovery protection container %s (Vault %s): %+v", containerName, vaultName, err)
}

d.SetId(azure.HandleAzureSdkForGoBug2824(*resp.ID))

return resourceArmBackupProtectionContainerStorageAccountRead(d, meta)
}

func resourceArmBackupProtectionContainerStorageAccountRead(d *schema.ResourceData, meta interface{}) error {
id, err := azure.ParseAzureResourceID(d.Id())
if err != nil {
return err
}

resGroup := id.ResourceGroup
vaultName := id.Path["vaults"]
fabricName := id.Path["backupFabrics"]
containerName := id.Path["protectionContainers"]

client := meta.(*ArmClient).RecoveryServices.BackupProtectionContainersClient
ctx, cancel := timeouts.ForRead(meta.(*ArmClient).StopContext, d)
defer cancel()

resp, err := client.Get(ctx, vaultName, resGroup, fabricName, containerName)
if err != nil {
if utils.ResponseWasNotFound(resp.Response) {
d.SetId("")
return nil
}
return fmt.Errorf("Error making Read request on backup protection container %s (Vault %s): %+v", containerName, vaultName, err)
}

d.Set("resource_group_name", resGroup)
d.Set("recovery_vault_name", vaultName)

if properties, ok := resp.Properties.AsAzureStorageContainer(); ok && properties != nil {
d.Set("storage_account_id", properties.SourceResourceID)
}

return nil
}

func resourceArmBackupProtectionContainerStorageAccountDelete(d *schema.ResourceData, meta interface{}) error {
id, err := azure.ParseAzureResourceID(d.Id())
if err != nil {
return err
}

resGroup := id.ResourceGroup
vaultName := id.Path["vaults"]
fabricName := id.Path["backupFabrics"]
containerName := id.Path["protectionContainers"]

client := meta.(*ArmClient).RecoveryServices.BackupProtectionContainersClient
opClient := meta.(*ArmClient).RecoveryServices.BackupOperationStatusesClient
ctx, cancel := timeouts.ForDelete(meta.(*ArmClient).StopContext, d)
defer cancel()

resp, err := client.Unregister(ctx, vaultName, resGroup, fabricName, containerName)
if err != nil {
return fmt.Errorf("Error deregistering backup protection container %s (Vault %s): %+v", containerName, vaultName, err)
}

locationURL, err := resp.Response.Location()
if err != nil || locationURL == nil {
return fmt.Errorf("Error unregistering backup protection container %s (Vault %s): Location header missing or empty", containerName, vaultName)
}

opResourceID := azure.HandleAzureSdkForGoBug2824(locationURL.Path)

parsedLocation, err := azure.ParseAzureResourceID(opResourceID)
if err != nil {
return err
}
operationID := parsedLocation.Path["backupOperationResults"]

if _, err = resourceArmBackupProtectionContainerStorageAccountWaitForOperation(ctx, opClient, vaultName, resGroup, operationID, d); err != nil {
return err
}

return nil
}

func resourceArmBackupProtectionContainerStorageAccountWaitForOperation(ctx context.Context, client *backup.OperationStatusesClient, vaultName, resourceGroup, operationID string, d *schema.ResourceData) (backup.OperationStatus, error) {
state := &resource.StateChangeConf{
MinTimeout: 10 * time.Second,
Delay: 10 * time.Second,
Pending: []string{"InProgress"},
Target: []string{"Succeeded"},
Refresh: resourceArmBackupProtectionContainerStorageAccountCheckOperation(ctx, client, vaultName, resourceGroup, operationID),
ContinuousTargetOccurence: 5, // Without this buffer, file share backups and storage account deletions may fail if performed immediately after creating/destroying the container
}

if features.SupportsCustomTimeouts() {
if d.IsNewResource() {
state.Timeout = d.Timeout(schema.TimeoutCreate)
} else {
state.Timeout = d.Timeout(schema.TimeoutUpdate)
}
} else {
state.Timeout = 30 * time.Minute
}

log.Printf("[DEBUG] Waiting for backup container operation %q (Vault %q) to complete", operationID, vaultName)
resp, err := state.WaitForState()
if err != nil {
return resp.(backup.OperationStatus), err
}
return resp.(backup.OperationStatus), nil
}

func resourceArmBackupProtectionContainerStorageAccountCheckOperation(ctx context.Context, client *backup.OperationStatusesClient, vaultName, resourceGroup, operationID string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
resp, err := client.Get(ctx, vaultName, resourceGroup, operationID)
if err != nil {
return resp, "Error", fmt.Errorf("Error making Read request on Recovery Service Protection Container operation %q (Vault %q in Resource Group %q): %+v", operationID, vaultName, resourceGroup, err)
}

if opErr := resp.Error; opErr != nil {
errMsg := "No upstream error message"
if opErr.Message != nil {
errMsg = *opErr.Message
}
err = fmt.Errorf("Recovery Service Protection Container operation status failed with status %q (Vault %q Resource Group %q Operation ID %q): %+v", resp.Status, vaultName, resourceGroup, operationID, errMsg)
}

return resp, string(resp.Status), err
}
}
Loading

0 comments on commit 42b5c34

Please sign in to comment.