Copy-Disk.ps1
<# .SYNOPSIS Copy a disk to the cloud. .DESCRIPTION Copy a disk to a cloud platform. The currently supported platforms are Azure, AWS and GCP. .PARAMETER ConfigJsonFile Specifies a file containing JSON configuration. .PARAMETER CustomerId Citrix customer ID. .PARAMETER SmbHost Specifies the hostname of the SMB server where the disk to be copied is located. .PARAMETER SmbPort Specifies the port number of the SMB server where the disk to be copied is located. .PARAMETER SmbShare Specifies the name of the SMB share where the disk to be copied is located. .PARAMETER SmbPath Specifies the path in the SMB share (excluding the file name) to the disk to be copied. .PARAMETER SmbDiskName Specifies the filename of the disk to be copied. .PARAMETER SmbDiskFormat Specifies the format of the disk to be copied. Must be one of "VhdDiskFormat" or "VhdxDiskFormat". "VhdDiskFormat" is the default. .PARAMETER CloudPlatform Specifies the cloud platform to copy the disk to. Must be one of "aws", "azure" or "gcp". .PARAMETER CloudDiskName Specifies the name of the Azure managed disk or Google Cloud image to copy the disk to. .PARAMETER SmbUserDomain Specifies the domain name of the account to use for authenticating to the SMB share where the disk to be copied is located. .PARAMETER SmbUserName Specifies the user name of the account to use for authenticating to the SMB share where the disk to be copied is located. .PARAMETER SmbCred Specifies a credential to use for authenticating to the SMB share where the disk to be copied is located. .PARAMETER UploadTimeout Specifies a timeout for the upload of the disk. Applies to Azure only. .PARAMETER Threads Specifies the number of threads to use for uploading to the cloud. .PARAMETER Install Install required powershell modules. Applies to Azure only. .PARAMETER LogFile Specifies the path to the file to log to. ".\Upload.log" is the default. .PARAMETER OverwriteLog If specified the log file is overwritten otherwise it is appended to. .PARAMETER Force If the destination of the copy already exists delete it before doing the copy. .PARAMETER AwsRegion Specifies the AWS region to create the snapshot in. .PARAMETER AwsProfileName Specifies the name of the profile containing the AWS credentials to use. .PARAMETER AzureSubscriptionId Specifies the ID of the Azure subscription the target resource group is in. .PARAMETER AzureLocation Specifies the Azure location to create the managed disk in.. .PARAMETER TargetResourceGroup Specifies the resource group to create the managed disk in. .PARAMETER AzureStorageType Specifies the storage type to use for the managed disk. Must be one of "Standard_LRS", "Premium_LRS", "StandardSSD_LRS", "Premium_ZRS" or " StandardSSD_ZRS". "Premium_LRS" is the default. .PARAMETER AzureClientId Azure client ID. .PARAMETER AzureSecret Azure client secret. .PARAMETER AzureTenantId Azure tenant ID. .PARAMETER GcpServiceAccountKeyFile Specifies the name of a file containing Google Cloud service principal credentials. .INPUTS None. .OUTPUTS System.String. When copying to AWS the ID of the EC2 snapshot the disk is copied to is returned. .EXAMPLE PS> @CopyParams = @{ CloudPlatform = 'aws' SmbHost = 'share.example.com' SmbShare = 'ips' SmbPath = 'staging/disks' SmbDiskName = 'test' SmbDiskFormat = 'VhdDiskFormat' SmbUserDomain = 'users' SmbUserName = 'demo' } PS> Copy-Disk @CopyParams -LogFile aws-copy.log -OverwriteLog snap-b4191a9783f1f568e .EXAMPLE PS> $CopyParams = @{ CloudPlatform = 'azure' SmbHost = 'share.example.com' SmbShare = 'ips' SmbPath = 'staging/disks' SmbDiskName = 'test' SmbDiskFormat = 'VhdDiskFormat' SmbUserDomain = '.' SmbUserName = 'demo' AzureSubscriptionId = 'efa90dc7-c7d7-4f7b-b633-80a896a56b11' AzureLocation = 'eastus' TargetResourceGroup = 'ips-test' CloudDiskName = 'test' AzureSecret = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' AzureClientId = 'a8470b17-f549-42f4-8802-0f81234046a0' AzureTenantId = 'ff6cec26-b5c6-4b5c-9b9b-d427ab131a2f' } PS> Copy-Disk @CopyParams .EXAMPLE PS> $CopyParams = @{ CloudPlatform = 'gcp' SmbHost = 'share.example.com' SmbShare = 'ips' SmbPath = 'staging/disks' SmbDiskName = 'test' SmbDiskFormat = 'VhdDiskFormat' SmbUserDomain = 'users' SmbUserName = 'demo' SmbDiskFormat = 'VhdDiskFormat' SmbUserDomain = 'citrite' SmbUserName = 'nicholasn' CloudDiskName = 'nicholasn-test' GcpServiceAccountKeyFile = 'demo-project-af94dadb30a1.json' } PS> Copy-Disk @CopyParams -LogFile gcp-copy.log #> Function Copy-Disk { [CmdletBinding(DefaultParameterSetName = 'cmd')] [Obsolete("Use one of Copy-DiskToAWS, Copy-DiskToAzure or Copy-DiskToGCP.")] Param( [Parameter(Mandatory = $True, ParameterSetName = 'file')] [string] $ConfigJsonFile, [Parameter(ParameterSetName = 'cmd')] [string] $CustomerId, [Parameter(Mandatory = $True, ParameterSetName = 'cmd')] [string] $CloudPlatform, [Parameter(Mandatory = $True, ParameterSetName = 'cmd')] [string] $SmbHost, [Parameter(ParameterSetName = 'cmd')] [string] $SmbPort = $null, [Parameter(Mandatory = $True, ParameterSetName = 'cmd')] [string] $SmbShare, [Parameter(ParameterSetName = 'cmd')] [string] $SmbPath, [Parameter(Mandatory = $True, ParameterSetName = 'cmd')] [string] $SmbDiskName, [Parameter(ParameterSetName = 'cmd')] [string] $SmbDiskFormat = "VhdDiskFormat", [Parameter(Mandatory = $True, ParameterSetName = 'cmd')] [string] $SmbUserDomain, [Parameter(Mandatory = $True, ParameterSetName = 'cmd')] [string] $SmbUserName, [Parameter(ParameterSetName = 'cmd')] [string] $AzureSubscriptionId, [Parameter(ParameterSetName = 'cmd')] [string] $AzureLocation, [Parameter(ParameterSetName = 'cmd')] [string] $TargetResourceGroup, [Parameter(ParameterSetName = 'cmd')] [string] $CloudDiskName, [Parameter(ParameterSetName = 'cmd')] [int] $UploadTimeout = 36000, [Parameter(ParameterSetName = 'cmd')] [string] $AzureStorageType = "Premium_LRS", [Parameter(ParameterSetName = 'cmd')] [int] $Threads, [Parameter()] [string] $AzureClientId, [Parameter()] [string] $AzureSecret, [Parameter()] [string] $AzureTenantId, [Parameter()] [string] $AwsRegion, [Parameter()] [string] $AwsProfileName, [Parameter()] [string] $GcpServiceAccountKeyFile, [Parameter()] [pscredential] $SmbCred, [Parameter()] [switch] $Install, [Parameter()] [string] $LogFile, [Parameter()] [switch] $OverwriteLog, [Parameter()] [switch] $Force ) Begin { InitUploadLog $LogFile $OverwriteLog if ($PSCmdlet.ParameterSetName -eq 'file') { Log "Loading configuration from $ConfigJsonFile" $False $configData = Get-Content -Raw -Path $ConfigJsonFile | ConvertFrom-Json Log "Configuration: $configData" $False $CustomerId = $configData.CustomerId $CloudPlatform = $configData.CloudPlatform $SmbHost = $configData.UploadSmb.Host $SmbPort = $configData.UploadSmb.Port $SmbShare = $configData.UploadSmb.Share $SmbPath = $configData.UploadSmb.Path $SmbDiskName = $configData.UploadSmb.DiskName $SmbUserDomain = $configData.UploadSmb.UserDomain $SmbUserName = $configData.UploadSmb.UserName $AzureSubscriptionId = $configData.AzureSubscriptionId $AzureLocation = $configData.AzureLocation $TargetResourceGroup = $configData.TargetResourceGroup $CloudDiskName = $configData.CloudDiskName if (-not [String]::IsNullOrWhiteSpace($configData.UploadSmb.DiskFormat)) { $SmbDiskFormat = $configData.UploadSmb.DiskFormat } if ($null -ne $configData.UploadTimeout -and $configData.UploadTimeout -gt 0) { $UploadTimeout = [int]$configData.UploadTimeout } if (-not [String]::IsNullOrWhiteSpace($configData.AzureStorageType)) { $AzureStorageType = $configData.AzureStorageType } if ($Threads -le 0 -and -not [String]::IsNullOrWhiteSpace($configData.Threads)) { $Threads = [int]$configData.Threads } if ([String]::IsNullOrWhiteSpace($GcpServiceAccountKeyFile)) { $GcpServiceAccountKeyFile = $configData.GcpServiceAccountKeyFile } } $smbConfig = InitSmbConfig $SmbHost $SmbPort $SmbShare $SmbPath $SmbUserDomain $SmbUserName $SmbDiskName $SmbDiskFormat if ($null -eq $SmbCred) { Log "Generating SMB credential using username and password" $False $password = Read-Host -assecurestring "SMB user password" $smbConfig.SmbCred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $smbConfig.UserAndDomain, $password } else { Log "SMB credential given as input" $False $smbConfig.SmbCred = $SmbCred } } Process { try { if ($CloudPlatform -eq "Azure") { Log "Loading required modules" $False LoadModules @('Az.Accounts', 'Az.Compute') $Install AuthAzure $AzureClientId $AzureSecret $AzureTenantId $AzureSubscriptionId if ($Force) { CleanUpAzureDisk $CloudDiskName $TargetResourceGroup } elseif ($null -ne (GetAzureDisk $CloudDiskName $TargetResourceGroup)) { $msg = "Managed disk '$CloudDiskName' aleady exists in resource group $TargetResourceGroup. Consider using the -Force option" ThrowError ([UploaderException]::new($msg)) } $fileSize = GetVhdSizeOnSmbShare $smbConfig $diskUrlWithSas = CreateManagedDisk -sizeInBytes $fileSize -uploadTimeout $UploadTimeout -azureStorageType $AzureStorageType -azureLocation $AzureLocation -targetResourceGroup $TargetResourceGroup -cloudDiskName $CloudDiskName $InformationPreference = "Continue" try { UploadFromSmbToAzure $diskUrlWithSas $smbConfig $Threads $TargetResourceGroup $CloudDiskName $Global:UploadLogFile } catch { ThrowError ([UploaderException]::new("Failed to copy disk to Azure", $_.Exception)) } } if ($CloudPlatform -eq "Aws") { $InformationPreference = "Continue" try { return UploadFromSmbToAws $smbConfig $AwsProfileName $AwsRegion $null $null $Threads $Global:UploadLogFile } catch { ThrowError ([UploaderException]::new("Failed to copy disk to AWS", $_.Exception)) } } if ($CloudPlatform -eq "Gcp") { # Temporary solution to redirecting 1.48 assemblies that are compile time referenced in the Google.Api.Gax.Rest assemblies # to the 1.49 assemblies that the other Google assemblies reference # This is the powershell way to accomplish Binding Redirects, that are typically done in the configuration file. $modulePath = (Get-Item (Get-Module -Name Citrix.Image.Uploader).Path).DirectoryName $GoogleApis = [reflection.assembly]::LoadFrom($modulePath + "\bin\netstandard2.0\Google.Apis.dll") $GoogleApisCore = [reflection.assembly]::LoadFrom($modulePath + "\bin\netstandard2.0\Google.Apis.Core.dll") $GoogleApisAuth = [reflection.assembly]::LoadFrom($modulePath + "\bin\netstandard2.0\Google.Apis.Auth.dll") $OnAssemblyResolve = [System.ResolveEventHandler] { param($s, $e) Log "Resolving assembly '$($e.Name)'" $False if (($e.Name.StartsWith("Google.Apis.Core, Version=1.49.0.0")) -or ($e.Name.StartsWith("Google.Apis.Auth, Version=1.49.0.0")) -or ($e.Name.StartsWith("Google.Apis, Version=1.49.0.0"))) { Log ("This workaround may no longer be necessary. The Google assemblies may now be referencing the correct assemblies. " + "Try removing this event handler (OnAssemblyResolve) and try again.") $False } if ($e.Name -eq "Google.Apis.Core, Version=1.48.0.0, Culture=neutral, PublicKeyToken=4b01fa6e34db77ab") { Log ("Forcing the Assembly '$($e.Name)' to be '$($GoogleApisCore.GetName().Name), " + "Version=$($GoogleApisCore.GetName().Version)'") $False return $GoogleApisCore } if ($e.Name -eq "Google.Apis.Auth, Version=1.48.0.0, Culture=neutral, PublicKeyToken=4b01fa6e34db77ab") { Log ("Forcing the Assembly '$($e.Name)' to be '$($GoogleApisAuth.GetName().Name), " + "Version=$($GoogleApisAuth.GetName().Version)'") $False return $GoogleApisAuth } if ($e.Name -eq "Google.Apis, Version=1.48.0.0, Culture=neutral, PublicKeyToken=4b01fa6e34db77ab") { Log "Forcing the Assembly '$($e.Name)' to be '$($GoogleApis.GetName().Name), Version=$($GoogleApis.GetName().Version)'" $False return $GoogleApis } foreach($a in [System.AppDomain]::CurrentDomain.GetAssemblies()) { if ($a.FullName -eq $e.Name) { return $a } } return $null } Log "Registering AssemblyResolve event handling" $False [System.AppDomain]::CurrentDomain.add_AssemblyResolve($OnAssemblyResolve) if ($Force) { CleanUpGcpDisk $GcpServiceAccountKeyFile $CloudDiskName } $InformationPreference = "Continue" try { UploadFromSmbToGcp $CloudDiskName $GcpServiceAccountKeyFile $smbConfig $Global:UploadLogFile } catch [System.Reflection.ReflectionTypeLoadException] { Log "Message: $($_.Exception.Message)" $False Log "StackTrace: $($_.Exception.StackTrace)" $False Log "LoaderExceptions: $($_.Exception.LoaderExceptions)" $False try { Log "Redirected Google SDK package version and retry uploading process" UploadFromSmbToGcp $CloudDiskName $GcpServiceAccountKeyFile $smbConfig $Global:UploadLogFile } catch { ThrowError ([UploaderException]::new("Failed to copy disk to Google Cloud", $_.Exception)) } } catch { ThrowError ([UploaderException]::new("Failed to copy disk to Google Cloud", $_.Exception)) } } } catch [UploaderException] { LogIfSslError $_ $PSCmdlet.ThrowTerminatingError($_) } catch { Log $_ LogIfSslError $_ $PSCmdlet.ThrowTerminatingError($_) } } } Function InitSmbConfig([string]$smbHost, [string]$smbPort, [string]$smbShare, [string]$smbPath, [string]$smbUserDomain, [string]$smbUserName, [string]$smbDiskName, [string]$smbDiskFormat) { $DISK_FORMATS = @{ VhdDiskFormat = "vhd" VhdxDiskFormat = "vhdx" VmdkDiskFormat = "vmdk" VmdkSparseDiskFormat = "vmdk" QCow2DiskFormat = "qcow" RawDiskFormat = "raw" } $smbConfig = @{} $smbConfig.DiskExtension = $DISK_FORMATS[$smbDiskFormat] $smbConfig.UserAndDomain = "$($smbUserDomain)\$($smbUserName)" if ($smbPort) { $smbConfig.ShareUnc = "\\$($smbHost):$($smbPort)\$($smbShare)" } else { $smbConfig.ShareUnc = "\\$($smbHost)\$($smbShare)" } if ($smbPath) { $smbConfig.ExportFilePath = Join-Path -Path $smbPath -ChildPath "$($smbDiskName).$($smbConfig.DiskExtension)" } else { $smbConfig.ExportFilePath = "$($smbDiskName).$($smbConfig.DiskExtension)" } $smbConfig.FileOnShare = Join-Path -Path $smbConfig.ShareUnc -ChildPath $smbConfig.ExportFilePath return $smbConfig } Function GetVhdSizeOnSmbShare([psobject]$smbConfig) { try { Log "Getting VHD size as $($smbConfig.UserAndDomain) for $($smbConfig.FileOnShare)" $False $getVhdSize = { param($SharePath, $Arguments) $fullPath = Join-Path -Path $SharePath -ChildPath $Arguments[0] return Get-VhdSize -File $fullPath -RoundUp -IncludeFooterSize } $fileSize = ExecuteOnSmbShare $getVhdSize $smbConfig.SmbCred $smbConfig.ShareUnc @($smbConfig.ExportFilePath) Log "VHD size for $($smbConfig.FileOnShare) is $fileSize" return $fileSize } catch { ThrowError ([UploaderException]::new("Failed to get VHD size for $($smbConfig.FileOnShare)", $_)) } } Function UploadFromSmbToAzure([string]$destination, [psobject]$smbConfig, [int]$threads, [string]$targetResourceGroup, [string]$cloudDiskName, [string]$logFileName) { $uploadScript = { CopyWithCloudUploader $destination $smbConfig $threads $logFileName } DoAzureUpload $uploadScript $smbConfig.FileOnShare $threads $targetResourceGroup $cloudDiskName } Function CopyWithCloudUploader([string]$destination, [psobject]$smbConfig, [int]$threads, [string]$logFileName) { $cloudupload = { param($SharePath, $Arguments) $fullPath = Join-Path -Path $SharePath -ChildPath $Arguments[0] Copy-ToAzDisk -File $fullPath -Sas $destination -Threads $threads -LogFileName $logFileName } ExecuteOnSmbShare $cloudupload $smbConfig.SmbCred $smbConfig.ShareUnc @($smbConfig.ExportFilePath) } Function UploadFromSmbToAws([psobject]$smbConfig, [string]$awsProfileName, [string]$awsRegion, [string]$description, [HashTable]$tags, [int]$threads, [string]$logFileName) { Log ("Copying disk '$($smbConfig.FileOnShare)' to AWS " + $(if ($threads -le 0) {"(threads=default)"} else {"(threads=$threads)"})) $cloudupload = { param($SharePath, $Arguments) $fullPath = Join-Path -Path $SharePath -ChildPath $Arguments[0] $copyArgs = GetAwsCopyArgs $fullPath $awsProfileName $awsRegion $description $tags $threads $logFileName Copy-ToAwsDisk @copyArgs } $snapshotId = ExecuteOnSmbShare $cloudupload $smbConfig.SmbCred $smbConfig.ShareUnc @($smbConfig.ExportFilePath) Log "Copied disk to AWS snapshot $snapshotId" return $snapshotId } Function UploadFromSmbToGcp([string]$cloudDiskName, [string]$gcpServiceAccountKeyFile, [psobject]$smbConfig, [string]$logFileName) { $bucketName = DeriveBucketName $cloudDiskName Log "Copying disk '$($smbConfig.FileOnShare)' to bucket '$bucketName'" $cloudupload = { param($SharePath, $Arguments) $fullPath = Join-Path -Path $SharePath -ChildPath $Arguments[0] Copy-ToGcpDisk -File $fullPath -BucketName $bucketName -ServiceAccountKeyFile $gcpServiceAccountKeyFile -LogFileName $logFileName } ExecuteOnSmbShare $cloudupload $smbConfig.SmbCred $smbConfig.ShareUnc @($smbConfig.ExportFilePath) Log "Copied disk to image '$cloudDiskName' via bucket '$bucketName'" } Function ExecuteOnSmbShare([ScriptBlock]$scriptblock, [PSCredential]$smbCred, [string]$share, [string[]]$arguments) { $name = "CtxMapping" Log "ExecuteOnSmbShareWithCreds on share $share with args $arguments" $False $err = "" $drive = New-PSDrive -Name $name -PSProvider "FileSystem" -Root $share -Credential $smbCred -Scope Script -ErrorAction SilentlyContinue ` -ErrorVariable err if ($null -eq $drive) { ThrowError ([UploaderException]::new("Failed to map share $share", $_.Exception)) } try { Log "Operating on $($drive.Name) with $arguments" $False $output = & $scriptblock -SharePath "$($name):" -Arguments $arguments Log "Upload successful" $False } finally { try { $null = Remove-PSDrive -Name $name -Force } catch { Log "Failure removing PS drive $($name). $_" $False } } return $output } |