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 @@ -302,9 +302,15 @@ function Test-ReplicationPrequisites {
throw $VmReplicationValidationMessages.HyperVIntegrationServicesNotRunning
}

# Hyper-V VMs should be highly available
if (![string]::IsNullOrEmpty($Machine.ClusterId) -and $Machine.HighAvailability -eq $HighAvailability.NO) {
throw $VmReplicationValidationMessages.VmNotHighlyAvailable
# Hyper-V VMs on cluster should be highly available
if (![string]::IsNullOrEmpty($Machine.ClusterId)) {
if ($Machine.HighAvailability -eq $HighAvailability.NO) {
throw $VmReplicationValidationMessages.VmNotHighlyAvailable
}
elseif ($Machine.HighAvailability -ne $HighAvailability.YES) {
# Unknown or unexpected value
throw $VmReplicationValidationMessages.VmUnknownHighlyAvailable
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ $PowerStatus = @{
}

$HighAvailability = @{
Unknown = "Unknown";
NO = "No";
YES = "Yes";
}
Expand All @@ -111,7 +112,8 @@ $VmReplicationValidationMessage = "Replication could not be initiated. Please en
$VmReplicationValidationMessages = @{
VmPoweredOff = "The VM is currently powered off. $VmReplicationValidationMessage";
AlreadyInReplication = "The VM is already in replication. $VmReplicationValidationMessage";
VmNotHighlyAvailable = "VM not highly available. $VmReplicationValidationMessage";
VmNotHighlyAvailable = "The VM is not highly available. $VmReplicationValidationMessage";
VmUnknownHighlyAvailable = "The VM has unknown high availability status. $VmReplicationValidationMessage";
HyperVIntegrationServicesNotRunning = "Hyper-V Integration Services are not running on VM. $VmReplicationValidationMessage";
VmWareToolsNotInstalled = "VMware Tools are not installed on the VM. To preserve static IPs during migration, install VMware Tools and wait up to 30 minutes for the system to detect the changes.";
VmWareToolsNotRunning = "VMware Tools are not running on the VM. To preserve static IPs during migration, ensure VMware Tools are running and wait up to 30 minutes for the system to detect the changes.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,6 @@ function Initialize-AzMigrateLocalReplicationInfrastructure {
Import-Module Az.Resources
Import-Module Az.Storage

$hasCacheStorageAccountId = $PSBoundParameters.ContainsKey('CacheStorageAccountId')

$parameterSetName = $PSCmdlet.ParameterSetName
$null = $PSBoundParameters.Remove('ResourceGroupName')
$null = $PSBoundParameters.Remove('ProjectName')
$null = $PSBoundParameters.Remove('CacheStorageAccountId')
Expand All @@ -143,14 +140,19 @@ function Initialize-AzMigrateLocalReplicationInfrastructure {
$null = $PSBoundParameters.Add('ErrorVariable', 'notPresent')
$null = $PSBoundParameters.Add('ErrorAction', 'SilentlyContinue')

# Get subscription Id
# Validate Azure login
$context = Get-AzContext
if ($null -eq $context -or $null -eq $context.Account) {
throw "Not logged in to Azure. Please run 'Connect-AzAccount' before running this command."
}

# Get subscription Id
if ([string]::IsNullOrEmpty($SubscriptionId)) {
Write-Host "No -SubscriptionId provided. Using the one from Get-AzContext."

$SubscriptionId = $context.Subscription.Id
if ([string]::IsNullOrEmpty($SubscriptionId)) {
throw "Please login to Azure to select a subscription."
throw "No subscription selected. Please run 'Set-AzContext -SubscriptionId <id>' or provide -SubscriptionId."
}
}
Write-Host "*Selected Subscription Id: '$($SubscriptionId)'"
Expand All @@ -165,34 +167,6 @@ function Initialize-AzMigrateLocalReplicationInfrastructure {
}
Write-Host "*Selected Resource Group: '$($ResourceGroupName)'"

# Verify user validity
$userObject = Get-AzADUser -UserPrincipalName $context.Subscription.ExtendedProperties.Account

if (-not $userObject) {
$userObject = Get-AzADUser -Mail $context.Subscription.ExtendedProperties.Account
}

if (-not $userObject) {
$mailNickname = "{0}#EXT#" -f $($context.Account.Id -replace '@', '_')

$userObject = Get-AzADUser |
Where-Object { $_.MailNickname -eq $mailNickname }
}

if (-not $userObject) {
if ($context.Account.Id.StartsWith("MSI@")) {
$hostname = $env:COMPUTERNAME
$userObject = Get-AzADServicePrincipal -DisplayName $hostname
}
else {
$userObject = Get-AzADServicePrincipal -ApplicationID $context.Account.Id
}
}

if (-not $userObject) {
throw 'User Object Id Not Found!'
}

# Get Migrate Project with ResourceGroupName, Name
$null = $PSBoundParameters.Add('ResourceGroupName', $ResourceGroupName)
$null = $PSBoundParameters.Add('Name', $ProjectName)
Expand Down Expand Up @@ -760,7 +734,7 @@ function Initialize-AzMigrateLocalReplicationInfrastructure {
-Location $params.location `
-Kind $params.kind `
-Tags $params.tags `
-AllowBlobPublicAccess $true
-AllowBlobPublicAccess $false

if ($null -ne $cacheStorageAccount -and
$null -ne $cacheStorageAccount.ProvisioningState -and
Expand Down Expand Up @@ -799,6 +773,17 @@ function Initialize-AzMigrateLocalReplicationInfrastructure {
throw "Unexpected error occurs during Cache Storage Account selection process. Please re-run this command or contact support if help needed."
}

# Validate Cache Storage Account SKU tier is Standard (not Premium)
if ($cacheStorageAccount.Sku.Tier -ne "Standard") {
throw "Cache Storage Account '$($cacheStorageAccount.StorageAccountName)' uses an unsupported SKU tier '$($cacheStorageAccount.Sku.Tier)'. Only 'Standard' tier storage accounts are supported. Please provide a Standard tier storage account."
}

# Validate public network access should not be disabled even for private endpoint
if (![string]::IsNullOrEmpty($cacheStorageAccount.PublicNetworkAccess) -and
$cacheStorageAccount.PublicNetworkAccess -eq "Disabled") {
throw "Cache Storage Account '$($cacheStorageAccount.StorageAccountName)' does not allow public network access. Please enable 'Public network access' on the storage account and re-run this command."
}
Comment on lines +776 to +785
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

New cache storage account validations (SKU tier and PublicNetworkAccess) are added here, but the existing Pester tests for this cmdlet only cover the happy path. Please add test cases that exercise these failure modes (e.g., Premium tier and PublicNetworkAccess Disabled) so the new validation behavior doesn’t regress.

Copilot uses AI. Check for mistakes.

$params = @{
contributorRoleDefId = [System.Guid]::parse($RoleDefinitionIds.ContributorId);
storageBlobDataContributorRoleDefId = [System.Guid]::parse($RoleDefinitionIds.StorageBlobDataContributorId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,6 @@ function New-AzMigrateLocalServerReplication {
CheckResourceGraphModuleDependency
CheckResourcesModuleDependency

$HasMachineId = $PSBoundParameters.ContainsKey('MachineId')
$HasTargetStoragePathId = $PSBoundParameters.ContainsKey('TargetStoragePathId')
$HasTargetResourceGroupId = $PSBoundParameters.ContainsKey('TargetResourceGroupId')
$HasTargetVMCPUCore = $PSBoundParameters.ContainsKey('TargetVMCPUCore')
$HasIsDynamicMemoryEnabled = $PSBoundParameters.ContainsKey('IsDynamicMemoryEnabled')
if ($HasIsDynamicMemoryEnabled) {
Expand Down Expand Up @@ -212,22 +209,22 @@ function New-AzMigrateLocalServerReplication {
$null = $PSBoundParameters.Add('ErrorAction', 'SilentlyContinue')

# Validate ARM ID format from inputs
if ($HasMachineId -and !(Test-AzureResourceIdFormat -Data $MachineId -Format $IdFormats.MachineArmIdTemplate))
if (!(Test-AzureResourceIdFormat -Data $MachineId -Format $IdFormats.MachineArmIdTemplate))
{
throw New-InvalidResourceIdProvidedException `
-ResourceId $MachineId `
-ResourceType "DiscoveredMachine" `
-Format $IdFormats.MachineArmIdTemplate
}

if ($HasTargetStoragePathId -and !(Test-AzureResourceIdFormat -Data $TargetStoragePathId -Format $IdFormats.StoragePathArmIdTemplate)) {
if (!(Test-AzureResourceIdFormat -Data $TargetStoragePathId -Format $IdFormats.StoragePathArmIdTemplate)) {
throw New-InvalidResourceIdProvidedException `
-ResourceId $TargetStoragePathId `
-ResourceType "StorageContainer" `
-Format $IdFormats.StoragePathArmIdTemplate
}

if ($HasTargetResourceGroupId -and !(Test-AzureResourceIdFormat -Data $TargetResourceGroupId -Format $IdFormats.ResourceGroupArmIdTemplate)) {
if (!(Test-AzureResourceIdFormat -Data $TargetResourceGroupId -Format $IdFormats.ResourceGroupArmIdTemplate)) {
throw New-InvalidResourceIdProvidedException `
-ResourceId $TargetResourceGroupId `
-ResourceType "ResourceGroup" `
Expand Down Expand Up @@ -651,6 +648,34 @@ function New-AzMigrateLocalServerReplication {
$customProperties.FabricDiscoveryMachineId = $machine.Id
$customProperties.RunAsAccountId = $runAsAccountId
$customProperties.SourceFabricAgentName = $sourceDra.Name

# Validate storage path exists and is in a usable state
$storagePath = Get-AzResource `
-ResourceId $TargetStoragePathId `
-ErrorVariable notPresent `
-ErrorAction SilentlyContinue
if ($null -eq $storagePath) {
throw "Storage path with Id '$TargetStoragePathId' not found. Please provide a valid storage path ARM ID."
}

# Creation must have succeeded for the storage path to be usable
$creationStatus = $storagePath.Properties.status.provisioningStatus.status
if ([string]::IsNullOrEmpty($creationStatus)) {
throw "Storage path '$($storagePath.Name)' creation status is unavailable. Please verify the storage path resource is fully provisioned."
}
if ($creationStatus -ne "Succeeded") {
throw "Storage path '$($storagePath.Name)' has a creation provisioning status of '$creationStatus'. Only storage paths with a successful creation can be used. Please select a different storage path or wait for provisioning to complete."
}

# The latest operation (ProvisioningState) must also be Succeeded
$provisioningState = $storagePath.Properties.provisioningState
if ([string]::IsNullOrEmpty($provisioningState)) {
throw "Storage path '$($storagePath.Name)' provisioning state is unavailable. Please verify the storage path resource is fully provisioned."
}
if ($provisioningState -ne "Succeeded") {
throw "Storage path '$($storagePath.Name)' has a provisioning state of '$provisioningState'. Only storage paths with a 'Succeeded' provisioning state can be used. Please resolve the issue or select a different storage path."
Comment on lines +652 to +676
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

Storage path health validation is newly introduced here, but the Pester tests for New-AzMigrateLocalServerReplication are currently skipped and don’t cover these new error paths. Please add/enable tests that validate behavior for (1) non-existent storage path id and (2) non-succeeded provisioning states, ideally using playback recordings/mocking.

Copilot uses AI. Check for mistakes.
}

$customProperties.StorageContainerId = $TargetStoragePathId
$customProperties.TargetArcClusterCustomLocationId = $arbArgResult.CustomLocation
$customProperties.TargetFabricAgentName = $targetDra.Name
Expand Down
6 changes: 6 additions & 0 deletions src/Migrate/Migrate/ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
- Additional information about change #1
-->
## Upcoming Release
* Fixed bugs in `Initialize-AzMigrateLocalReplicationInfrastructure`
- Added early Azure login validation with a clear error message when user is not logged in
- Removed unnecessary caller identity resolution
- Added cache storage account validations to reject unsupported SKU tiers and disabled public network access
Comment on lines +21 to +24
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

The changelog entry mentions "Removed unnecessary caller identity resolution", but the PR description calls out a bug fix for caller identity resolution specifically for Service Principal and Managed Identity. Consider updating this entry to describe the user-visible impact (e.g., initialization now works reliably when logged in with a service principal or managed identity) so release notes match the stated fix.

Copilot uses AI. Check for mistakes.
* Updated `New-AzMigrateLocalServerReplication`
- Added storage path health validation before initiating replication

## Version 2.11.0
* Updated DefaultCrashConsistentFrequencyInMinutes and DefaultAppConsistentFrequencyInMinutes to align with Azure Portal UX for Replication Policy
Expand Down
Loading