diff --git a/src/azure-cli/HISTORY.rst b/src/azure-cli/HISTORY.rst index c0d13ee1b65..63670e2ede4 100644 --- a/src/azure-cli/HISTORY.rst +++ b/src/azure-cli/HISTORY.rst @@ -46,6 +46,11 @@ Release History * Fix #33183: `az cloud set`: Typo correction on AZURE_BLEU_CLOUD active directory endpoint +**Backup** + +* `az backup protection undelete`: Add support for rehydrating soft-deleted Azure File Share backup items (`--backup-management-type AzureStorage --workload-type AzureFileShare`) +* `az backup item list`: Add `--is-deleted` flag to return only soft-deleted backup items in the vault + **Compute** * `az sig create`: Add new argument group "Managed Service Identity" to configure the service identity of a Shared Image Gallery (SIG) (#33137) diff --git a/src/azure-cli/azure/cli/command_modules/backup/_help.py b/src/azure-cli/azure/cli/command_modules/backup/_help.py index 21bc6b8dfdf..b79ae7426bf 100644 --- a/src/azure-cli/azure/cli/command_modules/backup/_help.py +++ b/src/azure-cli/azure/cli/command_modules/backup/_help.py @@ -71,6 +71,8 @@ - name: List all backed up items within a container. (autogenerated) text: az backup item list --resource-group MyResourceGroup --vault-name MyVault crafted: true + - name: List only soft-deleted backup items in the vault. Soft-deleted items can be rehydrated with "az backup protection undelete". + text: az backup item list --resource-group MyResourceGroup --vault-name MyVault --backup-management-type AzureStorage --workload-type AzureFileShare --is-deleted """ helps['backup item set-policy'] = """ @@ -292,6 +294,8 @@ examples: - name: Rehydrate an item from softdeleted state to stop protection with retained data state. text: az backup protection undelete --container-name MyContainer --item-name MyItem --resource-group MyResourceGroup --vault-name MyVault --backup-management-type AzureIaasVM --workload-type VM + - name: Rehydrate a soft-deleted Azure File Share item. The item moves to "ProtectionStopped" state and recovery points are retained until soft-delete is disabled or the item is explicitly removed. + text: az backup protection undelete --container-name MyStorageAccount --item-name MyFileShare --resource-group MyResourceGroup --vault-name MyVault --backup-management-type AzureStorage --workload-type AzureFileShare """ helps['backup protection enable-for-azurewl'] = """ diff --git a/src/azure-cli/azure/cli/command_modules/backup/_params.py b/src/azure-cli/azure/cli/command_modules/backup/_params.py index dc667d99ffb..0d57535eb14 100644 --- a/src/azure-cli/azure/cli/command_modules/backup/_params.py +++ b/src/azure-cli/azure/cli/command_modules/backup/_params.py @@ -228,6 +228,8 @@ def load_arguments(self, _): c.argument('backup_management_type', extended_backup_management_type) c.argument('workload_type', workload_type) c.argument('use_secondary_region', action='store_true', help='Use this flag to list items in secondary region.') + c.argument('is_deleted', action='store_true', help='Use this flag to list only soft-deleted backup items in the vault. ' + 'These items can be rehydrated using "az backup protection undelete" while still in the soft-delete retention window.') # Policy with self.argument_context('backup policy') as c: diff --git a/src/azure-cli/azure/cli/command_modules/backup/custom_afs.py b/src/azure-cli/azure/cli/command_modules/backup/custom_afs.py index 2980a9e37a0..b5d1c358ade 100644 --- a/src/azure-cli/azure/cli/command_modules/backup/custom_afs.py +++ b/src/azure-cli/azure/cli/command_modules/backup/custom_afs.py @@ -463,6 +463,22 @@ def disable_protection(cmd, client, resource_group_name, vault_name, item, return helper.track_backup_job(cmd.cli_ctx, result, vault_name, resource_group_name) +def undelete_protection(cmd, client, resource_group_name, vault_name, item): + container_uri = helper.get_protection_container_uri_from_id(item.id) + item_uri = helper.get_protected_item_uri_from_id(item.id) + + afs_item_properties = AzureFileshareProtectedItem() + afs_item_properties.policy_id = '' + afs_item_properties.protection_state = ProtectionState.protection_stopped + afs_item_properties.source_resource_id = item.properties.source_resource_id + afs_item_properties.is_rehydrate = True + afs_item = ProtectedItemResource(properties=afs_item_properties) + + result = client.create_or_update(vault_name, resource_group_name, fabric_name, + container_uri, item_uri, afs_item, cls=helper.get_pipeline_response) + return helper.track_backup_job(cmd.cli_ctx, result, vault_name, resource_group_name) + + def resume_protection(cmd, client, resource_group_name, vault_name, item, policy): return update_policy_for_item(cmd, client, resource_group_name, vault_name, item, policy) diff --git a/src/azure-cli/azure/cli/command_modules/backup/custom_base.py b/src/azure-cli/azure/cli/command_modules/backup/custom_base.py index 826aac1944a..b9dbc7e94b7 100644 --- a/src/azure-cli/azure/cli/command_modules/backup/custom_base.py +++ b/src/azure-cli/azure/cli/command_modules/backup/custom_base.py @@ -107,9 +107,9 @@ def show_item(cmd, client, resource_group_name, vault_name, container_name, name def list_items(cmd, client, resource_group_name, vault_name, workload_type=None, container_name=None, - backup_management_type=None, use_secondary_region=None): + backup_management_type=None, use_secondary_region=None, is_deleted=None): return common.list_items(cmd, client, resource_group_name, vault_name, workload_type, - container_name, backup_management_type, use_secondary_region) + container_name, backup_management_type, use_secondary_region, is_deleted) def show_recovery_point(cmd, client, resource_group_name, vault_name, container_name, item_name, name, @@ -630,6 +630,9 @@ def undelete_protection(cmd, client, resource_group_name, vault_name, container_ if item.properties.backup_management_type.lower() == "azureiaasvm": return custom.undelete_protection(cmd, client, resource_group_name, vault_name, item) + if item.properties.backup_management_type.lower() == "azurestorage": + return custom_afs.undelete_protection(cmd, client, resource_group_name, vault_name, item) + if item.properties.backup_management_type.lower() == "azureworkload": return custom_wl.undelete_protection(cmd, client, resource_group_name, vault_name, item) diff --git a/src/azure-cli/azure/cli/command_modules/backup/custom_common.py b/src/azure-cli/azure/cli/command_modules/backup/custom_common.py index 1dff56b231a..847e3bed06a 100644 --- a/src/azure-cli/azure/cli/command_modules/backup/custom_common.py +++ b/src/azure-cli/azure/cli/command_modules/backup/custom_common.py @@ -137,7 +137,7 @@ def show_item(cmd, client, resource_group_name, vault_name, container_name, name def list_items(cmd, client, resource_group_name, vault_name, workload_type=None, container_name=None, - container_type=None, use_secondary_region=None): + container_type=None, use_secondary_region=None, is_deleted=None): workload_type = _check_map(workload_type, workload_type_map) filter_string = custom_help.get_filter_string({ 'backupManagementType': container_type, @@ -159,6 +159,10 @@ def list_items(cmd, client, resource_group_name, vault_name, workload_type=None, items = client.list(vault_name, resource_group_name, filter_string) paged_items = custom_help.get_list_from_paged_response(items) + if is_deleted: + paged_items = [item for item in paged_items + if getattr(item.properties, 'is_scheduled_for_deferred_delete', None)] + if container_name: if custom_help.is_native_name(container_name): return [item for item in paged_items if diff --git a/src/azure-cli/azure/cli/command_modules/backup/tests/latest/test_afs_commands.py b/src/azure-cli/azure/cli/command_modules/backup/tests/latest/test_afs_commands.py index 97a71df2ede..9b342a45917 100644 --- a/src/azure-cli/azure/cli/command_modules/backup/tests/latest/test_afs_commands.py +++ b/src/azure-cli/azure/cli/command_modules/backup/tests/latest/test_afs_commands.py @@ -672,3 +672,73 @@ def test_afs_backup_vaultstandard_item(self, resource_group, vault_name, storage self.cmd('backup protection disable -g {rg} -v {vault} -c {container} -i {item2} --backup-management-type AzureStorage --delete-backup-data true --yes').get_output_in_json() # self.cmd('backup container unregister -g {rg} -v {vault} -c {container} --yes --backup-management-type AzureStorage') # time.sleep(100) + + + @live_only() + @AllowLargeResponse() + @ResourceGroupPreparer(name_prefix="AzureBackupRG_clitest_", location="eastus2euap", random_name_length=32) + @VaultPreparer() + @StorageAccountPreparer(location="eastus2euap") + @FileSharePreparer() + @AFSPolicyPreparer() + @AFSItemPreparer() + def test_afs_backup_softdelete(self, resource_group, vault_name, storage_account, afs_name, policy_name): + """Validates the soft-delete lifecycle for Azure File Share backup items. + + Steps mirror test_backup_softdelete (test_backup_commands.py) for IaaS VM: + 1. Disable AFS protection with --delete-backup-data so the item is moved to + soft-deleted state. + 2. Confirm `backup item show` reports ProtectionStopped + isScheduled- + ForDeferredDelete=True. + 3. Confirm the new `--is-deleted` filter on `backup item list` includes the + soft-deleted item. + 4. Run `backup protection undelete` to rehydrate. + 5. Confirm `backup item show` reports ProtectionStopped + isScheduled- + ForDeferredDelete=None (item is rehydrated, recovery points retained). + """ + self.kwargs.update({ + 'vault': vault_name, + 'item': afs_name, + 'container': storage_account, + 'rg': resource_group, + }) + + # 1. Disable protection with delete-backup-data => soft delete + self.cmd('backup protection disable -g {rg} -v {vault} -c {container} -i {item} ' + '--backup-management-type AzureStorage --delete-backup-data true --yes', checks=[ + self.check("properties.operation", "DeleteBackupData"), + self.check("properties.status", "Completed"), + self.check("resourceGroup", '{rg}') + ]) + + # 2. Item is now soft-deleted + self.cmd('backup item show -g {rg} -v {vault} -c {container} -n {item} ' + '--backup-management-type AzureStorage --workload-type AzureFileShare', checks=[ + self.check("properties.friendlyName", '{item}'), + self.check("properties.protectionState", "ProtectionStopped"), + self.check("properties.isScheduledForDeferredDelete", True), + ]) + + # 3. --is-deleted should return the soft-deleted item + self.cmd('backup item list -g {rg} -v {vault} --backup-management-type AzureStorage ' + '--workload-type AzureFileShare --is-deleted', checks=[ + self.check("length([?properties.friendlyName == '{item}'])", 1), + self.check("[?properties.friendlyName == '{item}'].properties.isScheduledForDeferredDelete | [0]", True), + ]) + + # 4. Rehydrate + self.cmd('backup protection undelete -g {rg} -v {vault} -c {container} -i {item} ' + '--backup-management-type AzureStorage --workload-type AzureFileShare', checks=[ + self.check("properties.entityFriendlyName", '{item}'), + self.check("properties.operation", "Undelete"), + self.check("properties.status", "Completed"), + self.check("resourceGroup", '{rg}') + ]) + + # 5. Item is rehydrated, no longer soft-deleted + self.cmd('backup item show -g {rg} -v {vault} -c {container} -n {item} ' + '--backup-management-type AzureStorage --workload-type AzureFileShare', checks=[ + self.check("properties.friendlyName", '{item}'), + self.check("properties.protectionState", "ProtectionStopped"), + self.check("properties.isScheduledForDeferredDelete", None), + ])