Skip to content

Commit ca880ae

Browse files
zubairabidCopilot
andauthored
[Backup] backup protection reconfigure: Add new command to support reconfiguring backup to an alternate vault (#32193)
Co-authored-by: Copilot <[email protected]>
1 parent 27c1a5c commit ca880ae

File tree

12 files changed

+13384
-7
lines changed

12 files changed

+13384
-7
lines changed

src/azure-cli/azure/cli/command_modules/backup/_help.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,43 @@
583583
- name: Create/Update resource guard mapping of the Recovery Services vault.
584584
text: az backup vault resource-guard-mapping update --resource-group MyResourceGroup --name MyVault --resource-guard-id MyResourceGuardId
585585
"""
586+
587+
helps['backup protection'] = """
588+
type: group
589+
short-summary: Manage protection of items in a Recovery Services vault.
590+
"""
591+
592+
helps['backup protection reconfigure'] = """
593+
type: command
594+
short-summary: Reconfigures backup protection from an old vault to a new vault.
595+
examples:
596+
- name: Reconfigure VM backup from one vault to another
597+
text: |
598+
az backup protection reconfigure \\
599+
--vault-name OldVault \\
600+
--resource-group OldVaultRG \\
601+
--container-name myVM \\
602+
--item-name myVM \\
603+
--backup-management-type AzureIaasVM \\
604+
--new-vault-name NewVault \\
605+
--new-vault-resource-group NewVaultRG \\
606+
--new-policy-name DailyPolicy \\
607+
--retain-as-per-policy
608+
- name: Reconfigure VM backup with cross-tenant MUA scenario
609+
text: |
610+
az backup protection reconfigure \\
611+
--vault-name OldVault \\
612+
--resource-group OldVaultRG \\
613+
--container-name myVM \\
614+
--item-name myVM \\
615+
--backup-management-type AzureIaasVM \\
616+
--new-vault-name NewVault \\
617+
--new-vault-resource-group NewVaultRG \\
618+
--new-policy-name DailyPolicy \\
619+
--retain-as-per-policy \\
620+
--tenant-id 12345678-1234-1234-1234-123456789012
621+
"""
622+
586623
helps['backup vault resource-guard-mapping show'] = """
587624
type: command
588625
short-summary: Get resource guard mapping of the Recovery Services vault.

src/azure-cli/azure/cli/command_modules/backup/_params.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,17 @@ def load_arguments(self, _):
326326
c.argument('workload_type', workload_type)
327327
c.argument('tenant_id', help='ID of the tenant if the Resource Guard protecting the vault exists in a different tenant.')
328328

329+
with self.argument_context('backup protection reconfigure') as c:
330+
c.argument('container_name', container_name_type, id_part='child_name_2')
331+
c.argument('item_name', item_name_type, id_part='child_name_3')
332+
c.argument('backup_management_type', backup_management_type)
333+
c.argument('workload_type', workload_type)
334+
c.argument('new_vault_name', help='Name of the destination Recovery Services vault.')
335+
c.argument('new_vault_resource_group', options_list=['--new-vault-resource-group', '--new-rg'], help='Resource group name of the destination Recovery Services vault.')
336+
c.argument('new_policy_name', options_list=['--new-policy-name'], help='Name of the backup policy in the destination vault.')
337+
c.argument('retain_as_per_policy', arg_type=get_three_state_flag(), help='Retain existing recovery points as per current backup policy when stopping protection in the source vault (the source vault is always the one specified by --vault-name/--resource-group).')
338+
c.argument('tenant_id', help='ID of the tenant if the Resource Guard protecting the source vault exists in a different tenant.')
339+
329340
with self.argument_context('backup protection check-vm') as c:
330341
c.argument('vm_id', help='ID of the virtual machine to be checked for protection.', deprecate_info=c.deprecate(redirect='--vm', hide=True))
331342

src/azure-cli/azure/cli/command_modules/backup/_validators.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,36 @@ def validate_archive_restore(recovery_point, rehydration_priority):
134134
rehydration_priority is None):
135135
raise InvalidArgumentValueError("""The selected recovery point is in archive tier, provide additional
136136
parameters of rehydration duration and rehydration priority.""")
137+
138+
139+
def validate_reconfigure_cli_parameters(source_vault_name, source_vault_resource_group, new_vault_name, new_vault_resource_group,
140+
backup_management_type, workload_type):
141+
"""Top-level CLI validation for backup reconfiguration (name / type sanity checks).
142+
143+
Note: Source vault is always the vault specified by --vault-name / --resource-group in the command context."""
144+
145+
# Ensure old and new vaults are different
146+
if (source_vault_name.lower() == new_vault_name.lower() and
147+
source_vault_resource_group.lower() == new_vault_resource_group.lower()):
148+
raise InvalidArgumentValueError("Source and destination vaults cannot be the same")
149+
150+
# Validate workload type is provided for Azure workloads
151+
if backup_management_type.lower() == 'azureworkload' and not workload_type:
152+
raise RequiredArgumentMissingError("Workload type is required for Azure workload reconfiguration")
153+
154+
# Validate incompatible parameter combinations
155+
if backup_management_type.lower() == 'azureiaasvm' and workload_type:
156+
raise MutuallyExclusiveArgumentError("Workload type should not be specified for VM backup reconfiguration")
157+
158+
159+
def validate_afs_policy_compatibility(old_policy_type, new_policy_type):
160+
"""Validate AFS policy type compatibility for reconfiguration"""
161+
162+
# AFS policy validation: cannot go from vault-based to snapshot-based
163+
if old_policy_type == 'vault' and new_policy_type == 'snapshot':
164+
raise InvalidArgumentValueError("""
165+
Cannot reconfigure from vault-based policy to snapshot-based policy for Azure File Share.
166+
This transition is not supported.""")
167+
168+
# Allow snapshot to vault transition
169+
return True

src/azure-cli/azure/cli/command_modules/backup/commands.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def load_command_table(self, _):
8585
g.custom_command('auto-disable-for-azurewl', 'disable_auto_for_azure_wl', client_factory=protection_intent_cf)
8686
g.custom_command('resume', 'resume_protection')
8787
g.custom_command('undelete', 'undelete_protection')
88+
g.custom_command('reconfigure', 'reconfigure_backup_protection', client_factory=backup_protected_items_cf)
8889

8990
with self.command_group('backup item', backup_custom_base, client_factory=protected_items_cf, exception_handler=backup_exception_handler) as g:
9091
g.show_command('show', 'show_item', client_factory=backup_protected_items_cf, table_transformer=transform_item)

src/azure-cli/azure/cli/command_modules/backup/custom.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1937,3 +1937,80 @@ def _run_client_script_for_linux(client_scripts):
19371937
def _validate_restore_disk_parameters(restore_only_osdisk, diskslist):
19381938
if restore_only_osdisk and diskslist is not None:
19391939
logger.warning("Value of diskslist parameter will be ignored as restore-only-osdisk is set to be true.")
1940+
1941+
1942+
def reconfigure_vm_protection(cmd, item, source_vault_name, source_vault_resource_group,
1943+
new_vault_name, new_vault_resource_group,
1944+
new_policy_name, retain_as_per_policy, tenant_id):
1945+
"""Workload-specific implementation: Reconfigure Azure IaaS VM protection to a new vault.
1946+
1947+
Assumes all high-level validations and item retrieval already performed in custom_base.reconfigure_backup_protection.
1948+
"""
1949+
logger.warning("(VM) Starting backup protection reconfiguration from source vault '%s' to destination vault '%s'...",
1950+
source_vault_name, new_vault_name)
1951+
1952+
# Step 1: Stop protection in old vault (retain data)
1953+
logger.warning("Step 1: Stopping protection in old vault...")
1954+
_disable_protection_in_old_vault(cmd, source_vault_resource_group, source_vault_name,
1955+
item, retain_as_per_policy, tenant_id)
1956+
1957+
# Step 2: Enable protection in new vault
1958+
logger.warning("Step 2: Enabling protection in new vault...")
1959+
enable_result = _enable_vm_protection_in_new_vault(cmd, new_vault_resource_group, new_vault_name,
1960+
item, new_policy_name)
1961+
1962+
logger.warning("(VM) Backup protection reconfiguration completed successfully.")
1963+
return enable_result
1964+
1965+
1966+
def _disable_protection_in_old_vault(cmd, vault_resource_group, vault_name, item, retain_as_per_policy, tenant_id):
1967+
"""Stop protection in the old vault"""
1968+
protected_items_client = protected_items_cf(cmd.cli_ctx)
1969+
1970+
# Use the existing disable_protection function
1971+
return disable_protection(cmd, protected_items_client, vault_resource_group, vault_name, item,
1972+
retain_as_per_policy, tenant_id)
1973+
1974+
1975+
def _enable_vm_protection_in_new_vault(cmd, vault_resource_group, vault_name, old_item, policy_name):
1976+
"""Enable VM protection in new vault"""
1977+
1978+
# Extract VM information from the protected item
1979+
vm_id = _extract_vm_id_from_protected_item(old_item)
1980+
1981+
diskslist = _extract_disk_list_from_protected_item(old_item)
1982+
1983+
# Use the existing enable_protection_for_vm function
1984+
protected_items_client = protected_items_cf(cmd.cli_ctx)
1985+
return enable_protection_for_vm(cmd, protected_items_client, vault_resource_group, vault_name,
1986+
vm_id, policy_name, diskslist)
1987+
1988+
1989+
def _extract_vm_id_from_protected_item(protected_item):
1990+
"""Extract VM resource ID from protected item"""
1991+
# The VM ID is typically in the sourceResourceId property
1992+
if hasattr(protected_item.properties, 'source_resource_id'):
1993+
return protected_item.properties.source_resource_id
1994+
1995+
# Fallback: try to extract from the virtual machine id property
1996+
if hasattr(protected_item.properties, 'virtual_machine_id'):
1997+
return protected_item.properties.virtual_machine_id
1998+
1999+
raise CLIError("Could not extract VM resource ID from protected item")
2000+
2001+
2002+
def _extract_disk_list_from_protected_item(protected_item):
2003+
"""Extract the list of protected disks from the protected item"""
2004+
if (hasattr(protected_item.properties, 'extended_info') and
2005+
protected_item.properties.extended_info and
2006+
hasattr(protected_item.properties.extended_info, 'disk_exclusion_properties') and
2007+
protected_item.properties.extended_info.disk_exclusion_properties):
2008+
2009+
disk_props = protected_item.properties.extended_info.disk_exclusion_properties
2010+
2011+
# Return the list of LUNs that were originally protected
2012+
if hasattr(disk_props, 'disk_lun_list'):
2013+
return disk_props.disk_lun_list
2014+
2015+
# If we can't extract disk info, return None to protect all disks
2016+
return None

src/azure-cli/azure/cli/command_modules/backup/custom_afs.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
from azure.cli.core.util import CLIError
1919
from azure.cli.command_modules.backup._client_factory import protection_containers_cf, protectable_containers_cf, \
2020
protection_policies_cf, backup_protection_containers_cf, backup_protectable_items_cf, \
21-
resources_cf, backup_protected_items_cf
22-
from azure.cli.core.azclierror import ArgumentUsageError
21+
resources_cf, backup_protected_items_cf, protected_items_cf
22+
from azure.cli.core.azclierror import ArgumentUsageError, ValidationError
2323

2424
from azure.mgmt.recoveryservicesbackup.activestamp import RecoveryServicesBackupClient
2525
from azure.cli.core.commands.client_factory import get_mgmt_service_client
@@ -32,6 +32,55 @@
3232
workload_type = "AzureFileShare"
3333

3434

35+
def reconfigure_afs_protection(cmd, item, source_vault_name, source_vault_rg,
36+
new_vault_name, new_vault_rg,
37+
new_policy_name, retain_as_per_policy, tenant_id):
38+
"""Reconfigure Azure File Share protection to a new vault and policy.
39+
40+
Steps:
41+
1. Disable protection (retain or stop based on flag) in source vault.
42+
2. Unregister storage account container (if no remaining protected items) from source vault.
43+
3. Ensure storage account is registered / refreshed in destination vault.
44+
4. Enable protection for the same file share name in destination vault with new policy.
45+
5. Return the newly protected item from destination vault.
46+
"""
47+
logger.warning("For Storage reconfigure protection, all backup items within the "
48+
"container must have protection disabled first.")
49+
50+
# 1. Disable in old vault (retain as per policy if requested)
51+
items_client = protected_items_cf(cmd.cli_ctx)
52+
disable_protection(cmd, items_client, source_vault_rg, source_vault_name, item,
53+
retain_as_per_policy, tenant_id)
54+
55+
# 2. Unregister container in old vault only if this was the last protected item for that storage account
56+
_maybe_unregister_storage_account(cmd, backup_protected_items_cf(cmd.cli_ctx), source_vault_rg, source_vault_name,
57+
item.properties.container_name)
58+
59+
# 3. Enable protection in destination vault - also registers storage account in destination vault
60+
new_item = enable_for_AzureFileShare(cmd, items_client, new_vault_rg, new_vault_name, item.name,
61+
item.properties.container_name, new_policy_name)
62+
return new_item
63+
64+
65+
def _maybe_unregister_storage_account(cmd, client, resource_group_name, vault_name, container_name):
66+
"""Unregister the storage account container if no more protected items exist in the source vault."""
67+
items = common.list_items(cmd, client, resource_group_name, vault_name,
68+
workload_type=workload_type, container_name=container_name,
69+
container_type=backup_management_type)
70+
remaining = [pi for pi in items if pi.properties.protection_state.lower() == 'protected']
71+
if remaining:
72+
raise ValidationError('Cannot unregister container as other items are still protected.')
73+
74+
# Attempt unregister
75+
try:
76+
containers_client = protection_containers_cf(cmd.cli_ctx)
77+
unregister_afs_container(cmd, containers_client, vault_name, resource_group_name, container_name)
78+
except Exception as ex: # pylint: disable=broad-except
79+
logger.warning('Skipping unregister workload container of container %s due to a failure: %s.'
80+
' Continuing the operation, but if the container is still registered, it may need to be '
81+
'unregistered manually for the operation to succeed.', container_name, str(ex))
82+
83+
3584
def enable_for_AzureFileShare(cmd, client, resource_group_name, vault_name, afs_name,
3685
storage_account_name, policy_name):
3786

src/azure-cli/azure/cli/command_modules/backup/custom_base.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
from azure.cli.command_modules.backup import custom
77
from azure.cli.command_modules.backup import custom_afs
88
from azure.cli.command_modules.backup import custom_help
9-
import azure.cli.command_modules.backup.custom_common as common
109
from azure.cli.command_modules.backup import custom_wl
10+
import azure.cli.command_modules.backup.custom_common as common
11+
from azure.cli.command_modules.backup._validators import validate_reconfigure_cli_parameters
1112
from azure.cli.command_modules.backup._client_factory import protection_policies_cf, backup_protected_items_cf, \
1213
backup_protection_containers_cf, backup_protectable_items_cf, registered_identities_cf, vaults_cf
1314
from azure.cli.core.azclierror import ValidationError, RequiredArgumentMissingError, InvalidArgumentValueError, \
@@ -19,6 +20,51 @@
1920
fabric_name = "Azure"
2021

2122

23+
def reconfigure_backup_protection(cmd, client, resource_group_name, vault_name, container_name, item_name,
24+
new_vault_name, new_vault_resource_group,
25+
new_policy_name, backup_management_type, workload_type=None,
26+
retain_as_per_policy=False, tenant_id=None):
27+
"""Entry point for reconfiguring backup protection across vaults.
28+
29+
This function performs common validation and dispatches to workload specific implementations
30+
in custom.py (VM), custom_afs.py (AFS) and custom_wl.py (Workload)."""
31+
32+
# Common CLI-level validation (different vaults, workload type rules etc.)
33+
validate_reconfigure_cli_parameters(vault_name, resource_group_name,
34+
new_vault_name, new_vault_resource_group,
35+
backup_management_type, workload_type)
36+
37+
# Fetch the protected item from old vault (use existing show_item logic)
38+
item = show_item(cmd, client, resource_group_name, vault_name,
39+
container_name, item_name, backup_management_type, workload_type)
40+
custom_help.validate_item(item)
41+
if isinstance(item, list): # Ambiguous friendly name
42+
raise ValidationError("Multiple items found. Please use native container and item names.")
43+
44+
# Item-level validation (state, workload specifics)
45+
from azure.mgmt.recoveryservicesbackup.activestamp.models import ProtectionState
46+
if item.properties.protection_state not in [ProtectionState.protected,
47+
ProtectionState.protection_stopped]:
48+
raise ValidationError(f"Reconfiguration only supported for items in states: Protected or "
49+
f"ProtectionStopped. Current state: "
50+
f"{item.properties.protection_state}")
51+
52+
# Dispatch by backup management type
53+
dispatch_type = backup_management_type.lower()
54+
if dispatch_type == 'azureiaasvm':
55+
return custom.reconfigure_vm_protection(cmd, item, vault_name, resource_group_name,
56+
new_vault_name, new_vault_resource_group,
57+
new_policy_name, retain_as_per_policy, tenant_id)
58+
if dispatch_type == 'azurestorage':
59+
return custom_afs.reconfigure_afs_protection(cmd, item, vault_name, resource_group_name,
60+
new_vault_name, new_vault_resource_group,
61+
new_policy_name, retain_as_per_policy, tenant_id)
62+
if dispatch_type == 'azureworkload':
63+
return custom_wl.reconfigure_wl_protection(cmd, item, vault_name, resource_group_name,
64+
new_vault_name, new_vault_resource_group,
65+
new_policy_name, workload_type, retain_as_per_policy, tenant_id)
66+
67+
2268
def show_container(cmd, client, name, resource_group_name, vault_name, backup_management_type=None,
2369
status="Registered", use_secondary_region=None):
2470
return common.show_container(cmd, client, name, resource_group_name, vault_name, backup_management_type, status,

0 commit comments

Comments
 (0)