Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,11 @@ private static CmdletModel.AzureVmJob GetPSAzureVmJob(JobResource serviceClientJ
{
detailedResponse.Properties.Add(key, vmJob.ExtendedInfo.PropertyBag[key]);
}

if (vmJob.ExtendedInfo.PropertyBag.ContainsKey("VM Subscription ID"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: extract the "VM Subscription ID" propertyBag key into a named const — it's duplicated here and in GetPSAzureVmJobCrr and will drift.

{
detailedResponse.ContainerSubscriptionId = vmJob.ExtendedInfo.PropertyBag["VM Subscription ID"];
}
}

if (vmJob.ExtendedInfo.TasksList != null)
Expand Down Expand Up @@ -875,6 +880,11 @@ private static CmdletModel.AzureVmJob GetPSAzureVmJobCrr(CrrModel.JobResource se
{
detailedResponse.Properties.Add(key, vmJob.ExtendedInfo.PropertyBag[key]);
}

if (vmJob.ExtendedInfo.PropertyBag.ContainsKey("VM Subscription ID"))
{
detailedResponse.ContainerSubscriptionId = vmJob.ExtendedInfo.PropertyBag["VM Subscription ID"];
}
}

if (vmJob.ExtendedInfo.TasksList != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// ----------------------------------------------------------------------------------

using System.Collections.Generic;
using Microsoft.Azure.Management.Internal.Resources.Utilities.Models;
using Microsoft.Azure.Management.RecoveryServices.Backup.Models;
using CrrModel = Microsoft.Azure.Management.RecoveryServices.Backup.CrossRegionRestore.Models;

Expand All @@ -25,6 +26,8 @@ public class AzureVmItem : AzureItem
{
public string VirtualMachineId { get; set; }

public string ContainerSubscriptionId { get; set; }

public string HealthStatus { get; set; }

public bool? IsInclusionList { get; set; }
Expand All @@ -51,6 +54,13 @@ public AzureVmItem(ProtectedItemResource protectedItemResource,
ProtectionStatus = EnumUtils.GetEnum<ItemProtectionStatus>(protectedItem.ProtectionStatus);
VirtualMachineId = protectedItem.VirtualMachineId;
HealthStatus = protectedItem.HealthStatus;

if (!string.IsNullOrEmpty(SourceResourceId))
{
var resourceIdentifier = new ResourceIdentifier(SourceResourceId);
ContainerSubscriptionId = resourceIdentifier.Subscription;
}

IsArchiveEnabled = protectedItem.IsArchiveEnabled;
SoftDeleteRetentionPeriodInDays = protectedItem.SoftDeleteRetentionPeriodInDays;
IsScheduledForDeferredDelete = protectedItem.IsScheduledForDeferredDelete;
Expand Down Expand Up @@ -94,6 +104,13 @@ public AzureVmItem(CrrModel.ProtectedItemResource protectedItemResource,
ProtectionStatus = EnumUtils.GetEnum<ItemProtectionStatus>(protectedItem.ProtectionStatus);
VirtualMachineId = protectedItem.VirtualMachineId;
HealthStatus = protectedItem.HealthStatus;

if (!string.IsNullOrEmpty(SourceResourceId))
{
var resourceIdentifier = new ResourceIdentifier(SourceResourceId);
ContainerSubscriptionId = resourceIdentifier.Subscription;
}

DateOfPurge = null;
DeleteState = EnumUtils.GetEnum<ItemDeleteState>("NotDeleted");
if (protectedItem.IsScheduledForDeferredDelete.HasValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ public class AzureVmJobDetails : AzureVmJob
/// </summary>
public string DynamicErrorMessage { get; set; }

/// <summary>
/// Subscription ID of the VM associated with this job.
/// </summary>
public string ContainerSubscriptionId { get; set; }

/// <summary>
/// Property bag consisting of the some Azure VM specific job details.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ public enum ItemParams
InclusionDisksList,
ExclusionDisksList,
ResetExclusionSettings,
ExcludeAllDataDisks
ExcludeAllDataDisks,
ContainerSubscriptionId
}

public enum ProtectionCheckParams
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public RestAzureNS.AzureOperationResponse<ProtectedItemResource> EnableProtectio
string[] exclusionDisksList = ProviderData.ContainsKey(ItemParams.ExclusionDisksList) ? (string[])ProviderData[ItemParams.ExclusionDisksList] : null;
SwitchParameter resetDiskExclusionSetting = ProviderData.ContainsKey(ItemParams.ResetExclusionSettings) ? (SwitchParameter)ProviderData[ItemParams.ResetExclusionSettings] : new SwitchParameter(false);
bool excludeAllDataDisks = ProviderData.ContainsKey(ItemParams.ExcludeAllDataDisks) ? (bool)ProviderData[ItemParams.ExcludeAllDataDisks] : false;
string containerSubscriptionId = ProviderData.ContainsKey(ItemParams.ContainerSubscriptionId) ? (string)ProviderData[ItemParams.ContainerSubscriptionId] : null;
PolicyBase policy = (PolicyBase)ProviderData[ItemParams.Policy];
ItemBase itemBase = ProviderData.ContainsKey(ItemParams.Item) ? (ItemBase)ProviderData[ItemParams.Item] : null;
AzureVmItem item = ProviderData.ContainsKey(ItemParams.Item) ? (AzureVmItem)ProviderData[ItemParams.Item] : null;
Expand Down Expand Up @@ -141,27 +142,48 @@ public RestAzureNS.AzureOperationResponse<ProtectedItemResource> EnableProtectio
azureVMResourceGroupName,
policy);

WorkloadProtectableItemResource protectableObjectResource =
GetAzureVMProtectableObject(
azureVMName,
azureVMRGName,
isComputeAzureVM,
vaultName: vaultName,
resourceGroupName: resourceGroupName);

Dictionary<UriEnums, string> keyValueDict =
HelperUtils.ParseUri(protectableObjectResource.Id);
containerUri = HelperUtils.GetContainerUri(
keyValueDict, protectableObjectResource.Id);
protectedItemUri = HelperUtils.GetProtectableItemUri(
keyValueDict, protectableObjectResource.Id);

IaaSVMProtectableItem iaasVmProtectableItem =
(IaaSVMProtectableItem)protectableObjectResource.Properties;

if (iaasVmProtectableItem != null)
if (!string.IsNullOrEmpty(containerSubscriptionId))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: on the CSB path we skip discovery entirely, so a typo in -Name/-ResourceGroupName or an ineligible VM no longer surfaces the clear "VM not discovered" client error and instead fails opaquely at the backend — is a lightweight existence/eligibility check (or at least a doc note) worth adding?

{
sourceResourceId = iaasVmProtectableItem.VirtualMachineId;
// Cross Subscription Backup (CSB): the VM resides in a different subscription than the vault.
// Discovery (RefreshContainers/ListProtectableItems) only operates on the vault's subscription
// and will not find the cross-subscription VM. Hence, we skip discovery and construct the
// container URI, protected item URI and source resource ID directly. The backend derives the
// VM's subscription from the source resource ID and configures backup accordingly.
string vmVersion = isComputeAzureVM ? computeAzureVMVersion : classicComputeAzureVMVersion;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-ContainerSubscriptionId is only in AzureVMComputeParameterSet, so isComputeAzureVM is always true here — the classic (iaasvmcontainer / ClassicCompute) branch of these ternaries can never execute; consider dropping it or intentionally supporting classic.

string containerType = isComputeAzureVM ? "iaasvmcontainerv2" : "iaasvmcontainer";

containerUri = string.Format("IaasVMContainer;{0};{1};{2}",
containerType, azureVMRGName, azureVMName);
protectedItemUri = string.Format("VM;{0};{1};{2}",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the rest of the module emits lowercase "vm;" (via GetProtectableItemUri); "VM;" works (ARM is case-insensitive) but is inconsistent.

containerType, azureVMRGName, azureVMName);
sourceResourceId = string.Format(
"/subscriptions/{0}/resourceGroups/{1}/providers/{2}/virtualMachines/{3}",
containerSubscriptionId, azureVMRGName, vmVersion, azureVMName);
}
else
{
WorkloadProtectableItemResource protectableObjectResource =
GetAzureVMProtectableObject(
azureVMName,
azureVMRGName,
isComputeAzureVM,
vaultName: vaultName,
resourceGroupName: resourceGroupName);

Dictionary<UriEnums, string> keyValueDict =
HelperUtils.ParseUri(protectableObjectResource.Id);
containerUri = HelperUtils.GetContainerUri(
keyValueDict, protectableObjectResource.Id);
protectedItemUri = HelperUtils.GetProtectableItemUri(
keyValueDict, protectableObjectResource.Id);

IaaSVMProtectableItem iaasVmProtectableItem =
(IaaSVMProtectableItem)protectableObjectResource.Properties;

if (iaasVmProtectableItem != null)
{
sourceResourceId = iaasVmProtectableItem.VirtualMachineId;
}
}
}
else if(isDiskExclusionParamPresent && parameterSetName.Contains("Modify"))
Expand Down Expand Up @@ -521,7 +543,23 @@ public RestAzureNS.AzureOperationResponse TriggerRestore()
string cVMOsDiskEncryptionSetId = ProviderData.ContainsKey(RestoreVMBackupItemParams.CVMOsDiskEncryptionSetId) ?
ProviderData[RestoreVMBackupItemParams.CVMOsDiskEncryptionSetId].ToString() : null;

if (targetSubscriptionId == null || targetSubscriptionId == "") targetSubscriptionId = ServiceClientAdapter.SubscriptionId;
if (targetSubscriptionId == null || targetSubscriptionId == "")
{
// For Original Location Recovery (OLR) of a cross-subscription (CSB) protected item, the disks
// are restored to the VM's original subscription, which may differ from the vault's subscription.
// Extract the container (VM) subscription from the recovery point's SourceResourceId so that the
// target storage account is resolved in the correct subscription. No new input is required from
// the customer. For all other cases, fall back to the vault's (context) subscription.
if (string.Compare(restoreType, "OriginalLocation") == 0 && !string.IsNullOrEmpty(rp.SourceResourceId))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: deriving targetSubscriptionId from rp.SourceResourceId for OLR (so staging storage resolves in the VM's subscription) is the right fix and matches the existing restoreType convention.

{
Dictionary<UriEnums, string> sourceResourceUriDict = HelperUtils.ParseUri(rp.SourceResourceId);
targetSubscriptionId = HelperUtils.GetSubscriptionIdFromId(sourceResourceUriDict, rp.SourceResourceId);
}
else
{
targetSubscriptionId = ServiceClientAdapter.SubscriptionId;
}
}

GenericResource storageAccountResource = ServiceClientAdapter.GetStorageAccountResource(storageAccountName, targetSubscriptionId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,5 +363,29 @@ public void TestAzureVMRedoProtection()
"Test-AzureVMRedoProtection"
);
}

[Fact]
[Trait(Category.AcceptanceType, Category.CheckIn)]
[Trait(TestConstants.Workload, TestConstants.AzureVM)]
public void TestAzureVMCSBProtection()
{
TestRunner.RunTestScript(
$"Import-Module {_IaasVmcommonModule.AsAbsoluteLocation()}",
$"Import-Module {_IaasVmtestModule.AsAbsoluteLocation()}",
"Test-AzureVMCSBProtection"
);
}

[Fact]
[Trait(Category.AcceptanceType, Category.CheckIn)]
[Trait(TestConstants.Workload, TestConstants.AzureVM)]
public void TestAzureVMCSBRestoreOLR()
{
TestRunner.RunTestScript(
$"Import-Module {_IaasVmcommonModule.AsAbsoluteLocation()}",
$"Import-Module {_IaasVmtestModule.AsAbsoluteLocation()}",
"Test-AzureVMCSBRestoreOLR"
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1409,3 +1409,151 @@ function Test-AzureRestoreWithCVMOsDiskEncryptionSetId()
Delete-VM $targetRG $targetVMName
}
}

function Test-AzureVMCSBProtection
{
# Cross Subscription Backup (CSB): the vault and the VM to be protected live in
# different subscriptions. These values point to the recording environment.
$resourceGroupName = "singhprab-csb-vault-rg-ea"
$vaultName = "singhprab-csb-vault-ea"
# The VM (container) resides in a subscription different from the vault's subscription.
$vmName = "singhprab-ps-vm3"
$vmResourceGroupName = "singhprab-rg-1c"
$containerSubscriptionId = "80abcfe3-b410-42b2-983f-df23cba781dc"
# Policy used to initially protect the VM, and the policy to switch to via modify protection.
$policyName = "singhprab"
$modifyPolicyName = "singhprab2"

try
{
# Setup
$vault = Get-AzRecoveryServicesVault -ResourceGroupName $resourceGroupName -Name $vaultName

$policy = Get-AzRecoveryServicesBackupProtectionPolicy `
-VaultId $vault.ID `
-Name $policyName;

# Sleep to give the service time to add the policy to the vault
Start-TestSleep -Seconds 5

# Enable protection for a cross-subscription VM using the new -ContainerSubscriptionId parameter.
# This path skips discovery/RefreshContainers and constructs the request directly.
Enable-AzRecoveryServicesBackupProtection `
-VaultId $vault.ID `
-Policy $policy `
-Name $vmName `
-ResourceGroupName $vmResourceGroupName `
-ContainerSubscriptionId $containerSubscriptionId;

$item = Get-AzRecoveryServicesBackupItem `
-BackupManagementType AzureVM -WorkloadType AzureVM -VaultId $vault.ID `
| Where-Object { $_.Name -match $vmName }

Assert-NotNull $item;

# The source resource id (and hence the item) must belong to the VM's subscription, not the vault's.
Assert-True { $item.SourceResourceId -match $vmName };
Assert-True { $item.SourceResourceId -match $vmResourceGroupName };
Assert-True { $item.ContainerSubscriptionId -eq $containerSubscriptionId };

# The item must be protected with the initial policy.
Assert-True { $item.ProtectionPolicyName -eq $policyName };

# Modify protection: switch the already-protected CSB item to a different policy.
# This uses the ModifyProtection parameter set (-Item), which derives the container
# from the item's ARM id and works for cross-subscription items without extra input.
$modifyPolicy = Get-AzRecoveryServicesBackupProtectionPolicy `
-VaultId $vault.ID `
-Name $modifyPolicyName;

Enable-AzRecoveryServicesBackupProtection `
-VaultId $vault.ID `
-Policy $modifyPolicy `
-Item $item;

$modifiedItem = Get-AzRecoveryServicesBackupItem `
-BackupManagementType AzureVM -WorkloadType AzureVM -VaultId $vault.ID `
| Where-Object { $_.Name -match $vmName }

Assert-NotNull $modifiedItem;

# The item must now be associated with the new policy, and remain a CSB item.
Assert-True { $modifiedItem.ProtectionPolicyName -eq $modifyPolicyName };
Assert-True { $modifiedItem.ContainerSubscriptionId -eq $containerSubscriptionId };
}
finally
{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This finally is empty but the test enables protection — file convention (e.g. Test-AzureRestoreWithCVMOsDiskEncryptionSetId) disables/deletes in finally; please clean up the protected item.

# Cleanup
}
}

function Test-AzureVMCSBRestoreOLR
{
# Original Location Recovery (OLR) for a Cross Subscription Backup protected item.
# No target subscription/resource group input is required - the VM's subscription is derived
# from the recovery point's SourceResourceId, and the staging storage account is resolved in
# that subscription. This fetches an existing recovery point (no fresh backup is triggered).
# These values point to the recording environment.
$resourceGroupName = "singhprab-csb-vault-rg-ea"
$vaultName = "singhprab-csb-vault-ea"
$vmName = "singhprab-csb-vm-ea"
# Staging storage account (resides in the VM's subscription).
$saName = "asmaskasaea"
$saResourceGroupName = "asmaskarrg"

try
{
# Setup
$vault = Get-AzRecoveryServicesVault -ResourceGroupName $resourceGroupName -Name $vaultName

$item = Get-AzRecoveryServicesBackupItem `
-BackupManagementType AzureVM -WorkloadType AzureVM -VaultId $vault.ID `
| Where-Object { $_.Name -match $vmName }

Assert-NotNull $item;

# Fetch an existing recovery point for the CSB item (no new backup is triggered).
# Use Get-QueryDateInUtc so the date window is recorded and replayed deterministically.
$from = Get-QueryDateInUtc $((Get-Date).AddDays(-2)) "CSBOlrFrom"
$to = Get-QueryDateInUtc $(Get-Date) "CSBOlrTo"
# Restrict to instant-restore capable tiers (Snapshot or SnapshotAndVaultStandard) so OLR can
# leverage the snapshot instead of a vault-tier restore.
$rps = @(Get-AzRecoveryServicesBackupRecoveryPoint `
-VaultId $vault.ID `
-Item $item `
-StartDate $from `
-EndDate $to `
| Where-Object { $_.RecoveryPointTier -eq "Snapshot" })

Assert-True { $rps.Count -gt 0 };
$rp = $rps[0]

# The chosen recovery point must be a Snapshot tier recovery point.
Assert-True { $rp.RecoveryPointTier -eq "Snapshot" };

# The recovery point should belong to the VM's (cross) subscription.
Assert-True { $rp.SourceResourceId -match $item.ContainerSubscriptionId };

# Trigger OLR (Replace Existing): no -TargetResourceGroupName and no -TargetSubscriptionId.
# The storage account must be resolved in the VM's subscription (derived from rp.SourceResourceId).
# Capture the restore job first, then wait separately. Piping Restore directly into
# Wait-AzRecoveryServicesBackupJob causes the downstream cmdlet's BeginProcessing to reset the
# shared Logger.Instance, which breaks WriteDebug calls made during Restore's ProcessRecord.
$restoreJob = Restore-AzRecoveryServicesBackupItem `
-VaultId $vault.ID `
-VaultLocation $vault.Location `
-RecoveryPoint $rp `
-StorageAccountName $saName `
-StorageAccountResourceGroupName $saResourceGroupName

$restoreJob = $restoreJob | Wait-AzRecoveryServicesBackupJob -VaultId $vault.ID

Assert-True { $restoreJob.Status -eq "Completed" };
}
finally
{
# Cleanup
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,17 @@ public void TestAzureVMCancelJob()
"Test-AzureVMCancelJob"
);
}

[Fact]
[Trait(Category.AcceptanceType, Category.CheckIn)]
[Trait(TestConstants.Workload, TestConstants.AzureVM)]
public void TestAzureVMCSBJobSubscription()
{
TestRunner.RunTestScript(
$"Import-Module {_IaasVmcommonModule.AsAbsoluteLocation()}",
$"Import-Module {_IaasVmtestModule.AsAbsoluteLocation()}",
"Test-AzureVMCSBJobSubscription"
);
}
}
}
Loading