<# .SYNOPSIS This script migrates VM images from a source Azure environment to a destination CloudLabs Azure environment. .COPYRIGHT Copyright 2024, Spektra Systems LLC. All rights reserved. .DESCRIPTION The script takes several parameters including the source resource group name, source image gallery name, source subscription ID, VM images list, CloudLabs tenant ID, CloudLabs subscription ID, CloudLabs application ID, CloudLabs application secret, and CloudLabs resource group. It performs the following steps: 1. Initializes logging. 2. Requires AZ module to function 3. Connects to the source Azure environment and retrieves the tenant ID. 4. Connects to the destination Azure environment using the provided application ID, secret, and tenant ID. 5. Retrieves the source image gallery context. 6. Creates temporary managed disks from the latest image versions in the source image gallery. 7. Creates a temporary storage account in the destination Azure environment. 8. Creates a new image gallery in the destination Azure environment. 9. Starts the disk copying operation from the source Azure environment to the destination Azure environment. 10. Monitors the copy status of the images. 11. Creates managed disks from the copied blobs and then creates respective images 12. Deletes the temporary disk in source environment .PARAMETER sourceResourceGroupName The name of the source resource group. .PARAMETER sourceImageGalleryName The name of the source image gallery. .PARAMETER sourceSubscriptionId The ID of the source subscription. .PARAMETER VMImagesList An array of VM image names to be migrated. .PARAMETER cloudLabsTenantId The tenant ID of the destination Azure environment. Your CloudLabs Account Manager will provide these values. .PARAMETER cloudLabsSubscriptionId The subscription ID of the destination Azure environment. Your CloudLabs Account Manager will provide these values. .PARAMETER cloudLabsApplicationId The application ID of the destination Azure environment. Your CloudLabs Account Manager will provide these values. .PARAMETER cloudLabsApplicationSecret The application secret of the destination Azure environment. Your CloudLabs Account Manager will provide these values. .PARAMETER cloudLabsResourceGroup The resource group name of the destination Azure environment. Your CloudLabs Account Manager will provide these values. .LATEST VERSION OF SCRIPT Download and save the latest version of this PowerShell script from this URL: https://cloudlabs.ai/alb-imagemigration-script.You can also use the Invoke-WebRequest cmdlet to download the script and save it to a local folder, for example: Invoke-WebRequest -Uri https://cloudlabs.ai/alb-imagemigration-script -OutFile C:\MigrateVMImagestoCloudLabs.ps1 .EXAMPLE .\MigrateVMImagestoCloudLabs.ps1 -sourceResourceGroupName "SourceRG" -sourceImageGalleryName "SourceGallery" -sourceSubscriptionId "12345678-1234-1234-1234-1234567890AB" -VMImagesList @("Image1", "Image2") -cloudLabsTenantId "98765432-4321-4321-4321-0987654321BA" -cloudLabsSubscriptionId "ABCD1234-5678-5678-5678-ABCD1234ABCD" -cloudLabsApplicationId "12345678-1234-1234-1234-1234567890AB" -cloudLabsApplicationSecret (ConvertTo-SecureString -String "yoursecret" -AsPlainText -Force) -cloudLabsResourceGroup "DestinationRG" .NOTES Author: Spektra Systems LLC Date: 05/20/2024 Version: 1.0. .SUPPORT Please contact cloudlabs-support@spektrasystems.com incase of any questions. #> [CmdletBinding()] ## Requires -Modules Az Param ( [Parameter(Mandatory = $false, HelpMessage = "The name of the resource group in source Azure environment that contains the image gallery.")] [ValidateNotNullOrEmpty()] [string] $sourceResourceGroupName, [Parameter(Mandatory = $true, HelpMessage = "The name of the image gallery in the source Azure environment that contains the images to be replicated.")] [ValidateNotNullOrEmpty()] [string] $sourceImageGalleryName, [Parameter(Mandatory = $true, HelpMessage = "The subscription ID of the source Azure environment.")] [ValidateNotNullOrEmpty()] [string] $sourceSubscriptionId, [Parameter(Mandatory = $true, HelpMessage = "A comma-separated list of the names of the images to be migrated, enclosed in double quotes")] [ValidateNotNullOrEmpty()] [string[]] $VMImagesList, [Parameter(Mandatory = $true, HelpMessage = "Provided by CloudLabs Team")] [ValidateNotNullOrEmpty()] [string] $cloudLabsTenantId, [Parameter(Mandatory = $true, HelpMessage = "Provided by CloudLabs Team")] [ValidateNotNullOrEmpty()] [string] $cloudLabsSubscriptionId, [Parameter(Mandatory = $true, HelpMessage = "Provided by CloudLabs Team")] [ValidateNotNullOrEmpty()] [string] $cloudLabsApplicationId, [Parameter(Mandatory = $true, HelpMessage = "Provided by CloudLabs Team")] [ValidateNotNullOrEmpty()] [System.Security.SecureString] $cloudLabsApplicationSecret, [Parameter(Mandatory = $true, HelpMessage = "Provided by CloudLabs Team")] [ValidateNotNullOrEmpty()] [string] $cloudLabsResourceGroup ) $ErrorActionPreference = "Stop" # Initialize Logging function Initialize-Logging { param ( [string] $LogDirectory = (Get-Location).Path ) $logFileName = "CloudLabsVMImageMigration_ScriptLog_{0:yyyyMMdd_hhmmss}.txt" -f (Get-Date) $logPath = Join-Path $LogDirectory $logFileName Start-Transcript -Path $logPath -Append Write-Host "Logging initialized. Log file: $logFileName" } # Connect to Source Azure Environment and retrieve the tenant ID dynamically function Connect-ToSourceAzure { param ( [Parameter(Mandatory = $true)] [string] $sourceSubscriptionId ) try { $subscription = Get-AzSubscription -SubscriptionId $sourceSubscriptionId -ErrorAction $ErrorActionPreference Set-AzContext -SubscriptionId $sourceSubscriptionId -ErrorAction $ErrorActionPreference Write-Host "Connected to source Azure context: $($subscription.Name) ($sourceSubscriptionId)" } catch { Write-Host "Error: Unable to set the context to the specified subscription ($sourceSubscriptionId). Please ensure you have logged in and have access to this subscription." throw $_ } } function Connect-ToDestinationAzure { param ( [Parameter(Mandatory = $true)] [string] $cloudLabsApplicationId, [Parameter(Mandatory = $true)] [System.Security.SecureString] $cloudLabsApplicationSecret, [Parameter(Mandatory = $true)] [string] $cloudLabsTenantId, [Parameter(Mandatory = $true)] [string] $cloudLabsSubscriptionId ) try { $psCredential = New-Object System.Management.Automation.PSCredential($cloudLabsApplicationId, $cloudLabsApplicationSecret) $global:destContext = Connect-AzAccount -ServicePrincipal -Credential $psCredential -Tenant $cloudLabsTenantId -SubscriptionId $cloudLabsSubscriptionId -ErrorAction $ErrorActionPreference Write-Host "Connected to destination CloudLabs Azure context." Set-AzContext -SubscriptionId $cloudLabsSubscriptionId -ErrorAction $ErrorActionPreference } catch { Write-Host "Error connecting to destination Azure. Contact support if required: $_" throw $_ } } function Get-SourceImageGallery { param ( [Parameter(Mandatory = $true)] [string] $sourceResourceGroupName, [Parameter(Mandatory = $true)] [string] $sourceImageGalleryName ) try { $global:sourceImageGalleryContext = Get-AzGallery -ResourceGroupName $sourceResourceGroupName -Name $sourceImageGalleryName -ErrorAction $ErrorActionPreference Write-Host "Retrieved source image gallery context." } catch { Write-Host "Error retrieving source image gallery: $_" throw $_ } } # Create temporary managed disks from latest image versions function New-DisksAndGenerateSAS { param ( [Parameter(Mandatory = $true)] [string] $sourceResourceGroupName, [Parameter(Mandatory = $true)] [string] $sourceImageGalleryName, [Parameter(Mandatory = $true)] [string[]] $VMImagesList ) $sourceImageDetails = @{} Write-Host "Starting to create temporary disks now" foreach ($imageName in $VMImagesList) { try { # Retrieve the latest image definition and version $latestImage = Get-AzGalleryImageDefinition -ResourceGroupName $sourceResourceGroupName ` -GalleryName $sourceImageGalleryName -GalleryImageDefinitionName $imageName $latestImageVersion = Get-AzGalleryImageVersion -ResourceGroupName $sourceResourceGroupName ` -GalleryName $sourceImageGalleryName -GalleryImageDefinitionName $imageName | Select-Object -Last 1 if ($latestImage) { $randomString = -join ((65..90) + (97..122) | Get-Random -Count 8 | ForEach-Object { [char]$_ }) $diskName = "${imageName}_Disk_$randomString" $imageReference = [Microsoft.Azure.Management.Compute.Models.ImageDiskReference]@{ Id = $latestImage.Id } Write-Host "Found VM Image $imageName in gallery, creating disk now" $diskConfig = New-AzDiskConfig -Location $latestImage.Location -CreateOption FromImage -GalleryImageReference $imageReference -DiskSizeGB $latestImageVersion.StorageProfile.OsDiskImage.SizeInGB -AccountType StandardSSD_LRS -OsType $latestImage.OsType # Retrieve and apply the security type from the image features $securityTypeFeature = $latestImage.Features | Where-Object { $_.Name -eq 'SecurityType' } | Select-Object -ExpandProperty Value switch ($securityTypeFeature) { 'TrustedLaunch' { $diskConfig.SecurityProfile = @{ SecurityType = "TrustedLaunch" } } 'ConfidentialVM' { $diskConfig.SecurityProfile = @{ SecurityType = "ConfidentialVM_VMGuestStateOnlyEncryptedWithPlatformKey" } } } $disk = New-AzDisk -ResourceGroupName $sourceResourceGroupName -DiskName $diskName -Disk $diskConfig Write-Host "Created Disk, will create SAS token now" $sasExpiryinSeconds = 172800 # 172800 seconds = 48 hours $sasToken = Grant-AzDiskAccess -ResourceGroupName $sourceResourceGroupName -DiskName $disk.Name -DurationInSecond $sasExpiryinSeconds -Access Read Write-Host "Created SAS Token, storing this information for further use" $sourceImageDetails[$imageName] = @{ ImageDefinition = $latestImage; DiskUri = $disk.Id; # Initially setting DiskUri to source disk, which will be updated post-copy SasToken = $sasToken.AccessSAS SourceImageResourceID = $latestImage.Id SourceDiskName = $disk.Name # Store the source disk name here SecurityType = $securityTypeFeature } Write-Host "Managed disk and SAS token created for image $imageName with version $($latestImage.Name)" } else { Write-Host "No available versions found for image $imageName" } } catch { Write-Host "Error processing image ${imageName}: $_" throw $_ } } return $sourceImageDetails } function New-DestinationTemporaryStorageAccount { param ( [Parameter(Mandatory = $true)] [string] $cloudLabsResourceGroup, [Parameter(Mandatory = $true)] [string] $sourceImageGalleryLocation ) $ErrorActionPreference = "Stop" try { Write-Host "Creating Storage Account now, in CloudLabs destination Azure Environment" # Generate a unique storage account name $storageAccountName = "tempstoragemigration" + (Get-Random -Maximum 9999).ToString("0000") # Create the storage account $storageAccount = New-AzStorageAccount -ResourceGroupName $cloudLabsResourceGroup ` -Name $storageAccountName -Location $sourceImageGalleryLocation -SkuName "Standard_LRS" # Create the container in the storage account $containerName = "tempimagevhdstorage" $context = $storageAccount.Context $container = New-AzStorageContainer -Name $containerName -Context $context -Permission Off Write-Host "Created Storage Account named $storageAccountName in CloudLabs destination Azure Environment" return @{ "Account" = $storageAccount; "Container" = $container } } catch { Write-Host "Error creating storage account or container: $_" throw $_ } } function Create-NewImageGallery { param ( [Parameter(Mandatory = $true)] [string] $sourceImageGalleryName, [Parameter(Mandatory = $true)] [string] $cloudLabsResourceGroup, [Parameter(Mandatory = $true)] [string] $sourceImageGalleryLocation ) $ErrorActionPreference = "Stop" Write-Host "Checking for existing Azure Compute Gallery in CloudLabs destination Azure Environment" $galleryName = "$($sourceImageGalleryName)_migrated" try { # Attempt to get the existing gallery $gallery = Get-AzGallery -GalleryName $galleryName -ResourceGroupName $cloudLabsResourceGroup -ErrorAction SilentlyContinue if ($gallery) { Write-Host "Existing image gallery named $galleryName found at location $sourceImageGalleryLocation." } else { Write-Host "No existing gallery found. Creating new Azure Compute Gallery." $gallery = New-AzGallery -ResourceGroupName $cloudLabsResourceGroup -GalleryName $galleryName -Location $sourceImageGalleryLocation Write-Host "Created new image gallery named $galleryName at location $sourceImageGalleryLocation." } } catch { Write-Host "Error checking or creating image gallery: $_" throw $_ } return $gallery } function Start-ImageDiskCopy { param ( [Parameter(Mandatory = $true)] [ref] $sourceImageDetails, [Parameter(Mandatory = $true)] [object] $storageInfo ) $ErrorActionPreference = "Stop" Write-Host "Starting disk copying operation from source Azure environment to destination CloudLabs Azure environment." foreach ($imageName in $sourceImageDetails.Value.keys) { try { $sasUri = $sourceImageDetails.Value[$imageName].SasToken Write-Host "Getting SAS Token for $imageName" if (-not $sasUri) { Write-Host "SAS token for $imageName is null, skipping copy." continue } $destinationBlobName = "DiskFromGalleryImage-$imageName.vhd" $blobCopy = Start-AzStorageBlobCopy -AbsoluteUri $sasUri -DestContainer $storageInfo.Container.Name -DestBlob $destinationBlobName -DestContext $storageInfo.Account.Context -Force Write-Host "Started Copy operation for $imageName, destinationBlobName $destinationBlobName" $destinationStorageContext = $storageInfo.Account.Context $destinationContainerName = $storageInfo.Container.Name $destinationBlobUri = "$($destinationStorageContext.BlobEndPoint)$destinationContainerName/$destinationBlobName" $sourceImageDetails.Value[$imageName].DiskUri = $destinationBlobUri } catch { Write-Host "Error during copy operation for image ${imageName}: $_" throw $_ } } } function Monitor-CopyStatus { param ( [Parameter(Mandatory = $true)] [hashtable] $sourceImageDetails, [Parameter(Mandatory = $true)] [object] $storageInfo ) $ErrorActionPreference = "Stop" $allCopiesCompleted = $false Write-Host "Checking for latest copy operation status" while (-not $allCopiesCompleted) { $allCopiesCompleted = $true # Assume all copies are completed until proven otherwise foreach ($imageName in $sourceImageDetails.Keys) { try { $blobName = "DiskFromGalleryImage-$imageName.vhd" $status = Get-AzStorageBlobCopyState -Blob $blobName -Container $storageInfo.Container.Name -Context $storageInfo.Account.Context -WaitForComplete:$false Write-Host "Copy status for ${imageName}: $($status.Status) - $($status.BytesCopied)/$($status.TotalBytes)" if ($status.Status -eq 'Pending') { $allCopiesCompleted = $false } } catch { Write-Host "Error checking copy status for ${imageName}: $_" throw $_ } } if (-not $allCopiesCompleted) { Write-Host "Not all copies are completed. Waiting for 2 minutes before checking again." Start-Sleep -Seconds 120 # Sleep for 2 minutes } } Write-Host "All image copies have completed successfully." } function Create-ManagedDisksFromBlobs { param ( [Parameter(Mandatory = $true)] [ref] $sourceImageDetails, [Parameter(Mandatory = $true)] [object] $storageInfo, [Parameter(Mandatory = $true)] [string] $cloudLabsResourceGroup, [Parameter(Mandatory = $true)] [string] $sourceImageGalleryLocation ) $ErrorActionPreference = "Stop" Write-Host "Creating temporary managed disks in CloudLabs Azure environment" foreach ($imageName in $sourceImageDetails.Value.Keys) { try { $blobUri = $sourceImageDetails.Value[$imageName].DiskUri $securityTypeForDisk = $sourceImageDetails.Value[$imageName].SecurityType # Retrieve SecurityType # Check if $securityTypeForDisk is empty and set it to 'Empty' if true if ([string]::IsNullOrEmpty($securityTypeForDisk)) { $securityTypeForDisk = 'Empty' } # Determine CreateOption based on SecurityType $createOption = switch ($securityTypeForDisk) { 'TrustedLaunch' { 'Import' } 'Empty' { 'Import' } 'ConfidentialVM' { 'ImportSecure' } default { 'Import' } } $diskConfig = New-AzDiskConfig -Location $sourceImageGalleryLocation -SourceUri $blobUri -StorageAccountId $storageInfo.Account.id -HyperVGeneration V2 -CreateOption $createOption $randomString = -join ((65..90) + (97..122) | Get-Random -Count 8 | ForEach-Object { [char]$_ }) $diskName = "${imageName}_OSDisk_$randomString" switch ($securityTypeForDisk) { 'TrustedLaunch' { $diskConfig.SecurityProfile = @{ SecurityType = "TrustedLaunch" } } 'ConfidentialVM' { $diskConfig.SecurityProfile = @{ SecurityType = "ConfidentialVM_VMGuestStateOnlyEncryptedWithPlatformKey" } } } $destDisk = New-AzDisk -Disk $diskConfig -DiskName $diskName -ResourceGroupName $cloudLabsResourceGroup $osDiskID = $destDisk.Id Write-Host "Created temporary managed disk for ${imageName}: $osDiskID" $sourceImageDetails.Value[$imageName].DestinationDiskID = $osDiskID $sourceImageDetails.Value[$imageName].DestinationDiskName = $diskName } catch { Write-Host "Error creating managed disk for ${imageName}: $_" throw $_ } } } function Create-NewImageDefinitions { param ( [Parameter(Mandatory = $true)] [hashtable] $sourceImageDetails, [Parameter(Mandatory = $true)] [object] $gallery ) $ErrorActionPreference = "Stop" Write-Host "Creating or updating image definitions and image versions from managed disks in CloudLabs Azure environment." foreach ($imageName in $sourceImageDetails.Keys) { try { $imageDefDetails = $sourceImageDetails[$imageName].ImageDefinition $versionName = "1.0.0" # Retrieve the security type from the source image details $securityType = $sourceImageDetails[$imageName].SecurityType # If the security type is empty, default to "Standard" if (-not $securityType) { $securityType = "None" } # Set features based on the security type $Feature1 = @{ Name = 'SecurityType'; Value = $securityType } $Features = @($Feature1) # Try to retrieve an existing gallery image definition $existingDef = Get-AzGalleryImageDefinition -GalleryName $gallery.Name -ResourceGroupName $gallery.ResourceGroupName -Name $imageName -ErrorAction SilentlyContinue if ($existingDef) { Write-Host "Found existing image definition, updating existing image definition for $imageName" $newImageDef = Update-AzGalleryImageDefinition -Name $imageName ` -GalleryName $gallery.Name ` -ResourceGroupName $gallery.ResourceGroupName ` -Tag @{MigratedImageSourceResourceID = $sourceImageDetails[$imageName].SourceImageResourceID} } else { Write-Host "Creating new image definition for $imageName" $newImageDef = New-AzGalleryImageDefinition -Name $imageName ` -GalleryName $gallery.Name ` -ResourceGroupName $gallery.ResourceGroupName ` -Location $gallery.Location ` -OsType $imageDefDetails.OsType ` -Publisher $imageDefDetails.Identifier.Publisher ` -Offer $imageDefDetails.Identifier.Offer ` -Sku $imageDefDetails.Identifier.Sku ` -OsState $imageDefDetails.OsState ` -HyperVGeneration $imageDefDetails.HyperVGeneration ` -Feature $Features ` -Tag @{MigratedImageSourceResourceID = $sourceImageDetails[$imageName].SourceImageResourceID} } # Create OSDiskImage object $managedDiskId = (Get-AzDisk -ResourceGroupName $gallery.ResourceGroupName -DiskName $sourceImageDetails[$imageName].DestinationDiskName).Id $osDiskImage = New-Object -TypeName Microsoft.Azure.Management.Compute.Models.GalleryOSDiskImage # Set properties $diskImageSource = New-Object -TypeName Microsoft.Azure.Management.Compute.Models.GalleryDiskImageSource $diskImageSource.Id = $managedDiskId $osDiskImage.Source = $diskImageSource New-AzGalleryImageVersion -ResourceGroupName $gallery.ResourceGroupName ` -GalleryName $gallery.Name ` -GalleryImageDefinitionName $newImageDef.Name ` -Name $versionName ` -Location $gallery.Location ` -OSDiskImage $osDiskImage ` -ReplicaCount 1 ` -StorageAccountType "Standard_LRS" ` -TargetRegion @( @{ Name = $gallery.Location; RegionalReplicaCount = 1 } ) ` -Tag @{MigratedImageSourceResourceID = $sourceImageDetails[$imageName].SourceImageResourceID} Write-Host "Created new image version for $imageName with version $versionName" } catch { Write-Host "Error creating image definition or version for ${imageName}: $_" throw $_ } } Write-Host "Script completed execution. Please check log file for more details and contact your CloudLabs account manager for next steps." } function Delete-TemporaryDisks { param ( [Parameter(Mandatory = $true)] [hashtable] $sourceImageDetails, [Parameter(Mandatory = $true)] [string] $sourceResourceGroupName ) $ErrorActionPreference = "Stop" Write-Host "Revoking SAS tokens and deleting temporary managed disks in CloudLabs Azure environment" foreach ($imageName in $sourceImageDetails.Keys) { try { $sourceDiskName = $sourceImageDetails[$imageName].SourceDiskName Write-Host "Revoking SAS token for source disk $sourceDiskName" Revoke-AzDiskAccess -ResourceGroupName $sourceResourceGroupName -DiskName $sourceDiskName Write-Host "SAS token revoked for source disk $sourceDiskName" Write-Host "Deleting source disk $sourceDiskName" Remove-AzDisk -ResourceGroupName $sourceResourceGroupName -DiskName $sourceDiskName -Force Write-Host "Successfully deleted source disk $sourceDiskName" } catch { Write-Host "Error deleting source disk ${sourceDiskName}: $_" throw $_ } } } # Main script execution Initialize-Logging # Initialize Azure modules and authenticate Connect-ToSourceAzure -sourceSubscriptionId $sourceSubscriptionId # Retrieve the source image gallery and prepare disks Get-SourceImageGallery -sourceResourceGroupName $sourceResourceGroupName -sourceImageGalleryName $sourceImageGalleryName $sourceImageDetails = New-DisksAndGenerateSAS -sourceResourceGroupName $sourceResourceGroupName -sourceImageGalleryName $sourceImageGalleryName -VMImagesList $VMImagesList Connect-ToDestinationAzure -cloudLabsApplicationId $cloudLabsApplicationId -cloudLabsApplicationSecret $cloudLabsApplicationSecret -cloudLabsTenantId $cloudLabsTenantId -cloudLabsSubscriptionId $cloudLabsSubscriptionId # Prepare destination storage and start copying $storageInfo = New-DestinationTemporaryStorageAccount -cloudLabsResourceGroup $cloudLabsResourceGroup -sourceImageGalleryLocation $global:sourceImageGalleryContext.Location Start-ImageDiskCopy -sourceImageDetails ([ref]$sourceImageDetails) -storageInfo $storageInfo # monitor the copy process Monitor-CopyStatus -sourceImageDetails $sourceImageDetails -storageInfo $storageInfo $gallery= Create-NewImageGallery -sourceImageGalleryName $sourceImageGalleryName -cloudLabsResourceGroup $cloudLabsResourceGroup -sourceImageGalleryLocation $global:sourceImageGalleryContext.Location Create-ManagedDisksFromBlobs -sourceImageDetails ([ref]$sourceImageDetails) -storageInfo $storageInfo -cloudLabsResourceGroup $cloudLabsResourceGroup -sourceImageGalleryLocation $global:sourceImageGalleryContext.Location Create-NewImageDefinitions -sourceImageDetails $sourceImageDetails -gallery $gallery Disconnect-AzAccount Set-AzContext -Subscription $sourceSubscriptionId Delete-TemporaryDisks -sourceImageDetails $sourceImageDetails -sourceResourceGroupName $sourceResourceGroupName Stop-Transcript