From 05cbe5702c40c03dce82ef5aa2623bfdbed19237 Mon Sep 17 00:00:00 2001 From: Bharat Purwar Date: Mon, 18 May 2026 13:27:13 +0530 Subject: [PATCH 1/3] [backup] AFS soft delete: implement undelete for Azure File Share * custom_afs.py: add `undelete_protection` mirroring the IaaS VM flow but built from `AzureFileshareProtectedItem` (policy_id="", protection_state=ProtectionStopped, source_resource_id from item, is_rehydrate=True). Issues PUT to the protected-item endpoint via `client.create_or_update` and tracks the resulting backup job. * custom_base.py (undelete_protection dispatcher): add `azurestorage` branch that calls `custom_afs.undelete_protection`. Sits between the existing `azureiaasvm` (VM) and `azureworkload` (SAP HANA/SQL) branches so `az backup protection undelete --backup-management-type AzureStorage --workload-type AzureFileShare ...` now succeeds for soft-deleted Azure File Shares. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cli/command_modules/backup/custom_afs.py | 16 ++++++++++++++++ .../cli/command_modules/backup/custom_base.py | 3 +++ 2 files changed, 19 insertions(+) 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..b7b94ffaa9b 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 @@ -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) From 24c3d3b8f4e8c085079ce1f3a5d333fdd484dc17 Mon Sep 17 00:00:00 2001 From: Bharat Purwar Date: Mon, 18 May 2026 13:28:51 +0530 Subject: [PATCH 2/3] [backup] Add --is-deleted flag to "az backup item list" Customers can now scope `az backup item list` to soft-deleted backup items only, mirroring the PowerShell -DeleteState SoftDeleted parameter on Get-AzRecoveryServicesBackupItem. * _params.py (backup item list): register `--is-deleted` switch. * custom_base.list_items / custom_common.list_items: thread the `is_deleted` boolean through and apply a client-side filter on `properties.is_scheduled_for_deferred_delete` after the LIST call. Client-side is required because the protectedItems LIST endpoint does not expose `isDeleted` as an OData predicate; the service already returns soft-deleted items in the default response. Works for every backup-management-type that supports soft delete (AzureIaasVM, AzureStorage / AzureFileShare, AzureWorkload). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/azure-cli/azure/cli/command_modules/backup/_params.py | 2 ++ .../azure/cli/command_modules/backup/custom_base.py | 4 ++-- .../azure/cli/command_modules/backup/custom_common.py | 6 +++++- 3 files changed, 9 insertions(+), 3 deletions(-) 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_base.py b/src/azure-cli/azure/cli/command_modules/backup/custom_base.py index b7b94ffaa9b..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, 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 From bcdefcaa19cb5a90829698d7bc94dfbb4315b145 Mon Sep 17 00:00:00 2001 From: Bharat Purwar Date: Mon, 18 May 2026 13:31:04 +0530 Subject: [PATCH 3/3] [backup] AFS soft delete: scenario test + help + HISTORY entries * tests/latest/test_afs_commands.py: add `test_afs_backup_softdelete` modelled on the IaaS VM equivalent (test_backup_softdelete in test_backup_commands.py). Covers the full soft-delete lifecycle: disable with delete-backup-data -> verify ProtectionStopped + isScheduledForDeferredDelete=True via `backup item show` and the new `backup item list --is-deleted` filter -> undelete -> verify rehydrated (isScheduledForDeferredDelete=None). Marked @live_only because soft-delete state transitions require a live AFS vault. * _help.py: add AFS example to `backup protection undelete` and an --is-deleted example to `backup item list`. * HISTORY.rst: under 2.86.0 add a Backup subsection (between Cloud and Compute) noting the AFS undelete support and `--is-deleted` flag. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/azure-cli/HISTORY.rst | 5 ++ .../azure/cli/command_modules/backup/_help.py | 4 ++ .../backup/tests/latest/test_afs_commands.py | 70 +++++++++++++++++++ 3 files changed, 79 insertions(+) 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/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), + ])