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 = 'VhdxDiskFormat'
  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 = 'VhdxDiskFormat'
  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 = 'VhdxDiskFormat'
  SmbUserDomain = 'users'
  SmbUserName = 'demo'
  CloudDiskName = '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

        VersionCheck $CustomerId

        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 ([UploaderError]::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 ([UploaderError]::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 ([UploaderError]::new("Failed to copy disk to AWS", $_.Exception))
                }
            }

            if ($CloudPlatform -eq "Gcp")
            {
                if ($Force)
                {
                    CleanUpGcpDisk $GcpServiceAccountKeyFile $CloudDiskName
                }
                $InformationPreference = "Continue"
                try
                {
                    UploadFromSmbToGcp $CloudDiskName $GcpServiceAccountKeyFile $smbConfig $Global:UploadLogFile
                }
                catch
                {
                    ThrowError ([UploaderError]::new("Failed to copy disk to Google Cloud", $_.Exception))
                }
            }
        }
        catch [UploaderError]
        {
            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 ([UploaderError]::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)
{
    $cloudupload = {
        param($SharePath, $Arguments)
        $fullPath = Join-Path -Path $SharePath -ChildPath $Arguments[0]
        Copy-ToGcpDisk -File $fullPath -ImageName $cloudDiskName -ServiceAccountKeyFile $gcpServiceAccountKeyFile -LogFileName $logFileName
    }
    ExecuteOnSmbShare $cloudupload $smbConfig.SmbCred $smbConfig.ShareUnc @($smbConfig.ExportFilePath)
}

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 ([UploaderError]::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
}