diff --git a/src/Migrate/Migrate.Autorest/custom/Helper/AzLocalCommonHelper.ps1 b/src/Migrate/Migrate.Autorest/custom/Helper/AzLocalCommonHelper.ps1 index a0a89fb4a4dd..2f92fe054ea7 100644 --- a/src/Migrate/Migrate.Autorest/custom/Helper/AzLocalCommonHelper.ps1 +++ b/src/Migrate/Migrate.Autorest/custom/Helper/AzLocalCommonHelper.ps1 @@ -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 + } } } diff --git a/src/Migrate/Migrate.Autorest/custom/Helper/AzLocalCommonSettings.ps1 b/src/Migrate/Migrate.Autorest/custom/Helper/AzLocalCommonSettings.ps1 index a0abcb0d4b2d..9e5ed52200cc 100644 --- a/src/Migrate/Migrate.Autorest/custom/Helper/AzLocalCommonSettings.ps1 +++ b/src/Migrate/Migrate.Autorest/custom/Helper/AzLocalCommonSettings.ps1 @@ -91,6 +91,7 @@ $PowerStatus = @{ } $HighAvailability = @{ + Unknown = "Unknown"; NO = "No"; YES = "Yes"; } @@ -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."; diff --git a/src/Migrate/Migrate.Autorest/custom/Initialize-AzMigrateLocalReplicationInfrastructure.ps1 b/src/Migrate/Migrate.Autorest/custom/Initialize-AzMigrateLocalReplicationInfrastructure.ps1 index 9eedb0cde11b..bd502536fc80 100644 --- a/src/Migrate/Migrate.Autorest/custom/Initialize-AzMigrateLocalReplicationInfrastructure.ps1 +++ b/src/Migrate/Migrate.Autorest/custom/Initialize-AzMigrateLocalReplicationInfrastructure.ps1 @@ -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') @@ -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 ' or provide -SubscriptionId." } } Write-Host "*Selected Subscription Id: '$($SubscriptionId)'" @@ -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) @@ -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 @@ -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." + } + $params = @{ contributorRoleDefId = [System.Guid]::parse($RoleDefinitionIds.ContributorId); storageBlobDataContributorRoleDefId = [System.Guid]::parse($RoleDefinitionIds.StorageBlobDataContributorId); diff --git a/src/Migrate/Migrate.Autorest/custom/New-AzMigrateLocalServerReplication.ps1 b/src/Migrate/Migrate.Autorest/custom/New-AzMigrateLocalServerReplication.ps1 index 1269524c6a1b..ee632e5da62e 100644 --- a/src/Migrate/Migrate.Autorest/custom/New-AzMigrateLocalServerReplication.ps1 +++ b/src/Migrate/Migrate.Autorest/custom/New-AzMigrateLocalServerReplication.ps1 @@ -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) { @@ -212,7 +209,7 @@ 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 ` @@ -220,14 +217,14 @@ function New-AzMigrateLocalServerReplication { -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" ` @@ -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." + } + $customProperties.StorageContainerId = $TargetStoragePathId $customProperties.TargetArcClusterCustomLocationId = $arbArgResult.CustomLocation $customProperties.TargetFabricAgentName = $targetDra.Name diff --git a/src/Migrate/Migrate/ChangeLog.md b/src/Migrate/Migrate/ChangeLog.md index 8242405db2ca..d11ecfeb0275 100644 --- a/src/Migrate/Migrate/ChangeLog.md +++ b/src/Migrate/Migrate/ChangeLog.md @@ -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 +* 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