Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
da1d89a
Initial code changes
Oct 22, 2025
ca2ebcc
Fix list, get, and undelete
Oct 23, 2025
e9cdb2b
list deleted containers works
Oct 23, 2025
969275d
azdev style now works
Oct 23, 2025
2f3328a
Initial test code
Oct 24, 2025
e957d2f
Merge remote-tracking branch 'upstream/dev' into users/zubair/RSVVaul…
Oct 24, 2025
0df0bdc
Update vault create to allow for new API operation
Oct 24, 2025
2dd0b71
The new tests work
Oct 24, 2025
85f04b1
Fix test RG name length
Oct 27, 2025
21bf9ee
Merge remote-tracking branch 'upstream/dev' into users/zubair/RSVVaul…
Oct 27, 2025
916aa37
Update src/azure-cli/azure/cli/command_modules/backup/_help.py
zubairabid Oct 27, 2025
2296190
Update src/azure-cli/azure/cli/command_modules/backup/_help.py
zubairabid Oct 27, 2025
d9310bc
Reruns of most tests. Some VM and some AFS tests pending.
Oct 27, 2025
8da4253
Merge branch 'users/zubair/RSVVaultSoftDelete' of https://github.com/…
Oct 27, 2025
e767630
More passing tests
Oct 27, 2025
353f959
more pass
Oct 27, 2025
631e27d
Run encryption test in redbox
Oct 27, 2025
7555845
Run encryption test
zubairabid Oct 27, 2025
408cf5a
All but one AFS test
Oct 27, 2025
93b86b2
Merge branch 'users/zubair/RSVVaultSoftDelete' of https://github.com/…
Oct 27, 2025
bc6d1bc
.
Oct 27, 2025
4de68f6
Using VM Pattern for ARG Queries.
zubairabid Oct 27, 2025
c4848d6
Rerun tests live
Oct 27, 2025
9cb8c75
Merge branch 'users/zubair/RSVVaultSoftDelete' of https://github.com/…
zubairabid Oct 27, 2025
220975e
Rerun VSD test, switch AFS to live only
Oct 27, 2025
ac0b611
SQL CRR now run
Oct 27, 2025
7e594c9
Reconfigure test run now
zubairabid Oct 27, 2025
438b94a
Merge branch 'users/zubair/RSVVaultSoftDelete' of https://github.com/…
zubairabid Oct 27, 2025
8acd3dc
Merge remote-tracking branch 'upstream/dev' into users/zubair/RSVVaul…
zubairabid Oct 27, 2025
39569ef
Linters work
zubairabid Oct 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ def vaults_cf(cli_ctx, *_):
return _common_client_factory(cli_ctx).vaults


def deleted_vaults_cf(cli_ctx, *_):
return _common_client_factory(cli_ctx).deleted_vaults


def registered_identities_cf(cli_ctx, *_):
return _common_client_factory(cli_ctx).registered_identities

Expand Down
39 changes: 39 additions & 0 deletions src/azure-cli/azure/cli/command_modules/backup/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,3 +597,42 @@
- name: Delete resource guard mapping of the Recovery Services vault.
text: az backup vault resource-guard-mapping delete --resource-group MyResourceGroup --name MyVault
"""

helps['backup deleted-vault'] = """
type: group
short-summary: Manage soft-deleted Recovery Services vaults.
"""

helps['backup deleted-vault list'] = """
type: command
short-summary: List soft-deleted Recovery Services vaults.
examples:
- name: List soft-deleted vaults in a specific location under the active subscription.
text: az backup vault deleted-vault list --location eastus
"""

helps['backup deleted-vault get'] = """
type: command
short-summary: Get details of a soft-deleted Recovery Services vault.
examples:
- name: Get details of a soft-deleted vault by name and location.
text: az backup vault deleted-vault list --location eastus --name deletedVaultName
Comment thread
zubairabid marked this conversation as resolved.
Outdated
"""

helps['backup deleted-vault undelete'] = """
type: command
short-summary: Restore a soft-deleted Recovery Services vault.
examples:
- name: Restore a soft-deleted vault by name and location.
text: az backup vault deleted-vault undelete --name MyVault --location eastus
- name: Restore a soft-deleted vault using its ARM ID.
text: az backup vault deleted-vault undelete --ids /subscriptions/{subscription-id}/locations/{location}/deletedVaults/{deleted-vault-name}
"""

helps['backup deleted-vault list-containers'] = """
type: command
short-summary: List backup containers in a soft-deleted vault.
examples:
- name: List backup containers in a soft-deleted vault.
text: az backup vault deleted-vault list-containers --name MyVault --resource-group MyResourceGroup
Comment thread
zubairabid marked this conversation as resolved.
Outdated
"""
10 changes: 9 additions & 1 deletion src/azure-cli/azure/cli/command_modules/backup/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,15 @@ def load_arguments(self, _):
with self.argument_context('backup vault encryption show') as c:
c.argument('vault_name', vault_name_type, options_list=['--name', '-n'], id_part='name')

# Deleted Vault
with self.argument_context('backup deleted-vault') as c:
c.argument('location', help='Location of the deleted vault.')

for command in ['get', 'undelete', 'list-containers']:
with self.argument_context('backup deleted-vault ' + command) as c:
c.argument('deleted_vault_name', vault_name_type, options_list=['--name', '-n'], help='Name of the deleted vault.')
c.argument('deleted_vault_id', options_list=['--ids', '--deleted-vault-id'], help='ID of the deleted vault.')

# Container
with self.argument_context('backup container') as c:
c.argument('vault_name', vault_name_type, id_part='name')
Expand Down Expand Up @@ -403,7 +412,6 @@ def load_arguments(self, _):
c.argument('tenant_id', help='ID of the tenant if the Resource Guard protecting the vault exists in a different tenant.')
c.argument('disk_access_option', arg_type=get_enum_type(allowed_disk_access_options), help='Specify the disk access option for target disks.')
c.argument('target_disk_access_id', help='Specify the target disk access ID when --disk-access-option is set to EnablePrivateAccessForAllDisks')
c.argument('cvm_os_des_id', options_list=['--cvm-os-des-id', '--cvm-os-disk-encryption-set-id'], help='Specify the Disk Encryption Set ID to use for OS disk encryption during restore of a Confidential VM. This is applicable only for Confidential VMs with managed disks. Please ensure that Disk Encryption Set has access to the Key vault.')

with self.argument_context('backup restore restore-azurefileshare') as c:
c.argument('resolve_conflict', resolve_conflict_type)
Expand Down
19 changes: 13 additions & 6 deletions src/azure-cli/azure/cli/command_modules/backup/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
# --------------------------------------------------------------------------------------------

from azure.cli.core.commands import CliCommandType
from azure.cli.command_modules.backup._client_factory import vaults_cf, backup_protection_containers_cf, \
protection_policies_cf, backup_policies_cf, protected_items_cf, backups_cf, backup_jobs_cf, \
job_details_cf, job_cancellations_cf, recovery_points_cf, restores_cf, backup_storage_configs_non_crr_cf, \
item_level_recovery_connections_cf, backup_protected_items_cf, backup_protectable_items_cf, \
protection_containers_cf, protection_intent_cf, backup_resource_encryption_config_cf, resource_guard_proxy_cf, \
deleted_protection_containers_cf # pylint: disable=unused-variable
from azure.cli.command_modules.backup._client_factory import (
vaults_cf, deleted_vaults_cf, backup_protection_containers_cf, protection_policies_cf,
backup_policies_cf, protected_items_cf, backups_cf, backup_jobs_cf, job_details_cf,
job_cancellations_cf, recovery_points_cf, restores_cf, backup_storage_configs_non_crr_cf,
item_level_recovery_connections_cf, backup_protected_items_cf, backup_protectable_items_cf,
protection_containers_cf, protection_intent_cf, backup_resource_encryption_config_cf,
resource_guard_proxy_cf, deleted_protection_containers_cf) # pylint: disable=unused-variable
from azure.cli.command_modules.backup._exception_handler import backup_exception_handler
from azure.cli.command_modules.backup._format import (
transform_container_list, transform_policy_list, transform_item_list, transform_job_list,
Expand Down Expand Up @@ -43,6 +44,12 @@ def load_command_table(self, _):
g.custom_command('encryption update', 'update_encryption')
g.custom_command('encryption show', 'show_encryption', client_factory=backup_resource_encryption_config_cf)

with self.command_group('backup deleted-vault', backup_custom, client_factory=deleted_vaults_cf, exception_handler=backup_exception_handler) as g:
g.custom_command('list', 'list_deleted_vaults')
g.custom_command('get', 'get_deleted_vault')
g.custom_command('undelete', 'undelete_vault')
Comment thread
zubairabid marked this conversation as resolved.
g.custom_command('list-containers', 'list_deleted_vault_containers')

with self.command_group('backup vault resource-guard-mapping', backup_custom, client_factory=resource_guard_proxy_cf, exception_handler=backup_exception_handler) as g:
g.show_command('update', 'update_resource_guard_mapping')
g.show_command('show', 'show_resource_guard_mapping')
Expand Down
82 changes: 81 additions & 1 deletion src/azure-cli/azure/cli/command_modules/backup/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from azure.mgmt.recoveryservices.models import Vault, VaultProperties, Sku, SkuName, PatchVault, IdentityData, \
CmkKeyVaultProperties, CmkKekIdentity, VaultPropertiesEncryption, UserIdentity, MonitoringSettings, \
AzureMonitorAlertSettings, ClassicAlertSettings, SecuritySettings, ImmutabilitySettings, RestoreSettings, \
CrossSubscriptionRestoreSettings
CrossSubscriptionRestoreSettings, DeletedVaultUndeleteInputProperties, DeletedVaultUndeleteInput
from azure.mgmt.recoveryservicesbackup.activestamp.models import ProtectedItemResource, \
AzureIaaSComputeVMProtectedItem, AzureIaaSClassicComputeVMProtectedItem, ProtectionState, IaasVMBackupRequest, \
BackupRequestResource, IaasVMRestoreRequest, RestoreRequestResource, BackupManagementType, WorkloadType, \
Expand Down Expand Up @@ -449,6 +449,86 @@ def list_vaults(client, resource_group_name=None):
return client.list_by_subscription_id()


def list_deleted_vaults(cmd, client, location):
return client.list_by_subscription_id(location=location)


def get_deleted_vault(cmd, client, deleted_vault_name=None, location=None, deleted_vault_id=None):
if deleted_vault_name is None or location is None:
# Parse the deleted vault ID to extract name and location
deleted_vault_name, location = cust_help.get_deleted_vault_parameters(deleted_vault_id)

return client.get(location, deleted_vault_name)


def undelete_vault(cmd, client, deleted_vault_name=None, location=None, deleted_vault_id=None):
if deleted_vault_name is None or location is None:
# Parse the deleted vault ID to extract name and location
deleted_vault_name, location = cust_help.get_deleted_vault_parameters(deleted_vault_id)

deleted_vault_entity = get_deleted_vault(cmd, client, deleted_vault_name, location)
if deleted_vault_entity is None:
raise ResourceNotFoundError(f"Deleted vault '{deleted_vault_name}' not found in location '{location}'.")

resource_group = cust_help.extract_arm_resource_group_from_id(deleted_vault_entity.properties['vaultId'])
request_body = DeletedVaultUndeleteInput(
properties=DeletedVaultUndeleteInputProperties(
recovery_resource_group_id=resource_group
)
)

# Start the undelete operation and wait for completion
return client.begin_undelete(location, deleted_vault_name, request_body).result()


def list_deleted_vault_containers(cmd, client, deleted_vault_name=None, location=None, deleted_vault_id=None):
"""List backup containers in a soft-deleted vault using Azure Resource Graph."""
from azure.mgmt.resourcegraph import ResourceGraphClient
from azure.mgmt.resourcegraph.models import QueryRequest

subscription_id = get_subscription_id(cmd.cli_ctx)

# Build the KQL query dynamically based on input parameters
query = '''recoveryservicesresources
| where type == "microsoft.recoveryservices/locations/deletedvaults/backupfabrics/protectioncontainers/protecteditems"
| extend dataSourceType = strcat(properties.backupManagementType, '/', properties.workloadType)'''
Comment thread
zubairabid marked this conversation as resolved.

# Add filtering based on provided parameters
if deleted_vault_id:
# If vault ID is provided, filter by it
query += f'''
| where tostring(id) contains "{deleted_vault_id}"'''
elif deleted_vault_name:
# If vault name is provided, filter by name
query += f'''
| where tostring(id) contains "{deleted_vault_name}"'''

# Also filter by location if provided
if location:
query += f'''
| where tostring(id) contains "{location}"'''

query += '''
| project id, type, name, location, resourceGroup, subscriptionId, dataSourceType, properties, tags'''

# Create Resource Graph client with proper credential handling
from azure.cli.core._profile import Profile
profile = Profile(cli_ctx=cmd.cli_ctx)
credential, _, _ = profile.get_login_credentials()
resource_graph_client = ResourceGraphClient(credential)

query_request = QueryRequest(
subscriptions=[subscription_id],
query=query
)

try:
response = resource_graph_client.resources(query_request)
return response.data
except Exception as ex:
raise CLIError(f"Failed to query backup containers: {str(ex)}")


def assign_identity(client, resource_group_name, vault_name, system_assigned=None, user_assigned=None):
vault_details = client.get(resource_group_name, vault_name)

Expand Down
26 changes: 26 additions & 0 deletions src/azure-cli/azure/cli/command_modules/backup/custom_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,32 @@ def get_containers(client, container_type, status, resource_group_name, vault_na
return containers


def extract_arm_resource_group_from_id(resource_id):
if not is_valid_resource_id(resource_id):
raise InvalidArgumentValueError("Please provide a valid resource id.")
id_parts = parse_resource_id(resource_id)
if id_parts.get('subscription') is None or id_parts.get('resource_group') is None:
raise InvalidArgumentValueError("The resource ID does not contain subscription or resource group information.")

arm_resource_group = (f"/subscriptions/{id_parts.get('subscription')}/"
f"resourceGroups/{id_parts.get('resource_group')}")
return arm_resource_group


def get_deleted_vault_parameters(deleted_vault_id):
if is_valid_resource_id(deleted_vault_id):
id_parts = parse_resource_id(deleted_vault_id)
deleted_vault_name = id_parts.get('child_name_1')
location = id_parts.get('name')
if deleted_vault_name is None or location is None:
raise InvalidArgumentValueError("Unable to fetch the deleted vault name and the location. "
"Please provide a valid deleted vault id.")
else:
raise InvalidArgumentValueError("Please provide a valid deleted vault id.")

return deleted_vault_name, location


def get_resource_name_and_rg(resource_group_name, name_or_id):
if is_valid_resource_id(name_or_id):
id_parts = parse_resource_id(name_or_id)
Expand Down
2 changes: 1 addition & 1 deletion src/azure-cli/requirements.py3.Darwin.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ azure-mgmt-postgresqlflexibleservers==1.1.0b2
azure-mgmt-privatedns==1.0.0
azure-mgmt-rdbms==10.2.0b17
azure-mgmt-mysqlflexibleservers==1.0.0b3
azure-mgmt-recoveryservices==3.1.0
azure-mgmt-recoveryservices==4.0.0
azure-mgmt-recoveryservicesbackup==9.2.0
azure-mgmt-redhatopenshift==1.5.0
azure-mgmt-redis==14.5.0
Expand Down
2 changes: 1 addition & 1 deletion src/azure-cli/requirements.py3.Linux.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ azure-mgmt-postgresqlflexibleservers==1.1.0b2
azure-mgmt-privatedns==1.0.0
azure-mgmt-rdbms==10.2.0b17
azure-mgmt-mysqlflexibleservers==1.0.0b3
azure-mgmt-recoveryservices==3.1.0
azure-mgmt-recoveryservices==4.0.0
azure-mgmt-recoveryservicesbackup==9.2.0
azure-mgmt-redhatopenshift==1.5.0
azure-mgmt-redis==14.5.0
Expand Down
2 changes: 1 addition & 1 deletion src/azure-cli/requirements.py3.windows.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ azure-mgmt-postgresqlflexibleservers==1.1.0b2
azure-mgmt-privatedns==1.0.0
azure-mgmt-rdbms==10.2.0b17
azure-mgmt-mysqlflexibleservers==1.0.0b3
azure-mgmt-recoveryservices==3.1.0
azure-mgmt-recoveryservices==4.0.0
azure-mgmt-recoveryservicesbackup==9.2.0
azure-mgmt-redhatopenshift==1.5.0
azure-mgmt-redis==14.5.0
Expand Down
2 changes: 1 addition & 1 deletion src/azure-cli/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
'azure-mgmt-rdbms==10.2.0b17',
'azure-mgmt-mysqlflexibleservers==1.0.0b3',
'azure-mgmt-recoveryservicesbackup~=9.2.0',
'azure-mgmt-recoveryservices~=3.1.0',
'azure-mgmt-recoveryservices~=4.0.0',
'azure-mgmt-redhatopenshift~=1.5.0',
'azure-mgmt-redis~=14.5.0',
'azure-mgmt-resource==23.3.0',
Expand Down
Loading