Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 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 @@ -276,6 +276,13 @@
--vault-name myRecoveryServicesVault \\
--policy-name DefaultPolicy \\
--vm "$(az vm show -g VMResourceGroup -n MyVm --query id)"
- name: Start protecting an Azure VM that resides in a different subscription than the Recovery Services vault (Cross Subscription Backup). Pass the complete ARM ID of the VM to the --vm parameter so its subscription, resource group and name are derived from it.
text: |
az backup protection enable-for-vm \\
--resource-group myResourceGroup \\
--vault-name myRecoveryServicesVault \\
--policy-name DefaultPolicy \\
--vm "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/VMResourceGroup/providers/Microsoft.Compute/virtualMachines/MyVm"
"""

helps['backup protection enable-for-azurefileshare'] = """
Expand Down Expand Up @@ -417,10 +424,26 @@
helps['backup restore restore-disks'] = """
type: command
short-summary: Restore disks of the backed VM from the specified recovery point.
long-summary: >
For a Cross Subscription Backup (CSB) protected item, pass the native container name and item name
(the 'name' field from 'az backup container list' and 'az backup item list') instead of the friendly
name. This is also required when two VMs with the same friendly name but in different resource groups
are protected in the same vault - passing the friendly name in that case fails with a clear error.
examples:
- name: Restore disks of the backed VM from the specified recovery point. (autogenerated)
text: az backup restore restore-disks --container-name MyContainer --item-name MyItem --resource-group MyResourceGroup --rp-name MyRp --storage-account mystorageaccount --vault-name MyVault
crafted: true
- name: Restore disks of a Cross Subscription Backup protected VM to its original location (OLR). Use OriginalLocation restore mode and pass native container and item names. No target subscription is required - it is derived from the recovery point, and the staging storage account is resolved in the VM's subscription.
text: |
az backup restore restore-disks \\
--resource-group MyVaultResourceGroup \\
--vault-name MyVault \\
--container-name "IaasVMContainer;iaasvmcontainerv2;VMResourceGroup;MyVm" \\
--item-name "VM;iaasvmcontainerv2;VMResourceGroup;MyVm" \\
--rp-name MyRp \\
--restore-mode OriginalLocation \\
--storage-account mystorageaccount \\
--storage-account-resource-group MyStorageAccountResourceGroup
"""

helps['backup restore restore-azurefileshare'] = """
Expand Down
115 changes: 85 additions & 30 deletions src/azure-cli/azure/cli/command_modules/backup/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -914,10 +914,6 @@ def enable_protection_for_vm(cmd, client, resource_group_name, vault_name, vm, p
disk_list_setting=None, exclude_all_data_disks=None):
from ..vm.operations.vm import VMShow
vm_name, vm_rg = cust_help.get_resource_name_and_rg(resource_group_name, vm)
vm = VMShow(cli_ctx=cmd.cli_ctx)(command_args={
'resource_group': vm_rg,
'vm_name': vm_name
})
vault = vaults_cf(cmd.cli_ctx).get(resource_group_name, vault_name)
policy = show_policy(protection_policies_cf(cmd.cli_ctx), resource_group_name, vault_name, policy_name)

Expand All @@ -928,39 +924,72 @@ def enable_protection_for_vm(cmd, client, resource_group_name, vault_name, vm, p
if policy.properties.protected_items_count >= 1000:
raise CLIError("Cannot configure backup for more than 1000 VMs per policy")

if vm.get('location', '').lower() != vault.location.lower():
raise CLIError(
"""
The VM should be in the same location as that of the Recovery Services vault to enable protection.
""")

if policy.properties.backup_management_type != BackupManagementType.azure_iaas_vm.value:
raise CLIError(
"""
The policy type should match with the workload being protected.
Use the relevant get-default policy command and use it to protect the workload.
""")

# Get protectable item.
protectable_item = _get_protectable_item_for_vm(cmd.cli_ctx, vault_name, resource_group_name, vm_name, vm_rg)
if protectable_item is None:
raise CLIError(
"""
The specified Azure Virtual Machine Not Found. Possible causes are
1. VM does not exist
2. The VM name or the Service name needs to be case sensitive
3. VM is already Protected with same or other Vault.
Please Unprotect VM first and then try to protect it again.
# Cross Subscription Backup (CSB): when the VM is specified as a full ARM id that resides in a
# subscription different from the vault's, discovery (RefreshContainers/ListProtectableItems) - which
# only operates on the vault's subscription - cannot find the VM. In this case we skip discovery and
# construct the container uri, protected item uri and source resource id directly from the VM ARM id.
# The backend derives the VM's subscription from the source resource id and validates region/existence.
vault_subscription_id = get_subscription_id(cmd.cli_ctx)
vm_subscription_id = cust_help.get_subscription_from_id(vm) if is_valid_resource_id(vm) else None
is_cross_subscription = (vm_subscription_id is not None and
vm_subscription_id.lower() != vault_subscription_id.lower())

if is_cross_subscription:
# Validate the cross-subscription VM exists and resides in the same region as the vault.
# Discovery cannot run cross-subscription, so we fetch the VM directly in its own subscription
# (same idiom used for cross-sub vnet/storage lookups) to fail fast on a wrong ARM id or a
# region mismatch before calling the backend.
vm_resource = _get_vm_resource(cmd.cli_ctx, vm_subscription_id, vm_rg, vm_name)
if vm_resource.location.lower() != vault.location.lower():
raise CLIError(
"""
The VM should be in the same location as that of the Recovery Services vault to enable protection.
""")

Please contact Microsoft for further assistance.
""")
container_uri = "IaasVMContainer;iaasvmcontainerv2;{};{}".format(vm_rg, vm_name)
item_uri = "vm;iaasvmcontainerv2;{};{}".format(vm_rg, vm_name)
vm_item_properties = _get_vm_item_properties_from_vm_id(vm)
vm_item_properties.policy_id = policy.id
vm_item_properties.source_resource_id = vm
else:
vm = VMShow(cli_ctx=cmd.cli_ctx)(command_args={
'resource_group': vm_rg,
'vm_name': vm_name
})

# Construct enable protection request object
container_uri = cust_help.get_protection_container_uri_from_id(protectable_item.id)
item_uri = cust_help.get_protectable_item_uri_from_id(protectable_item.id)
vm_item_properties = _get_vm_item_properties_from_vm_type(vm['type'])
vm_item_properties.policy_id = policy.id
vm_item_properties.source_resource_id = protectable_item.properties.virtual_machine_id
if vm.get('location', '').lower() != vault.location.lower():
raise CLIError(
"""
The VM should be in the same location as that of the Recovery Services vault to enable protection.
""")

# Get protectable item.
protectable_item = _get_protectable_item_for_vm(cmd.cli_ctx, vault_name, resource_group_name, vm_name, vm_rg)
if protectable_item is None:
raise CLIError(
"""
The specified Azure Virtual Machine Not Found. Possible causes are
1. VM does not exist
2. The VM name or the Service name needs to be case sensitive
3. VM is already Protected with same or other Vault.
Please Unprotect VM first and then try to protect it again.

Please contact Microsoft for further assistance.
""")

# Construct enable protection request object
container_uri = cust_help.get_protection_container_uri_from_id(protectable_item.id)
item_uri = cust_help.get_protectable_item_uri_from_id(protectable_item.id)
vm_item_properties = _get_vm_item_properties_from_vm_type(vm['type'])
vm_item_properties.policy_id = policy.id
vm_item_properties.source_resource_id = protectable_item.properties.virtual_machine_id

if disk_list_setting is not None and exclude_all_data_disks is not None:
raise MutuallyExclusiveArgumentError("""
Expand Down Expand Up @@ -1061,6 +1090,10 @@ def list_items(cmd, client, resource_group_name, vault_name, container_name=None
container_name, resource_group_name, vault_name,
container_type)
cust_help.validate_container(container)
if isinstance(container, list):
raise ValidationError("Multiple containers with same Friendly Name found. Please provide native "
"names instead. Native name can be obtained from the 'name' field in the "
"output of 'az backup container list'.")
container_uri = container.name

return [item for item in paged_items if
Expand Down Expand Up @@ -1474,6 +1507,16 @@ def restore_disks(cmd, client, resource_group_name, vault_name, container_name,
item_name, "AzureIaasVM", "VM", use_secondary_region)
cust_help.validate_item(item)

if isinstance(item, list):
raise ValidationError("Found multiple backup items. Please provide native names instead.")

# For Original Location Recovery (OLR) of a Cross Subscription Backup protected item, the disks are
# restored to the VM's original subscription, which may differ from the vault's subscription. Derive
# the container (VM) subscription from the protected item's sourceResourceId so that the target storage
# account is resolved in the correct subscription. No additional input is required from the customer.
if (restore_mode == "OriginalLocation" and item.properties.source_resource_id is not None):
target_subscription = cust_help.get_subscription_from_id(item.properties.source_resource_id)

recovery_point = show_recovery_point(cmd, recovery_points_cf(cmd.cli_ctx), resource_group_name, vault_name,
container_name, item_name, rp_name, "AzureIaasVM", "VM", use_secondary_region)

Expand Down Expand Up @@ -1724,9 +1767,9 @@ def show_job(cmd, client, resource_group_name, vault_name, name, use_secondary_r
azure_region = secondary_region_map[vault_location]
client = backup_crr_job_details_cf(cmd.cli_ctx)
response = client.get(azure_region, CrrJobRequest(resource_id=vault.id, job_name=name))
return cust_help.replace_min_value_in_subtask(response)
return cust_help.set_job_container_subscription_id(cust_help.replace_min_value_in_subtask(response))
response = client.get(vault_name, resource_group_name, name)
return cust_help.replace_min_value_in_subtask(response)
return cust_help.set_job_container_subscription_id(cust_help.replace_min_value_in_subtask(response))
Comment thread
Prabhkiratnitp marked this conversation as resolved.


def stop_job(client, resource_group_name, vault_name, name, use_secondary_region=None):
Expand Down Expand Up @@ -1839,6 +1882,18 @@ def _get_crr_access_token(cmd, azure_region, vault_name, resource_group_name, co
return crr_access_token


def _get_vm_resource(cli_ctx, vm_subscription, vm_resource_group, vm_name):
resources_client = get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES,
subscription_id=vm_subscription).resources
vm_resource_namespace = 'Microsoft.Compute'
parent_resource_path = 'virtualMachines'
resource_type = ''
api_version = '2023-03-01'

return resources_client.get(vm_resource_group, vm_resource_namespace, parent_resource_path, resource_type,
vm_name, api_version)


def _get_vnet_object(cli_ctx, vnet_subscription, vnet_name, vnet_resource_group):
resources_client = get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES,
subscription_id=vnet_subscription).resources
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ def show_item(cmd, client, resource_group_name, vault_name, container_name, name
else:
if custom_help.is_native_name(name) and custom_help.is_native_name(container_name):
client = protected_items_cf(cmd.cli_ctx)
return client.get(vault_name, resource_group_name, fabric_name, container_name, name)
item = client.get(vault_name, resource_group_name, fabric_name, container_name, name)
return custom_help.set_container_subscription_id(item)

items = list_items(cmd, client, resource_group_name, vault_name, workload_type, container_name,
container_type, use_secondary_region)
Expand Down Expand Up @@ -158,6 +159,8 @@ def list_items(cmd, client, resource_group_name, vault_name, workload_type=None,
client = backup_protected_items_crr_cf(cmd.cli_ctx)
items = client.list(vault_name, resource_group_name, filter_string)
paged_items = custom_help.get_list_from_paged_response(items)
for item in paged_items:
custom_help.set_container_subscription_id(item)

if container_name:
if custom_help.is_native_name(container_name):
Expand Down
30 changes: 30 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 @@ -668,6 +668,36 @@ def get_subscription_from_id(arm_id):
return m.group(0)


def set_container_subscription_id(item):
# For an Azure VM backup item, surface the subscription of the protected VM (container) in the
# response as 'containerSubscriptionId'. It is parsed from the item's sourceResourceId, which for a
# Cross Subscription Backup item points to a subscription different from the vault's subscription.
if item is None or not hasattr(item, 'properties'):
return item
properties = item.properties
backup_management_type = getattr(properties, 'backup_management_type', None)
source_resource_id = getattr(properties, 'source_resource_id', None)
if (backup_management_type is not None and backup_management_type.lower() == 'azureiaasvm' and
source_resource_id):
properties.container_subscription_id = get_subscription_from_id(source_resource_id)
return item


def set_job_container_subscription_id(job):
# For an Azure VM backup/restore job, surface the subscription of the protected VM (container) in the
# response as 'containerSubscriptionId'. It is read from the job's extendedInfo property bag, which
# contains the "VM Subscription ID" for Cross Subscription Backup jobs.
if job is None or not hasattr(job, 'properties'):
return job
extended_info = getattr(job.properties, 'extended_info', None)
if extended_info is None:
return job
property_bag = getattr(extended_info, 'property_bag', None)
if property_bag and 'VM Subscription ID' in property_bag:
job.properties.container_subscription_id = property_bag['VM Subscription ID']
return job


def get_operation_id_from_header(header):
parse_object = urlparse(header)
return parse_object.path.split("/")[-1]
Expand Down
Loading
Loading