Copy-Disk.ps1

<#
 .Synopsis
  Upload a disk to the cloud.

 .Description
  Upload a disk to a cloud platform. The currently supported platforms are Azure, AWS and GCP.
#>


Function Copy-Disk
{
    [CmdletBinding(DefaultParameterSetName = 'cmd')]
    Param(
        [Obsolete("Use command-line arguments specifying one of -ToAzure, -ToAWS or -ToGCP.")]
        [Parameter(Mandatory = $True, ParameterSetName = 'file')]
        [string] $ConfigJsonFile,

        [Obsolete("Use one of -ToAzure, -ToAWS or -ToGCP.")]
        [Parameter(Mandatory = $True, ParameterSetName = 'cmd')]
        [string] $CloudPlatform,

        [Parameter(ParameterSetName = 'cmd')]
        [string] $CustomerId,

        [Parameter(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')]
        [Parameter(ParameterSetName = 'file')]
        [pscredential] $SmbCred,

        [Parameter()]
        [int] $UploadTimeout = 36000,

        [Parameter()]
        [int] $Threads,

        [Parameter()]
        [switch] $Install,

        [Parameter()]
        [string] $LogFile,

        [Parameter()]
        [switch] $OverwriteLog,

        [Parameter()]
        [switch] $Force,

        #
        # AWS
        #
        [Parameter(Mandatory = $True, ParameterSetName = 'aws')]
        [switch] $ToAWS,

        [Parameter(ParameterSetName = 'cmd')]
        [Parameter(ParameterSetName = 'aws')]
        [string] $Description,

        [Parameter(ParameterSetName = 'cmd')]
        [Parameter(ParameterSetName = 'aws')]
        [HashTable] $Tags,

        [Parameter(ParameterSetName = 'cmd')]
        [Parameter(ParameterSetName = 'file')]
        [Parameter(ParameterSetName = 'aws')]
        [string] $AwsRegion,

        [Parameter(ParameterSetName = 'cmd')]
        [Parameter(ParameterSetName = 'file')]
        [Parameter(ParameterSetName = 'aws')]
        [string] $AwsProfileName,

        #
        # Azure
        #
        [Parameter(Mandatory = $True, ParameterSetName = 'azure')]
        [switch] $ToAzure,

        [Parameter(ParameterSetName = 'cmd')]
        [Parameter(ParameterSetName = 'azure')]
        [string] $AzureSubscriptionId,

        [Parameter(ParameterSetName = 'cmd')]
        [Parameter(ParameterSetName = 'azure')]
        [string] $AzureLocation,

        [Parameter(ParameterSetName = 'cmd')]
        [Parameter(ParameterSetName = 'azure')]
        [string] $TargetResourceGroup,

        [Parameter(ParameterSetName = 'cmd')]
        [Parameter(ParameterSetName = 'azure')]
        [string] $AzureStorageType = "Premium_LRS",

        [Parameter(ParameterSetName = 'cmd')]
        [Parameter(ParameterSetName = 'file')]
        [Parameter(ParameterSetName = 'azure')]
        [string] $AzureClientId,

        [Parameter(ParameterSetName = 'cmd')]
        [Parameter(ParameterSetName = 'file')]
        [Parameter(ParameterSetName = 'azure')]
        [string] $AzureSecret,

        [Parameter(ParameterSetName = 'cmd')]
        [Parameter(ParameterSetName = 'file')]
        [Parameter(ParameterSetName = 'azure')]
        [string] $AzureTenantId,

        #
        # GCP
        #
        [Parameter(Mandatory = $True, ParameterSetName = 'gcp')]
        [switch] $ToGCP,

        [Parameter(ParameterSetName = 'cmd')]
        [Parameter(ParameterSetName = 'file')]
        [Parameter(Mandatory = $True, ParameterSetName = 'gcp')]
        [string] $GcpServiceAccountKeyFile,

        [Parameter(ParameterSetName = 'cmd')]
        [Parameter(Mandatory = $True, ParameterSetName = 'azure')]
        [Parameter(Mandatory = $True, ParameterSetName = 'gcp')]
        [string] $CloudDiskName,

        [Parameter(Mandatory = $True, ParameterSetName = 'azure')]
        [Parameter(Mandatory = $True, ParameterSetName = 'gcp')]
        [Parameter(Mandatory = $True, ParameterSetName = 'aws')]
        [string] $Path
    )

    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
            }
        }

        if (($PSCmdlet.ParameterSetName -eq 'file') -or ($PSCmdlet.ParameterSetName -eq 'cmd'))
        {
            $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 ($ToAzure -or ($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))
                }
                if ($ToAzure)
                {
                    $fileSize = GetVhdSize $Path
                }
                else
                {
                    $fileSize = GetVhdSizeOnSmbShare $smbConfig
                }
                $diskUrlWithSas = CreateManagedDisk $fileSize $UploadTimeout $AzureStorageType $AzureLocation $TargetResourceGroup $CloudDiskName
                $InformationPreference = "Continue"
                try
                {
                    if ($ToAzure)
                    {
                        UploadToAzure $diskUrlWithSas $Path $Threads $TargetResourceGroup $CloudDiskName
                    }
                    else
                    {
                        UploadFromSmbToAzure $diskUrlWithSas $smbConfig $Threads $TargetResourceGroup $CloudDiskName
                    }
                }
                catch
                {
                    ThrowError ([UploaderException]::new("Failed to copy disk to Azure", $_.Exception))
                }
            }

            if ($ToAWS -or ($CloudPlatform -eq "Aws"))
            {
                $InformationPreference = "Continue"
                try
                {
                    if ($ToAWS)
                    {
                        $snapshotId = UploadToAws $Path $AwsProfileName $AwsRegion $Description $Tags $Threads
                    }
                    else
                    {
                        $snapshotId = UploadFromSmbToAws $smbConfig $AwsProfileName $AwsRegion $Description $Tags $Threads
                    }
                }
                catch
                {
                    ThrowError ([UploaderException]::new("Failed to copy disk to AWS", $_.Exception))
                }
                return $snapshotId
            }

            if ($ToGCP -or ($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
                {
                    if ($ToGCP)
                    {
                        UploadToGcp $CloudDiskName $Path $GcpServiceAccountKeyFile
                    }
                    else
                    {
                        UploadFromSmbToGcp $CloudDiskName $GcpServiceAccountKeyFile $smbConfig
                    }
                }
                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"
                        if ($ToGCP)
                        {
                            UploadToGcp $CloudDiskName $Path $GcpServiceAccountKeyFile
                        }
                        else
                        {
                            UploadFromSmbToGcp $CloudDiskName $GcpServiceAccountKeyFile $smbConfig
                        }
                    }
                    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 LoadModules([string[]]$modules, [bool]$installModule = $False)
{
    foreach ($module in $modules)
    {
        if (Get-Module -ListAvailable -Name $module)
        {
            Log "Module $module exists" $False
        }
        else
        {
            if ($installModule)
            {
                Log "Installing $module" $False
                Install-Module -Name $module -Scope CurrentUser -AllowClobber -Force
            }
            else
            {
                $msg = "Module $module is missing. You can either install it manually or add -Install in the cmdlet to auto-install all " +
                       "missing required modules"
                ThrowError ([UploaderException]::new($msg))
            }
        }
    }
}

Function AuthAzure([string]$azureClientId, [string]$azureSecret, [string]$azureTenantId, [string]$azureSubscriptionId)
{
    if ($azureClientId -And $azureSecret -And $azureTenantId)
    {
        Log "Authenticating to Azure as service principal $azureClientId in subscription $azureSubscriptionId"
        $secret = ConvertTo-SecureString $azureSecret -AsPlainText -Force
        $pscredential = New-Object -TypeName System.Management.Automation.PSCredential($azureClientId, $secret)
        $null = Connect-AzAccount -ServicePrincipal -Credential $pscredential -Tenant $azureTenantId -Subscription $azureSubscriptionId `
          -ErrorAction Stop
    }
    else
    {
        $context = Get-AzContext
        if ($null -eq $context -or ($context.Subscription.Id -ne $azureSubscriptionId))
        {
            Log "Authenticating to Azure interactively in subscription $azureSubscriptionId"
            $null = Connect-AzAccount -Subscription $azureSubscriptionId -ErrorAction Stop
        }
        else
        {
            Log "Already connected to subscription $azureSubscriptionId" $False
        }
    }
}

Function ErrorIsResourceNotFound([object]$err)
{
    try
    {
        return $err[0].Exception.Message -like "*ResourceNotFound*"
    }
    catch
    {
    }
    return $False
}

Function GetAzureDisk([string]$cloudDiskName, [string]$targetResourceGroup)
{
    $err = ""
    $disk = Get-AzDisk -ResourceGroupName $targetResourceGroup -DiskName $cloudDiskName -ErrorAction SilentlyContinue -ErrorVariable err
    if ($null -ne $disk) {
        return $disk
    }
    if (ErrorIsResourceNotFound $err)
    {
        return $null
    }

    $msg = "Failure trying to find managed disk '$cloudDiskName' in resource group $targetResourceGroup"
    ThrowError ([UploaderException]::new($msg, $err.Exception))
}

Function CleanUpAzureDisk([string]$cloudDiskName, [string]$targetResourceGroup)
{
    $disk = GetAzureDisk $cloudDiskName $targetResourceGroup
    if ($null -ne $disk)
    {
        Log "Deleting existing managed disk '$cloudDiskName' in resource group $targetResourceGroup"
        $err = ""
        $result = Remove-AzDisk -ResourceGroupName $targetResourceGroup -DiskName $disk.Name -Force -ErrorAction SilentlyContinue -ErrorVariable err
        if ($null -eq $result)
        {
            ThrowError ([UploaderException]::new("Failed to delete existing managed disk '$($disk.Name)'", $err.Exception))
        }
        Log "Remove-AzDisk status $($result.status)" $False
        Log "Deleted managed disk '$($disk.Name)'"
    }
}

Function GetVhdSize([string]$path)
{
    try
    {
        $fileSize = Get-VhdSize -File $path -RoundUp -IncludeFooterSize
        Log "VHD size for $path is $fileSize"
        return $fileSize
    }
    catch
    {
        ThrowError ([UploaderException]::new("Failed to get VHD size for $($path)", $_))
    }
}

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 CreateManagedDisk([long]$sizeInBytes, [int]$uploadTimeout, [string]$azureStorageType, [string]$azureLocation, [string]$targetResourceGroup,
                           [string]$cloudDiskName)
{
    $sasExpiryDuration = $uploadTimeout
    $diskConfig = New-AzDiskConfig -AccountType $azureStorageType -Location $azureLocation -UploadSizeInBytes $sizeInBytes -CreateOption 'Upload' `
      -OsType Windows -HyperVGeneration V2
    Log "Creating managed disk '$cloudDiskName' with size $sizeInBytes bytes in resource group $targetResourceGroup location $azureLocation"
    $err = ""
    $disk = New-AzDisk -ResourceGroupName $targetResourceGroup -DiskName $cloudDiskName -Disk $diskConfig -ErrorAction SilentlyContinue `
      -ErrorVariable err
    if ($null -eq $disk)
    {
        ThrowError ([UploaderException]::new("Failed to create managed disk '$cloudDiskName' in resource group $targetResourceGroup", $err.Exception))
    }

    Log "Granting access to managed disk '$cloudDiskName' for $sasExpiryDuration seconds" $False
    $err = ""
    $access = Grant-AzDiskAccess -ResourceGroupName $targetResourceGroup -DiskName $cloudDiskName -DurationInSecond $sasExpiryDuration `
      -Access 'Write' -ErrorAction SilentlyContinue -ErrorVariable err
    if ($null -eq $access)
    {
        ThrowError ([UploaderException]::new("Failed to create SAS for mananged disk '$cloudDiskName'", $err.Exception))
    }
    $sas = $access.AccessSAS
    Log "Created managed disk '$cloudDiskName' with SAS $sas" $False
    return $sas
}

Function DoAzureUpload([ScriptBlock]$uploadScript, [string]$diskName, [int]$threads, [string]$targetResourceGroup, [string]$cloudDiskName)
{
    Log ("Copying disk '$diskName' to managed disk '$cloudDiskName' " + $(if ($threads -le 0) {"(threads=default)"} else {"(threads=$threads)"}))
    $ex = $null
    try
    {
        & $uploadScript
        Log "Revoking Azure disk access" $False
        $err = ""
        $r = Revoke-AzDiskAccess -ResourceGroupName $targetResourceGroup -DiskName $cloudDiskName -ErrorAction SilentlyContinue -ErrorVariable err
        if ($null -eq $r)
        {
            ThrowError ([UploaderException]::new("Revoke-AzDiskAccess failed", $err.Exception)) $False
        }
        Log "Revoke-AzDiskAccess status '$($r.status)'" $False
    }
    catch
    {
        $ex = $_.Exception
    }
    finally
    {
        if ($null -eq $ex)
        {
            Log "Copied disk to Azure managed disk '$cloudDiskName'"
        }
        else
        {
            Log "Deleting managed disk '$cloudDiskName' because upload failed"
            $null = Remove-AzDisk -ResourceGroupName $targetResourceGroup -DiskName $cloudDiskName -Force
            throw $ex
        }
    }
}

Function UploadToAzure([string]$destination, [string]$path, [int]$threads, [string]$targetResourceGroup, [string]$cloudDiskName)
{
    $uploadScript = {
        Copy-ToAzDisk -File $path -Sas $destination -Threads $threads
    }
    DoAzureUpload $uploadScript $path $threads $targetResourceGroup $cloudDiskName
}

Function UploadFromSmbToAzure([string]$destination, [psobject]$smbConfig, [int]$threads, [string]$targetResourceGroup, [string]$cloudDiskName)
{
    $uploadScript = {
        CopyWithCloudUploader $destination $smbConfig $threads
    }
    DoAzureUpload $uploadScript $smbConfig.FileOnShare $threads $targetResourceGroup $cloudDiskName
}

Function CopyWithCloudUploader([string]$destination, [psobject]$smbConfig, [int]$threads)
{
    $cloudupload = {
        param($SharePath, $Arguments)
        $fullPath = Join-Path -Path $SharePath -ChildPath $Arguments[0]
        Copy-ToAzDisk -File $fullPath -Sas $destination -Threads $threads
    }
    ExecuteOnSmbShare $cloudupload $smbConfig.SmbCred $smbConfig.ShareUnc @($smbConfig.ExportFilePath)
}

Function CleanUpGcpDisk([string]$gcpServiceAccountKeyFile, [string]$cloudDiskName)
{
    $gcpServiceAccountKey = Get-Content -Raw -Path $gcpServiceAccountKeyFile | ConvertFrom-Json
    try
    {
        $disk = Get-GceImage -Name $cloudDiskName -Project $gcpServiceAccountKey.project_id
    }
    catch
    {
        if ($_.exception.Message -like '*The resource *was not found*')
        {
            return
        }
        ThrowError ([UploaderException]::new("Error finding disk image $($cloudDiskName)", $_.Exception))
    }
    Log "Deleting existing disk image $cloudDiskName"
    Remove-GceImage -Name $cloudDiskName -Project $gcpServiceAccountKey.project_id
}

Function GetAwsCopyArgs([string]$file, [string]$awsProfileName, [string]$awsRegion, [string]$description, [HashTable]$tags, [int]$threads)
{
    $copyArgs = @{File = $file; Description = $description; Tags = $tags; Threads = $threads}
    if (-not [String]::IsNullOrWhiteSpace($awsProfileName))
    {
        $copyArgs["ProfileName"] = $awsProfileName
    }
    if (-not [String]::IsNullOrWhiteSpace($awsRegion))
    {
        $copyArgs["Region"] = $awsRegion
    }
    return $copyArgs
}

Function UploadToAws([string]$path, [string]$awsProfileName, [string]$awsRegion, [string]$description, [HashTable]$tags, [int]$threads)
{
    Log ("Copying disk '$path' to AWS " + $(if ($threads -le 0) {"(threads=default)"} else {"(threads=$threads)"}))
    $copyArgs = GetAwsCopyArgs $path $awsProfileName $awsRegion $description $tags $threads
    $snapshotId = Copy-ToAwsDisk @copyArgs
    Log "Copied disk to AWS snapshot $snapshotId"
    return $snapshotId
}

Function UploadFromSmbToAws([psobject]$smbConfig, [string]$awsProfileName, [string]$awsRegion, [string]$description, [HashTable]$tags, [int]$threads)
{
    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
        Copy-ToAwsDisk @copyArgs
    }
    $snapshotId = ExecuteOnSmbShare $cloudupload $smbConfig.SmbCred $smbConfig.ShareUnc @($smbConfig.ExportFilePath)
    Log "Copied disk to AWS snapshot $snapshotId"
    return $snapshotId
}

Function DeriveBucketName([string]$cloudDiskName)
{
    $bucketName = $cloudDiskName.Split('.')[0]
    if (-not (($bucketName.Length -le 63) -and ($bucketName -cmatch '^[a-z]([-a-z0-9]*[a-z0-9])?$')))
    {
        $msg = "Invalid CloudDiskName '$cloudDiskName'. The stem must meet the requirements for Google Cloud image names. " +
               "See https://cloud.google.com/compute/docs/reference/rest/v1/images"
        ThrowError ([UploaderException]::new($msg))
    }
    return $bucketName
}

Function UploadToGcp([string]$cloudDiskName, [string]$path, [string]$gcpServiceAccountKeyFile)
{
    $bucketName = DeriveBucketName $cloudDiskName
    Log "Copying disk '$path' to bucket '$bucketName'"
    Copy-ToGcpDisk -File $path -BucketName $bucketName -ServiceAccountKeyFile $gcpServiceAccountKeyFile
    Log "Copied disk to image '$cloudDiskName' via bucket '$bucketName'"
}

Function UploadFromSmbToGcp([string]$cloudDiskName, [string]$gcpServiceAccountKeyFile, [psobject]$smbConfig)
{
    $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
    }
    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
}

Function InitUploadLog([string]$logFile, [bool]$overwrite)
{
    if ([String]::IsNullOrWhiteSpace($logFile))
    {
        $logFile = '.\Upload.log'
    }
    $Global:UploadLogFile = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($logFile)
    $timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.ffffZ")
    if ($overwrite)
    {
        "$($timestamp): New log" | Out-File -FilePath $Global:UploadLogFile
    }
    else
    {
        "$($timestamp): New log" | Out-File -FilePath $Global:UploadLogFile -Append
    }
    Write-Host "Logging to $($Global:UploadLogFile)"
}

Function Log([string]$message, [bool]$echoToScreen = $True)
{
    if ($echoToScreen)
    {
        Write-Host $message
    }
    $timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.ffffZ")
    "$($timestamp): $message" | Out-File -FilePath $Global:UploadLogFile -Append
}

Function LogIfSslError([System.Management.Automation.ErrorRecord]$err)
{
    if (CheckForSSLError $err)
    {
        Log ("There was a SSL/TLS error while trying to copy the disk. This is commonly caused by there being an intercepting proxy between " +
             "the machine the command is being run on and the cloud. Check with your network administrator.")
    }
}

Function CheckForSSLError([System.Management.Automation.ErrorRecord]$err)
{
    $ex = $err.Exception
    while ($null -ne $ex)
    {
        if ($ex.Message -like '*remote certificate is invalid*' -or $ex.Message -like '*certificate verify failed*')
        {
            return $True
        }
        $ex = $ex.InnerException
    }
    return $False
}

Function ThrowError([UploaderException]$exception, [bool]$echoToScreen = $True)
{
    if ($null -eq $exception.InnerException)
    {
        Log "$($exception.Message)." $echoToScreen
    }
    else
    {
        Log "$($exception.Message). $($exception.InnerException.Message)" $echoToScreen
    }
    throw $exception
}

Class UploaderException : System.Exception
{
    UploaderException([string]$message) : base($message)
    {
    }
    
    UploaderException([string]$message, [object]$object) : base($message, $object)
    {
    }
}