Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

azurerm_backup_protected_vm: support protection_state #20608

Merged
merged 12 commits into from
Jun 13, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ func resourceRecoveryServicesBackupProtectedVMCreateUpdate(d *pluginsdk.Resource
vmId := d.Get("source_vm_id").(string)
policyId := d.Get("backup_policy_id").(string)

if d.IsNewResource() && policyId == "" {
return fmt.Errorf("`backup_policy_id` must be specified when creating")
}

// get VM name from id
parsedVmId, err := vmParse.VirtualMachineID(vmId)
if err != nil {
Expand Down Expand Up @@ -106,17 +110,52 @@ func resourceRecoveryServicesBackupProtectedVMCreateUpdate(d *pluginsdk.Resource
},
}

if _, err = client.CreateOrUpdate(ctx, vaultName, resourceGroup, "Azure", containerName, protectedItemName, item); err != nil {
return fmt.Errorf("creating/updating Azure Backup Protected VM %q (Resource Group %q): %+v", protectedItemName, resourceGroup, err)
}
requireAdditionalUpdate := d.Get("protection_stopped").(bool)
skipNormalUpdate := d.Get("protection_stopped").(bool) && !d.IsNewResource()

resp, err := resourceRecoveryServicesBackupProtectedVMWaitForStateCreateUpdate(ctx, client, vaultName, resourceGroup, containerName, protectedItemName, d)
if err != nil {
return err
if !skipNormalUpdate {
ziyeqf marked this conversation as resolved.
Show resolved Hide resolved
if _, err = client.CreateOrUpdate(ctx, vaultName, resourceGroup, "Azure", containerName, protectedItemName, item); err != nil {
return fmt.Errorf("creating/updating Azure Backup Protected VM %q (Resource Group %q): %+v", protectedItemName, resourceGroup, err)
}

resp, err := resourceRecoveryServicesBackupProtectedVMWaitForStateCreateUpdate(ctx, client, vaultName, resourceGroup, containerName, protectedItemName, d)
if err != nil {
return err
}

id := strings.Replace(*resp.ID, "Subscriptions", "subscriptions", 1) // This code is a workaround for this bug https://github.com/Azure/azure-sdk-for-go/issues/2824
d.SetId(id)
ziyeqf marked this conversation as resolved.
Show resolved Hide resolved
}

id := strings.Replace(*resp.ID, "Subscriptions", "subscriptions", 1) // This code is a workaround for this bug https://github.com/Azure/azure-sdk-for-go/issues/2824
d.SetId(id)
if requireAdditionalUpdate {
ziyeqf marked this conversation as resolved.
Show resolved Hide resolved
updateInput := backup.ProtectedItemResource{
Properties: &backup.AzureIaaSComputeVMProtectedItem{
ProtectionState: backup.ProtectionStateProtectionStopped,
SourceResourceID: utils.String(vmId),
},
}

resp, err := client.CreateOrUpdate(ctx, vaultName, resourceGroup, "Azure", containerName, protectedItemName, updateInput)
if err != nil {
return fmt.Errorf("creating/updating Azure Backup Protected VM %q (Resource Group %q): %+v", protectedItemName, resourceGroup, err)
}

locationURL, err := resp.Response.Location()
if err != nil || locationURL == nil {
return fmt.Errorf("creating/updating Azure Backup Protected VM %q (Resource Group %q): Location header missing or empty", protectedItemName, resourceGroup)
}

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

opState := resourceRecoveryServicesBackupProtectedVMOperationRefreshFunc(ctx, meta.(*clients.Client).RecoveryServices.BackupOperationResultsClient, d.Timeout(pluginsdk.TimeoutCreate), vaultName, resourceGroup, parsedLocation.Path["operationResults"])
ziyeqf marked this conversation as resolved.
Show resolved Hide resolved
if _, err := opState.WaitForStateContext(ctx); err != nil {
return fmt.Errorf("creating/updating Azure Backup Protected VM %q (Resource Group %q): %+v", protectedItemName, resourceGroup, err)
}
ziyeqf marked this conversation as resolved.
Show resolved Hide resolved

}

return resourceRecoveryServicesBackupProtectedVMRead(d, meta)
}
Expand Down Expand Up @@ -149,6 +188,7 @@ func resourceRecoveryServicesBackupProtectedVMRead(d *pluginsdk.ResourceData, me
if properties := resp.Properties; properties != nil {
if vm, ok := properties.AsAzureIaaSComputeVMProtectedItem(); ok {
d.Set("source_vm_id", vm.SourceResourceID)
d.Set("protection_stopped", vm.ProtectionState == backup.ProtectionStateProtectionStopped)
ziyeqf marked this conversation as resolved.
Show resolved Hide resolved

if v := vm.PolicyID; v != nil {
d.Set("backup_policy_id", strings.Replace(*v, "Subscriptions", "subscriptions", 1))
Expand Down Expand Up @@ -268,7 +308,18 @@ func resourceRecoveryServicesBackupProtectedVMWaitForDeletion(ctx context.Contex
}

// we should also wait for the operation to complete, or it will fail when creating a new backup vm with the same vm in different vault immediately.
opState := &pluginsdk.StateChangeConf{
opState := resourceRecoveryServicesBackupProtectedVMOperationRefreshFunc(ctx, opResultClient, d.Timeout(pluginsdk.TimeoutDelete), vaultName, resourceGroup, operationId)
ziyeqf marked this conversation as resolved.
Show resolved Hide resolved

_, err = opState.WaitForStateContext(ctx)
if err != nil {
return resp.(backup.ProtectedItemResource), fmt.Errorf("waiting for the Recovery Service Protected Item operation %q to be deleted (Resource Group %q): %+v", containerName, resourceGroup, err)
}

return resp.(backup.ProtectedItemResource), nil
}

func resourceRecoveryServicesBackupProtectedVMOperationRefreshFunc(ctx context.Context, opResultClient *backup.OperationResultsClient, timeout time.Duration, vaultName, resourceGroup, operationId string) pluginsdk.StateChangeConf {
return pluginsdk.StateChangeConf{
MinTimeout: 30 * time.Second,
Delay: 10 * time.Second,
Pending: []string{"202"},
Expand All @@ -281,15 +332,8 @@ func resourceRecoveryServicesBackupProtectedVMWaitForDeletion(ctx context.Contex
return resp, strconv.Itoa(resp.StatusCode), err
},

Timeout: d.Timeout(pluginsdk.TimeoutDelete),
}

_, err = opState.WaitForStateContext(ctx)
if err != nil {
return resp.(backup.ProtectedItemResource), fmt.Errorf("waiting for the Recovery Service Protected Item operation %q to be deleted (Resource Group %q): %+v", containerName, resourceGroup, err)
Timeout: timeout,
}

return resp.(backup.ProtectedItemResource), nil
}

func resourceRecoveryServicesBackupProtectedVMRefreshFunc(ctx context.Context, client *backup.ProtectedItemsClient, vaultName, resourceGroup, containerName, protectedItemName string) pluginsdk.StateRefreshFunc {
Expand Down Expand Up @@ -365,8 +409,9 @@ func resourceRecoveryServicesBackupProtectedVMSchema() map[string]*pluginsdk.Sch

"backup_policy_id": {
Type: pluginsdk.TypeString,
Required: true,
Optional: true,
ValidateFunc: azure.ValidateResourceID,
ziyeqf marked this conversation as resolved.
Show resolved Hide resolved
ExactlyOneOf: []string{"backup_policy_id", "protection_stopped"},
},

"exclude_disk_luns": {
Expand All @@ -388,5 +433,12 @@ func resourceRecoveryServicesBackupProtectedVMSchema() map[string]*pluginsdk.Sch
ValidateFunc: validation.IntAtLeast(0),
},
},

"protection_stopped": {
Type: pluginsdk.TypeBool,
Optional: true,
Computed: true,
ExactlyOneOf: []string{"backup_policy_id", "protection_stopped"},
},
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,34 @@ func TestAccBackupProtectedVm_removeVM(t *testing.T) {
})
}

func TestAccBackupProtectedVm_protectionStopped(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_backup_protected_vm", "test")
r := BackupProtectedVmResource{}

data.ResourceTest(t, r, []acceptance.TestStep{
{
Config: r.basic(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
check.That(data.ResourceName).Key("resource_group_name").Exists(),
),
},
data.ImportStep(),
{
Config: r.protectionStopped(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
check.That(data.ResourceName).Key("resource_group_name").Exists(),
),
},
data.ImportStep(),
{
// vault cannot be deleted unless we unregister all backups
Config: r.base(data),
},
})
}

func (t BackupProtectedVmResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) {
id, err := parse.ProtectedItemID(state.ID)
if err != nil {
Expand Down Expand Up @@ -333,6 +361,7 @@ resource "azurerm_virtual_machine" "test" {
enabled = true
storage_uri = azurerm_storage_account.test.primary_blob_endpoint
}

}

resource "azurerm_recovery_services_vault" "test" {
Expand Down Expand Up @@ -687,3 +716,18 @@ resource "azurerm_backup_protected_vm" "test" {
}
`, r.additionalVault(data))
}

func (r BackupProtectedVmResource) protectionStopped(data acceptance.TestData) string {
return fmt.Sprintf(`
%s

resource "azurerm_backup_protected_vm" "test" {
resource_group_name = azurerm_resource_group.test.name
recovery_vault_name = azurerm_recovery_services_vault.test.name
source_vm_id = azurerm_virtual_machine.test.id

include_disk_luns = [0]
protection_stopped = true
}
`, r.base(data))
}
4 changes: 3 additions & 1 deletion website/docs/r/backup_protected_vm.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,14 @@ The following arguments are supported:
~> **NOTE:** After creation, the `source_vm_id` property can be removed without forcing a new resource to be created; however, setting it to a different ID will create a new resource.
This allows the source vm to be deleted without having to remove the backup.

* `backup_policy_id` - (Required) Specifies the id of the backup policy to use.
* `backup_policy_id` - (Optional) Specifies the id of the backup policy to use. Required in creation or when `protection_stopped` is not specified.

* `exclude_disk_luns` - (Optional) A list of Disks' Logical Unit Numbers(LUN) to be excluded for VM Protection.

* `include_disk_luns` - (Optional) A list of Disks' Logical Unit Numbers(LUN) to be included for VM Protection.

* `protection_stopped` - (Optional) Specifies whether to stop the protection. Defaults to `false`.
ziyeqf marked this conversation as resolved.
Show resolved Hide resolved

## Attributes Reference

The following attributes are exported:
Expand Down