Skip to content
44 changes: 44 additions & 0 deletions src/Migrate/Migrate.Autorest/custom/Helper/AzLocalCommonHelper.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -609,4 +609,48 @@ function New-OffAzureResourceNotFoundException {
)

return "'$Scenario' '$Name' not found in resource group '$ResourceGroupName' and site '$SiteName'."
}

function Get-AzMigrateSourceSecureBootState {
[Microsoft.Azure.PowerShell.Cmdlets.Migrate.DoNotExportAttribute()]
param(
[Parameter(Mandatory)]
[string]
${MachineId}
)

# Returns $true/$false, or $null when the state cannot be determined. Callers must treat $null
# as unknown and fall through to the service rather than blocking the migration.
$uri = "{0}?api-version={1}" -f $MachineId, $ApiVersions.OffAzureMachineRead

try {
$response = Invoke-AzRestMethod -Path $uri -Method GET -ErrorAction Stop
}
catch {
Write-Verbose "Could not read Secure Boot state from '$MachineId': $($_.Exception.Message)"
return $null
}

if ($null -eq $response -or $response.StatusCode -ne 200) {
Write-Verbose "Could not read Secure Boot state from '$MachineId'. Status code: $($response.StatusCode)."
return $null
}

try {
$properties = ($response.Content | ConvertFrom-Json).properties
}
catch {
Write-Verbose "Could not parse the discovered machine response for '$MachineId'."
return $null
}

# Absent on older appliance versions and on clouds still serving the GA contract.
if ($null -eq $properties -or
'secureBootEnabled' -notin $properties.PSObject.Properties.Name -or
$null -eq $properties.secureBootEnabled) {
Write-Verbose "Discovered machine '$MachineId' does not report Secure Boot state."
return $null
}

return [bool]$properties.secureBootEnabled
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,16 @@ $ApiVersions = @{
HyperVSites = "2020-01-01";
ProtectedItem = "2021-02-16-preview";
AzLocal = "2021-09-01-preview";
# The module is generated against OffAzure 2020-01-01, which has no secureBootEnabled on its
# machine models. Read that one field at the newer version instead of regenerating, since
# every OffAzure operationId was renamed after 2020-01-01.
OffAzureMachineRead = "2024-12-01-preview";
}

# Role definition GUIDs for storage account creation
$RoleDefinitionIds = @{
ContributorId = "b24988ac-6180-42a0-ab88-20f7382dd24c";
Comment thread
anhdinh-msft marked this conversation as resolved.
StorageAccountContributorId = "17d1049b-9a84-46fb-8f53-869881c3d3ab";
StorageBlobDataContributorId = "ba92f5b4-2d11-453d-a403-e96b0029c9fe";
}

Expand Down Expand Up @@ -108,6 +113,20 @@ $OsTypes = @{
OtherGuestFamily = "otherguestfamily";
}

# User-facing security types on the Local replication cmdlets.
$TargetVMSecurityTypes = @{
Standard = "Standard";
TrustedLaunch = "TrustedLaunch";
}

# Wire values for the service 'securityOption' field. 'EnablevTPM' is omitted deliberately:
# it means vTPM without Secure Boot, which the service always rejects (error 2109020).
$SecurityOptions = @{
None = "None";
SecureBootEnabled = "SecureBootEnabled";
TrustedLaunch = "TrustedLaunch";
}

$VmReplicationValidationMessage = "Replication could not be initiated. Please ensure the necessary changes are made, and allow up to 30 minutes before re-trying."
$VmReplicationValidationMessages = @{
VmPoweredOff = "The VM is currently powered off. $VmReplicationValidationMessage";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -785,7 +785,7 @@ function Initialize-AzMigrateLocalReplicationInfrastructure {
}

$params = @{
contributorRoleDefId = [System.Guid]::parse($RoleDefinitionIds.ContributorId);
storageAccountContributorRoleDefId = [System.Guid]::parse($RoleDefinitionIds.StorageAccountContributorId);
storageBlobDataContributorRoleDefId = [System.Guid]::parse($RoleDefinitionIds.StorageBlobDataContributorId);
sourceAppAadId = $sourceDra.Property.ResourceAccessIdentity.ObjectId;
targetAppAadId = $targetDra.Property.ResourceAccessIdentity.ObjectId;
Expand All @@ -796,17 +796,17 @@ function Initialize-AzMigrateLocalReplicationInfrastructure {
{
$params.vaultIdentityAadId = $replicationVault.IdentityPrincipalId

# Grant vault Identity Aad access to Cache Storage Account as "Contributor"
# Grant vault Identity Aad access to Cache Storage Account as "Storage Account Contributor"
$hasAadAppAccess = Get-AzRoleAssignment `
-ObjectId $params.vaultIdentityAadId `
-RoleDefinitionId $params.contributorRoleDefId `
-RoleDefinitionId $params.storageAccountContributorRoleDefId `
-Scope $cacheStorageAccount.Id `
-ErrorVariable notPresent `
-ErrorAction SilentlyContinue
if ($null -eq $hasAadAppAccess) {
New-AzRoleAssignment `
-ObjectId $params.vaultIdentityAadId `
-RoleDefinitionId $params.contributorRoleDefId `
-RoleDefinitionId $params.storageAccountContributorRoleDefId `
-Scope $cacheStorageAccount.Id | Out-Null
}

Expand All @@ -825,17 +825,17 @@ function Initialize-AzMigrateLocalReplicationInfrastructure {
}
}

# Grant Source Dra AAD App access to Cache Storage Account as "Contributor"
# Grant Source Dra AAD App access to Cache Storage Account as "Storage Account Contributor"
$hasAadAppAccess = Get-AzRoleAssignment `
-ObjectId $params.sourceAppAadId `
-RoleDefinitionId $params.contributorRoleDefId `
-RoleDefinitionId $params.storageAccountContributorRoleDefId `
-Scope $cacheStorageAccount.Id `
-ErrorVariable notPresent `
-ErrorAction SilentlyContinue
if ($null -eq $hasAadAppAccess) {
New-AzRoleAssignment `
-ObjectId $params.sourceAppAadId `
-RoleDefinitionId $params.contributorRoleDefId `
-RoleDefinitionId $params.storageAccountContributorRoleDefId `
-Scope $cacheStorageAccount.Id | Out-Null
}

Expand All @@ -853,17 +853,17 @@ function Initialize-AzMigrateLocalReplicationInfrastructure {
-Scope $cacheStorageAccount.Id | Out-Null
}

# Grant Target Dra AAD App access to Cache Storage Account as "Contributor"
# Grant Target Dra AAD App access to Cache Storage Account as "Storage Account Contributor"
$hasAadAppAccess = Get-AzRoleAssignment `
-ObjectId $params.targetAppAadId `
-RoleDefinitionId $params.contributorRoleDefId `
-RoleDefinitionId $params.storageAccountContributorRoleDefId `
-Scope $cacheStorageAccount.Id `
-ErrorVariable notPresent `
-ErrorAction SilentlyContinue
if ($null -eq $hasAadAppAccess) {
New-AzRoleAssignment `
-ObjectId $params.targetAppAadId `
-RoleDefinitionId $params.contributorRoleDefId `
-RoleDefinitionId $params.storageAccountContributorRoleDefId `
-Scope $cacheStorageAccount.Id | Out-Null
}

Expand All @@ -884,10 +884,10 @@ function Initialize-AzMigrateLocalReplicationInfrastructure {
# Give time for role assignments to be created. Times out after 2min
$rsaPermissionGranted = $false
for ($i = 0; $i -lt 3; $i++) {
# Check Source Dra AAD App access to Cache Storage Account as "Contributor"
# Check Source Dra AAD App access to Cache Storage Account as "Storage Account Contributor"
$hasAadAppAccess = Get-AzRoleAssignment `
-ObjectId $params.sourceAppAadId `
-RoleDefinitionId $params.contributorRoleDefId `
-RoleDefinitionId $params.storageAccountContributorRoleDefId `
-Scope $cacheStorageAccount.Id `
-ErrorVariable notPresent `
-ErrorAction SilentlyContinue
Expand All @@ -902,10 +902,10 @@ function Initialize-AzMigrateLocalReplicationInfrastructure {
-ErrorAction SilentlyContinue
$rsaPermissionGranted = $rsaPermissionGranted -and ($null -ne $hasAadAppAccess)

# Check Target Dra AAD App access to Cache Storage Account as "Contributor"
# Check Target Dra AAD App access to Cache Storage Account as "Storage Account Contributor"
$hasAadAppAccess = Get-AzRoleAssignment `
-ObjectId $params.targetAppAadId `
-RoleDefinitionId $params.contributorRoleDefId `
-RoleDefinitionId $params.storageAccountContributorRoleDefId `
-Scope $cacheStorageAccount.Id `
-ErrorVariable notPresent `
-ErrorAction SilentlyContinue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,22 @@ function New-AzMigrateLocalServerReplication {
# Specifies whether to migrate the server as an Azure Arc-enabled VM. When set to 'true', an Azure Arc-enabled machine resource with the same name as -TargetVMName must already exist in the resource group specified by -TargetResourceGroupId.
${MigrateAsArcVM},

[Parameter()]
[ValidateSet("Standard", "TrustedLaunch")]
Comment thread
anhdinh-msft marked this conversation as resolved.
[ArgumentCompleter( { "Standard", "TrustedLaunch" })]
[Microsoft.Azure.PowerShell.Cmdlets.Migrate.Category('Path')]
[System.String]
# Specifies the security type of the target VM. 'TrustedLaunch' enables Secure Boot and vTPM, and implies -EnableSecureBoot 'true'. Only supported for Generation 2 target VMs.
${TargetVMSecurityOption},

[Parameter()]
[ValidateSet("true" , "false")]
[ArgumentCompleter( { "true" , "false" })]
[Microsoft.Azure.PowerShell.Cmdlets.Migrate.Category('Path')]
[System.String]
# Specifies whether Secure Boot is enabled on the target VM. Only supported for Generation 2 target VMs. When omitted, the target VM inherits the Secure Boot setting of the source server.
${EnableSecureBoot},

[Parameter()]
[Microsoft.Azure.PowerShell.Cmdlets.Migrate.Category('Path')]
[System.Int64]
Expand Down Expand Up @@ -182,6 +198,18 @@ function New-AzMigrateLocalServerReplication {
$helperPath = [System.IO.Path]::Combine($PSScriptRoot, "Helper", "AzLocalCommonHelper.ps1")
Import-Module $helperPath

$HasTargetVMSecurityOption = $PSBoundParameters.ContainsKey('TargetVMSecurityOption')
$HasEnableSecureBoot = $PSBoundParameters.ContainsKey('EnableSecureBoot')
if ($HasEnableSecureBoot) {
$secureBootEnabled = [System.Convert]::ToBoolean($EnableSecureBoot)
}

# Purely a contradiction between parameters, so reject it before the module and service checks.
if ($HasTargetVMSecurityOption -and $TargetVMSecurityOption -eq $TargetVMSecurityTypes.TrustedLaunch -and
$HasEnableSecureBoot -and -not $secureBootEnabled) {
throw "-EnableSecureBoot 'false' cannot be used with -TargetVMSecurityOption 'TrustedLaunch'. Trusted Launch requires Secure Boot."
}

CheckResourceGraphModuleDependency
CheckResourcesModuleDependency

Expand All @@ -208,6 +236,8 @@ function New-AzMigrateLocalServerReplication {
$null = $PSBoundParameters.Remove('TargetTestVirtualSwitchId')
$null = $PSBoundParameters.Remove('IsDynamicMemoryEnabled')
$null = $PSBoundParameters.Remove('MigrateAsArcVM')
$null = $PSBoundParameters.Remove('TargetVMSecurityOption')
$null = $PSBoundParameters.Remove('EnableSecureBoot')
$null = $PSBoundParameters.Remove('TargetVMRam')
$null = $PSBoundParameters.Remove('DiskToInclude')
$null = $PSBoundParameters.Remove('NicToInclude')
Expand Down Expand Up @@ -732,6 +762,39 @@ function New-AzMigrateLocalServerReplication {
$customProperties.HyperVGeneration = if ($machine.Firmware -ieq "BIOS") { "1" } else { "2" }
}

# Gen 1 target VMs do not support Secure Boot or vTPM; fail before the service round-trip.
if ($HasTargetVMSecurityOption -or $HasEnableSecureBoot) {
$securityType = if ($HasTargetVMSecurityOption) { $TargetVMSecurityOption } else { $TargetVMSecurityTypes.Standard }

# For VMware sources -MachineName is an opaque id, so report the discovered name.
$sourceName = if ([string]::IsNullOrEmpty($machine.DisplayName)) { $MachineName } else { $machine.DisplayName }

if ($securityType -eq $TargetVMSecurityTypes.TrustedLaunch) {
$secureBootEnabled = $true
}

if ($customProperties.HyperVGeneration -eq "1" -and
($securityType -eq $TargetVMSecurityTypes.TrustedLaunch -or $secureBootEnabled)) {
throw "Secure Boot and Trusted Launch require a Generation 2 target VM. The source server '$sourceName' maps to a Generation 1 target VM."
}

# Only send securityOption once a choice is expressed. '-TargetVMSecurityOption Standard'
# on its own is not a choice about Secure Boot, so the target keeps inheriting the source.
if ($securityType -eq $TargetVMSecurityTypes.TrustedLaunch) {
$customProperties.SecurityOption = $SecurityOptions.TrustedLaunch
}
Comment thread
anhdinh-msft marked this conversation as resolved.
elseif ($HasEnableSecureBoot) {
if (-not $secureBootEnabled -and $customProperties.HyperVGeneration -eq "2") {
# The service rejects turning Secure Boot off for a Gen 2 source that has it on.
if ($true -eq (Get-AzMigrateSourceSecureBootState -MachineId $MachineId)) {
throw "Source server '$sourceName' has Secure Boot enabled, so it cannot be migrated with -EnableSecureBoot 'false'. Omit -EnableSecureBoot to keep Secure Boot enabled on the target VM, or disable Secure Boot on the source server first."
}
}

$customProperties.SecurityOption = if ($secureBootEnabled) { $SecurityOptions.SecureBootEnabled } else { $SecurityOptions.None }
}
}

# Validate TargetVMCPUCore
if ($HasTargetVMCPUCore)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,22 @@ function Set-AzMigrateLocalServerReplication {
# Specifies the OS type of the VM, either WindowsGuest or LinuxGuest.
${OsType},

[Parameter()]
[ValidateSet("Standard", "TrustedLaunch")]
[ArgumentCompleter( { "Standard", "TrustedLaunch" })]
[Microsoft.Azure.PowerShell.Cmdlets.Migrate.Category('Path')]
[System.String]
# Specifies the security type of the target VM. 'TrustedLaunch' enables Secure Boot and vTPM, and implies -EnableSecureBoot 'true'. Only supported for Generation 2 target VMs.
${TargetVMSecurityOption},

[Parameter()]
[ValidateSet("true" , "false")]
[ArgumentCompleter( { "true" , "false" })]
[Microsoft.Azure.PowerShell.Cmdlets.Migrate.Category('Path')]
[System.String]
# Specifies whether Secure Boot is enabled on the target VM. Only supported for Generation 2 target VMs.
${EnableSecureBoot},

[Parameter()]
[Microsoft.Azure.PowerShell.Cmdlets.Migrate.Category('Path')]
[Microsoft.Azure.PowerShell.Cmdlets.Migrate.Runtime.DefaultInfo(Script = '(Get-AzContext).Subscription.Id')]
Expand Down Expand Up @@ -133,6 +149,18 @@ function Set-AzMigrateLocalServerReplication {
$helperPath = [System.IO.Path]::Combine($PSScriptRoot, "Helper", "AzLocalCommonHelper.ps1")
Import-Module $helperPath

$HasTargetVMSecurityOption = $PSBoundParameters.ContainsKey('TargetVMSecurityOption')
$HasEnableSecureBoot = $PSBoundParameters.ContainsKey('EnableSecureBoot')
if ($HasEnableSecureBoot) {
$secureBootEnabled = [System.Convert]::ToBoolean($EnableSecureBoot)
}

# Purely a contradiction between parameters, so reject it before the module and service checks.
if ($HasTargetVMSecurityOption -and $TargetVMSecurityOption -eq $TargetVMSecurityTypes.TrustedLaunch -and
$HasEnableSecureBoot -and -not $secureBootEnabled) {
throw "-EnableSecureBoot 'false' cannot be used with -TargetVMSecurityOption 'TrustedLaunch'. Trusted Launch requires Secure Boot."
}

CheckResourcesModuleDependency

$HasTargetObjectId = $PSBoundParameters.ContainsKey('TargetObjectID')
Expand All @@ -153,6 +181,8 @@ function Set-AzMigrateLocalServerReplication {
$null = $PSBoundParameters.Remove('NicToInclude')
$null = $PSBoundParameters.Remove('TargetObjectID')
$null = $PSBoundParameters.Remove('OsType')
$null = $PSBoundParameters.Remove('TargetVMSecurityOption')
$null = $PSBoundParameters.Remove('EnableSecureBoot')
$null = $PSBoundParameters.Remove('WhatIf')
$null = $PSBoundParameters.Remove('Confirm')

Expand Down Expand Up @@ -209,6 +239,40 @@ function Set-AzMigrateLocalServerReplication {
$customPropertiesUpdate.InstanceType = $AzLocalInstanceTypes.VMwareToAzLocal
}

# Gen 1 target VMs do not support Secure Boot or vTPM; fail before the service round-trip.
if ($HasTargetVMSecurityOption -or $HasEnableSecureBoot) {
$securityType = if ($HasTargetVMSecurityOption) { $TargetVMSecurityOption } else { $TargetVMSecurityTypes.Standard }

# Trusted Launch always includes Secure Boot; moving to Standard drops vTPM but keeps it, matching the portal.
if ($securityType -eq $TargetVMSecurityTypes.TrustedLaunch -or -not $HasEnableSecureBoot) {
$secureBootEnabled = $true
}

if ($customProperties.HyperVGeneration -eq "1") {
# Only an explicit request is an error. The inherit-Secure-Boot default above is a
# Gen 2 convention, so on Gen 1 it resolves to None instead of being rejected.
if ($securityType -eq $TargetVMSecurityTypes.TrustedLaunch -or ($HasEnableSecureBoot -and $secureBootEnabled)) {
throw "Secure Boot and Trusted Launch require a Generation 2 target VM. Protected item '$TargetObjectID' has a Generation 1 target VM."
}

$secureBootEnabled = $false
}

if (-not $secureBootEnabled -and $customProperties.HyperVGeneration -eq "2") {
# The service rejects turning Secure Boot off for a Gen 2 source that has it on.
if ($true -eq (Get-AzMigrateSourceSecureBootState -MachineId $customProperties.FabricDiscoveryMachineId)) {
# For VMware sources $MachineName is an opaque id, so report the discovered name.
$sourceName = if ([string]::IsNullOrEmpty($customProperties.SourceVMName)) { $MachineName } else { $customProperties.SourceVMName }
throw "Source server '$sourceName' has Secure Boot enabled, so it cannot be migrated with -EnableSecureBoot 'false'. Omit -EnableSecureBoot to keep Secure Boot enabled on the target VM, or disable Secure Boot on the source server first."
}
}

$customPropertiesUpdate.SecurityOption =
if ($securityType -eq $TargetVMSecurityTypes.TrustedLaunch) { $SecurityOptions.TrustedLaunch }
elseif ($secureBootEnabled) { $SecurityOptions.SecureBootEnabled }
else { $SecurityOptions.None }
Comment thread
anhdinh-msft marked this conversation as resolved.
}

# Update target CPU core
if ($HasTargetVMCPUCore) {
if ($TargetVMCPUCore -lt $TargetVMCPUCores.Min -or $TargetVMCPUCore -gt $TargetVMCPUCores.Max)
Expand Down
Loading
Loading