Upgrade-SqlServerStandaloneDatabaseEngineInstance.ps1

# AWS Upgrade Utility for SQL Server
# Copyright 2019, Amazon Web Services, Inc. or its affiliates. All rights reserved.
# =======================================================================================

<#PSScriptInfo
    .VERSION 0.1.1
 
    .GUID 8f4b7f93-a23d-442a-b9a4-c8014bf5f927
 
    .AUTHOR Amazon Web Services, Inc.
 
    .COMPANYNAME Amazon Web Services, Inc.
 
    .COPYRIGHT Copyright 2019, Amazon Web Services, Inc. or its affiliates. All rights reserved.
 
    .TAGS AWS SqlServer SQL Server MSSQL upgrade compatibility VMware PowerCLI VM VirtualMachine PSEdition_Core PSEdition_Desktop Windows Linux Mac macOS
 
    .LICENSEURI https://github.com/awslabs/aws-tools-for-vmware/blob/master/LICENSE
 
    .PROJECTURI https://github.com/awslabs/aws-tools-for-vmware/blob/master/powershell/Upgrade-SqlServerStandaloneDatabaseEngineInstance.ps1
 
    .ICONURI
 
    .EXTERNALMODULEDEPENDENCIES
 
    .REQUIREDSCRIPTS
 
    .EXTERNALSCRIPTDEPENDENCIES
 
    .RELEASENOTES
    ### 0.1.1
    Added blog link.
 
    ### 0.1.0
    First version published to PowerShellGallery.
 
    .PRIVATEDATA
#>


<#
    .SYNOPSIS
    Upgrade a standalone SQL Server Database Engine instance in-place.
 
    .DESCRIPTION
    ### What this tool does
 
    This will perform an *impactful*, in-place, major version upgrade, such as
    SQL Server 2008 SP4 -> 2017 or 2008 R2 SP3 -> 2016, on a standalone
    SQL Server Database Engine instance on a Windows operating system. It can
    be run either locally, or remotely if deployed on a Windows VM in a vSphere
    environment such as VMware Cloud on AWS. You must supply your own
    SQL Server installation media and product key / license.
 
    The compatibility level of each database deployed on the target SQL Server
    instance will not be modified by this script, and *should* remain the same,
    but please test thoroughly.
 
    Please see the links below for additional resources such as Microsoft's
    best practices for planning your SQL Server instance upgrades, breaking
    changes and backwards compatibility, et cetera.
 
    Again, the upgrade process is impactful, so please test thoroughly and plan
    for application downtime.
 
    ### What this tool does not do
 
    !!! danger "Backups are *NOT* included"
        Backups are *NOT* included. Please make sure that you have implemented
        and verified proper backups, and that you have a recovery plan
        established that meets the recovery plan objective (RPO) and recovery
        time objective (RTO).
 
    This tool does not accommodate the intricacies of upgrading any
    high availability (HA) SQL Server instance types including:
 
    - Replicated Databases
    - Mirrored Databases
    - Log Shipping Instances
    - Failover Cluster Instances (FCI)
    - AlwaysOn Availability Groups (AAG)
 
    This tool does not accommodate edition upgrades within the same version of
    SQL Server either.
 
    The checks run prior to the upgrade cannot test for every eventuality. In
    fact, most of the requirements and compatibility testing is delegated to
    Microsoft's SQL Server installation media since it was built with a robust
    testing framework. Please test thoroughly.
 
    ### Local upgrades
    For local upgrades, this script requires elevated privileges and must be
    run from PowerShell launched with the 'Run as Administrator' option.
 
    ### Remote upgrades
    For VMware PowerCLI-based remote upgrades, HTTPS (443/tcp) connectivity is
    required to the ESXi hosts as well as vCenter for executing commands in the
    VM's guest operating system via the VMware Guest Operations API. This
    connectivity is not permitted by default, such as in VMware Cloud on AWS,
    but can be configured.
 
    This tool does not attempt to install or import the required PowerCLI
    modules, nor does it attempt to establish a PowerCLI session with vCenter.
    For VMware PowerCLI installation instructions, please see:
    https://www.powershellgallery.com/packages/VMware.PowerCLI/. Once
    installed, run `Import-Module -Name 'VMware.VimAutomation.Core'` to import
    the subset of modules required. To learn more about how to establish a
    PowerCLI session, run `Get-Help -Name 'Connect-VIServer' -Detailed`, which
    includes a few examples.
 
    All target VMs must be powered on, and VMware Tools must be installed and
    running in the guest operating system of each Windows VM.
 
    The supplied credentials will be used on each VM to access the guest
    operating system, and must have administrative privileges. Because feature:
    https://powercli.ideas.aha.io/ideas/PCLI-I-101 has neither been accepted
    nor released by the PowerCLI team, Windows User Account Control (UAC) must
    be disabled in each guest operating system as well.
 
    Multiple VMs can be specified in the same command for batch upgrades via an
    array of VM IDs or names, as well as wildcard globbing of VM names;
    however, the SQL Server instance on each VM is upgraded iteratively, not
    concurrently. Please plan accordingly.
 
    .INPUTS
    System.String
 
    .NOTES
    ### Security
    To reduce the risk of unintended code execution, a file hash must be
    supplied for the setup file, which will be compared to a file hash of the
    specified setup file in an attempt to confirm file integrity and that the
    correct media has been loaded before launching the upgrade. Additionally,
    a few properties will be checked in an attempt to confirm that a
    SQL Server setup file has been specified.
 
    .EXAMPLE
    ./Upgrade-SqlServerStandaloneDatabaseEngineInstance.ps1 -FilePath 'D:\setup.exe' -FileHash $sha256FileHash -IAcceptSqlServerLicenseTerms -WhatIf
    Performs a 'dry run test' of a local, in-place upgrade of the default
    SQL Server Database Engine instance (MSSQLSERVER) that would install in the
    default directory, and validates the integrity of the specified SQL Server
    setup file by comparing the SHA256 file hashes.
 
    Since a product key / license was not supplied, the instance would be
    upgraded into Evaluation mode unless upgraded to SQL Server Express
    edition.
 
    .EXAMPLE
    ./Upgrade-SqlServerStandaloneDatabaseEngineInstance.ps1 -FilePath 'D:\setup.exe' -FileHash $sha256FileHash -IAcceptSqlServerLicenseTerms
    Implements the previous example.
 
    Since a product key / license was not supplied, the instance will be
    upgraded into Evaluation mode unless upgraded to SQL Server Express
    edition.
 
    .EXAMPLE
    ./Upgrade-SqlServerStandaloneDatabaseEngineInstance.ps1 -FilePath 'E:\setup.exe' -FileHash $md5FileHash -Algorithm 'MD5' -InstanceName 'SQLEXPRESS' -InstanceDirectory 'D:\MSSQL' -ProductKey $productKey -IAcceptSqlServerLicenseTerms
    Performs a local, in-place upgrade of the SQLEXPRESS SQL Server Database
    Engine instance that will install in the specified directory, validates the
    integrity of the specified SQL Server setup file by comparing the MD5 file
    hashes, and applies the specified product key.
 
    .EXAMPLE
    ./Upgrade-SqlServerStandaloneDatabaseEngineInstance.ps1 'E:\setup.exe' $md5FileHash 'MD5' 'SQLEXPRESS' 'D:\MSSQL' $productKey $true
    The same in-place upgrade as in the example above using positional
    arguments.
 
    .EXAMPLE
    ./Upgrade-SqlServerStandaloneDatabaseEngineInstance.ps1 -FilePath 'O:\setup.exe' -FileHash $md5FileHash -Algorithm 'MD5' -IAcceptSqlServerLicenseTerms -Credential (Get-Credential) -VmName 'SQL1', 'MSSQL*'
    Performs a remote, PowerCLI-based in-place upgrade of the default
    SQL Server Database Engine instance (MSSQLSERVER) on the SQL1 VM, as well
    as any VM with a name starting 'MSSQL' (due to the '*' wildcard). It will
    install in the default directory, and validates the integrity of the
    specified SQL Server setup file by comparing the MD5 file hashes.
 
    .EXAMPLE
    ./Upgrade-SqlServerStandaloneDatabaseEngineInstance.ps1 -FilePath 'D:\setup.exe' -FileHash $sha512FileHash -Algorithm 'SHA512' -IAcceptSqlServerLicenseTerms -Credential (Get-Credential) -VmID 'VirtualMachine-vm-42'
    Performs a remote, PowerCLI-based in-place upgrade of the default
    SQL Server Database Engine instance (MSSQLSERVER) on the VM with MoRef ID
    'VirtualMachine-vm-42'. It will install in the default directory, and
    validates the integrity of the specified SQL Server setup file by comparing
    the SHA512 file hashes.
 
    .EXAMPLE
    ( Get-VM -Name '*SQL*' ).ID | ./Upgrade-SqlServerStandaloneDatabaseEngineInstance.ps1 -FilePath 'D:\setup.exe' -FileHash $sha256FileHash -IAcceptSqlServerLicenseTerms -Credential (Get-Credential)
    Performs a remote, PowerCLI-based in-place upgrade of the default
    SQL Server Database Engine instance (MSSQLSERVER) on all VMs with 'SQL' in
    the name. It will install in the default directory, and validates the
    integrity of the specified SQL Server setup file by comparing the SHA256
    file hashes.
 
    .LINK
    https://awslabs.github.io/aws-tools-for-vmware/powershell/Upgrade-SqlServerStandaloneDatabaseEngineInstance.ps1/
 
    .LINK
    https://github.com/awslabs/aws-tools-for-vmware/blob/master/powershell/Upgrade-SqlServerStandaloneDatabaseEngineInstance.ps1
 
    .LINK
    https://www.powershellgallery.com/packages/Upgrade-SqlServerStandaloneDatabaseEngineInstance/
 
    .LINK
    https://docs.microsoft.com/sql/database-engine/install-windows/supported-version-and-edition-upgrades
 
    .LINK
    https://docs.microsoft.com/sql/database-engine/install-windows/upgrade-database-engine
 
    .LINK
    https://docs.microsoft.com/sql/database-engine/install-windows/plan-and-test-the-database-engine-upgrade-plan
 
    .LINK
    https://docs.microsoft.com/sql/sql-server/install/hardware-and-software-requirements-for-installing-sql-server
 
    .LINK
    https://docs.microsoft.com/sql/database-engine/sql-server-database-engine-backward-compatibility
 
    .LINK
    https://github.com/awslabs/aws-tools-for-vmware/issues/new
 
    .LINK
    https://console.aws.amazon.com/support/home#/case/create?issueType=technical
 
    .LINK
    https://aws.amazon.com/blogs/database/upgrade-your-end-of-support-sql-server-instances-in-vmware-cloud-on-aws-with-ease/
#>


[CmdletBinding( DefaultParameterSetName = 'Local', SupportsShouldProcess = $true, ConfirmImpact = 'High' )]
[OutputType( [string] )]

param (
    <#
    Specifies the path to the SQL Server installation media.
 
    Example: D:\setup.exe
    #>

    [Parameter(
        Mandatory = $true,
        Position = 0
    )]
    [ValidateNotNull()]
    [System.IO.FileInfo]
    $FilePath,

    <#
    Specifies the expected SQL Server setup file hash. This can be obtained via
    the `Get-FileHash` cmdlet, the `certutil.exe -HashFile` command, or similar
    tools.
    #>

    [Parameter(
        Mandatory = $true,
        Position = 1
    )]
    [ValidateLength( 32, 128 )]
    [string]
    $FileHash,

    # Specifies the setup file hash algorithm.
    [Parameter( Position = 2 )]
    [ValidateSet( 'MD5', 'SHA1', 'SHA256', 'SHA384', 'SHA512' )]
    [string]
    $Algorithm = 'SHA256',

    # Specifies the target SQL Server instance name.
    [Parameter(
        Position = 3,
        ValueFromPipelineByPropertyName = $true
    )]
    [ValidateNotNullOrEmpty()]
    [string]
    $InstanceName = 'MSSQLSERVER',

    # Specifies a non-default installation directory for shared components.
    [Parameter(
        Position = 4,
        ValueFromPipelineByPropertyName = $true
    )]
    [ValidateNotNull()]
    [System.IO.FileInfo]
    $InstanceDirectory,

    # Specifies the product key for the edition of SQL Server.
    [Parameter(
        Position = 5,
        ValueFromPipelineByPropertyName = $true
    )]
    [ValidateNotNullOrEmpty()]
    [string]
    $ProductKey,

    <#
    Required to acknowledge acceptance of Microsoft's license terms for
    SQL Server.
 
    Reference: https://docs.microsoft.com/sql/database-engine/install-windows/install-sql-server-from-the-command-prompt#Upgrade
    #>

    [Parameter(
        Mandatory = $true,
        Position = 6
    )]
    [ValidateSet( $true )]
    [switch]
    $IAcceptSqlServerLicenseTerms,

    <#
    Specifies the Windows guest operating system credentials with
    administrative rights. Used for updating the SQL Server instance.
    #>

    [Parameter(
        ParameterSetName = 'Remote: VM by ID',
        Mandatory = $true,
        Position = 7
    )]
    [Parameter(
        ParameterSetName = 'Remote: VM by Name',
        Mandatory = $true,
        Position = 7
    )]
    [ValidateScript( { $_.GetNetworkCredential().Password.Length -gt 0 } )]
    [pscredential]
    $Credential,

    <#
    Specifies the vSphere managed object reference identifier (MoRef ID) of
    one or more target VMs.
 
    Example: VirtualMachine-vm-431
    #>

    [Parameter(
        ParameterSetName = 'Remote: VM by ID',
        Mandatory = $true,
        Position = 8,
        ValueFromPipeline = $true,
        ValueFromPipelineByPropertyName = $true
    )]
    [ValidatePattern( '^VirtualMachine-vm-\d+$' )]
    [Alias( 'ID' )]
    [string[]]
    $VmID,

    <#
    The name of one or more target VMs. Accepts wildcard characters.
 
    Example: SQL1, MSSQL*
    #>

    [Parameter(
        ParameterSetName = 'Remote: VM by Name',
        Mandatory = $true,
        ValueFromPipelineByPropertyName = $true
    )]
    [ValidateNotNullOrEmpty()]
    [SupportsWildcards()]
    [Alias( 'Name' )]
    [string[]]
    $VmName
)

begin {
    #region Functions
    function Get-LocalizedMessage {
        [CmdletBinding()]
        [OutputType( [string] )]

        param (
            # Specifies the key to lookup in the localized messages hash table
            [Parameter(
                Mandatory = $true,
                Position = 0,
                ValueFromPipeline = $true
            )]
            [ValidateNotNullOrEmpty()]
            [string]
            $Key,

            # Strings to insert into the message body.
            [Parameter(
                Position = 1,
                ValueFromRemainingArguments = $true
            )]
            [string[]]
            $Parameters
        )

        begin {
            $messages = @{
                'en-US' = @{
                    Beginning                          = 'Beginning: "{0}"'
                    Processing                         = "Processing: `"{0}`" with ParameterSetName `"{1}`" and Parameters:{2}"
                    Ending                             = 'Ending: "{0}"'

                    NoProductKeyUpgrade                = 'proceed with *NO PRODUCT KEY*'
                    Name                               = 'Name'
                    ID                                 = 'ID'
                    PowerState                         = 'PowerState'

                    ProductName                        = 'Microsoft SQL Server'
                    Comment                            = 'SQL'
                    CompanyName                        = 'Microsoft Corporation'

                    Local                              = 'local'
                    Remote                             = 'remote'
                    StartingPreFlight                  = 'Starting {0} mode pre-flight checks.'
                    StartingUpgrade                    = "`nStarting {0} mode SQL Server Database Engine instance upgrade.`n"

                    VerifyingCurrentSessionPermissions = 'Verifying current session permissions...'
                    VerifyingOperatingSystem           = 'Verifying operating system...'
                    VerifyingInstallationMediaExists   = 'Verifying installation media exists...'
                    VerifyingFileProperties            = 'Verifying file properties...'
                    VerifyingFileIntegrity             = 'Verifying file integrity...'
                    VerifyingInstance                  = 'Verifying SQL Server instance exists...'

                    TargetWithBackupWarning            = '{0} (MAKE SURE THAT YOU HAVE VERIFIED BACKUPS!)'

                    CurrentSessionIsUsingBuiltinRole   = 'Current session is using the {0} role.'
                    CurrentSessionNotUsingBuiltinRole  = 'Current session is not using the {0} role. Run this script as an {0}.'

                    OperatingSystemPass                = 'Windows operating system.'
                    OperatingSystemFail                = 'This tool only accommodates SQL Server upgrades on Windows operating systems.'

                    FileNotFound                       = 'File not found: "{0}".'
                    FileFound                          = 'File found: "{0}".'

                    FilePropertiesPass                 = 'File properties found on: "{0}" match known SQL Server installation file properties.'
                    FilePropertiesFail                 = 'File properties found on: "{0}" do not match known SQL Server installation file properties.'

                    FileIntegrityPass                  = 'File integrity confirmed: "{0}".'
                    FileIntegrityFail                  = 'File integrity failed: "{0}" Expected: "{1}" Actual: "{2}" Algorithm: "{3}".'

                    InstanceNotFound                   = 'SQL Server instance not found: "{0}"'
                    InstanceFound                      = 'SQL Server instance found: "{0}"'

                    PreFlightFailed                    = "`nOne or more pre-flight checks failed. Please remediate."

                    InstallModule                      = "Please install the VMware.PowerCLI module (reference: https://www.powershellgallery.com/packages/VMware.PowerCLI/)."
                    ImportModule                       = "Please import the VMware PowerCLI Core module (example: Import-Module -Name 'VMware.VimAutomation.Core')."
                    ConnectvCenter                     = "Please connect to vCenter (example: Connect-VIServer -Server 'vcenter.sddc.vmwarevmc.com')."

                    VmNotFound                         = 'VM with {0} "{1}" was not found using the specified filter(s).'
                    VmFound                            = 'VM(s) found:{0}'

                    VerifyingVmStatus                  = 'Verifying status of VM Name: "{0}" ID: "{1}"...'
                    VmPoweredOn                        = 'VM Name: "{0}" ID: "{1}" is powered on.'
                    VmPoweredOff                       = 'VM Name: "{0}" ID: "{1}" is powered off.'
                    VMwareToolsAreRunning              = 'VMware Tools are running in VM Name: "{0}" ID: "{1}".'
                    VMwareToolsNotRunning              = 'VMware Tools are not running in VM Name: "{0}" ID: "{1}".'

                    WhatIf                             = 'What if: Performing the operation "{0}" on target "{1}".'

                    UpgradePass                        = 'Completed successfully.'
                    UpgradeFail                        = 'Completed with errors.'
                }
            }
        }

        process {
            if ( $Key ) {
                if ( $PSCulture -in $messages.Keys ) {
                    $culture = $PSCulture
                }
                else {
                    $culture = 'en-US'
                }

                $message = $messages[$culture][$Key]
                if ( $Parameters ) {
                    $message = $message -f $Parameters
                }

                if ( $message ) {
                    $message
                }
                else {
                    $key
                }
            }
        }
    }

    # ===================================================================================
    function Write-Message {
        [CmdletBinding( DefaultParameterSetName = 'Message' )]
        [OutputType( [void] )]

        param (
            [Parameter(
                ParameterSetName = 'Message',
                Position = 0,
                ValueFromPipeline = $true
            )]
            [AllowEmptyString()]
            [string]
            $Message,

            [Parameter( ParameterSetName = 'Key' )]
            [ValidateNotNullOrEmpty()]
            [string]
            $Key,

            [Parameter( Position = 1 )]
            [ValidateSet( 'SUCCESS', 'WARNING', 'ERROR', 'INFO', 'VERBOSE' )]
            [string]
            $Mode = 'INFO',

            [Parameter(
                Position = 2,
                ValueFromRemainingArguments = $true
            )]
            [AllowNull()]
            [string[]]
            $Parameters
        )

        process {
            if ( $PSCmdlet.ParameterSetName -eq 'Key' ) {
                $Message = Get-LocalizedMessage -Key $Key -Parameters $Parameters
            }

            switch ( $Mode ) {
                'SUCCESS' {
                    Write-Host -Object "[+] ${Message}" -ForegroundColor 'Green' -BackgroundColor 'Black'
                }

                'ERROR' {
                    # Uses ForegroundColor Red and BackgroundColor Black
                    $Host.UI.WriteErrorLine( "[-] ${Message}" )

                    # Set ErrorVariable parameter and $error
                    Write-Error -Message $Message -ErrorAction 'SilentlyContinue'
                }

                'VERBOSE' {
                    Write-Verbose -Message $Message
                }

                default {
                    Write-Host -Object $Message
                }
            }
        }
    }

    # ===================================================================================
    function Test-Role {
        [CmdletBinding()]
        [OutputType( [bool] )]

        param (
            [Parameter( Position = 0 )]
            [ValidateSet(
                'AccountOperator',
                'Administrator',
                'BackupOperator',
                'Guest',
                'PowerUser',
                'PrintOperator',
                'Replicator',
                'SystemOperator',
                'User'
            )]
            [System.Security.Principal.WindowsBuiltInRole]
            $BuiltInRole = 'Administrator'
        )

        process {
            try {
                $currentSessionIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
                $currentSessionPrincipal = New-Object -TypeName 'System.Security.Principal.WindowsPrincipal' -ArgumentList $currentSessionIdentity
            }
            catch {
                Write-Message -Key 'ErrorVerifyingPSWindowsUser' -Mode 'ERROR'
                Write-Message -Message $_.Exception.Message -Mode 'ERROR'
                $false
            }

            if ( $currentSessionPrincipal.IsInRole( $BuiltInRole ) ) {
                $true
            }
            else {
                $false
            }
        }
    }

    function Out-UpgradeResult {
        [CmdletBinding()]
        [OutputType( [string] )]

        param (
            [Parameter(
                Mandatory = $true,
                Position = 0,
                ValueFromPipeline = $true
            )]
            [ValidateNotNullOrEmpty()]
            [string]
            $InputObject
        )

        process {
            $InputObject

            if ( $InputObject -match '(?:Exception type|The following error occurred):' ) {
                Write-Message -Key 'UpgradeFail' -Mode 'ERROR'
            }
            else {
                Write-Message -Key 'UpgradePass' -Mode 'SUCCESS'
            }
        }
    }
    #endregion

    # ===================================================================================
    $scriptName = $MyInvocation.MyCommand.Name
    Write-Message -Key 'Beginning' -Mode 'VERBOSE' -Parameters $scriptName

    #region Temporarily disable progress bars
    $progressBarAction = $ProgressPreference
    $ProgressPreference = 'SilentlyContinue'
    #endregion

    #region Read-only variables
    Set-Variable -Name 'FilePath' -Value $FilePath -Option 'ReadOnly' -WhatIf:$false
    Set-Variable -Name 'FileHash' -Value ( $FileHash -replace '\s', '' ).ToLower() -Option 'ReadOnly' -WhatIf:$false
    Set-Variable -Name 'Algorithm' -Value $Algorithm -Option 'ReadOnly' -WhatIf:$false
    New-Variable -Name 'parameterSetNameVmID' -Value 'Remote: VM by ID' -Option 'ReadOnly' -WhatIf:$false
    New-Variable -Name 'parameterSetNameVmName' -Value 'Remote: VM by Name' -Option 'ReadOnly' -WhatIf:$false
    New-Variable -Name 'role' -Value 'Administrator' -Option 'ReadOnly' -WhatIf:$false
    New-Variable -Name 'noProductKeyUpgrade' -Value ( Get-LocalizedMessage -Key 'NoProductKeyUpgrade' ) -Option 'ReadOnly' -WhatIf:$false
    New-Variable -Name 'name' -Value ( Get-LocalizedMessage -Key 'Name' ) -Option 'ReadOnly' -WhatIf:$false
    New-Variable -Name 'id' -Value ( Get-LocalizedMessage -Key 'ID' ) -Option 'ReadOnly' -WhatIf:$false
    New-Variable -Name 'productName' -Value ( Get-LocalizedMessage -Key 'ProductName' ) -Option 'ReadOnly' -WhatIf:$false
    New-Variable -Name 'comment' -Value ( Get-LocalizedMessage -Key 'Comment' ) -Option 'ReadOnly' -WhatIf:$false
    New-Variable -Name 'companyName' -Value ( Get-LocalizedMessage -Key 'CompanyName' ) -Option 'ReadOnly' -WhatIf:$false
    New-Variable -Name 'powerState' -Value ( Get-LocalizedMessage -Key 'PowerState' ) -Option 'ReadOnly' -WhatIf:$false
    New-Variable -Name 'local' -Value ( Get-LocalizedMessage -Key 'Local' ) -Option 'ReadOnly' -WhatIf:$false
    New-Variable -Name 'remote' -Value ( Get-LocalizedMessage -Key 'Remote' ) -Option 'ReadOnly' -WhatIf:$false
    New-Variable -Name 'vmGuestMessagePrefixLength' -Value 37 -Option 'ReadOnly' -WhatIf:$false
    #endregion
}

process {
    $parameters = $PSBoundParameters |
        Format-Table -AutoSize |
        Out-String
    Write-Message -Key 'Processing' -Mode 'VERBOSE' -Parameters $scriptName, $PSCmdlet.ParameterSetName, $parameters

    $preflightStatus = $true

    #region Build SQL Server upgrade command for this instance
    $command = "& '${FilePath}' /ACTION=Upgrade /Q /INSTANCENAME=${InstanceName} /INDICATEPROGRESS"
    if ( $IAcceptSqlServerLicenseTerms -eq $true ) {
        $command += ' /IACCEPTSQLSERVERLICENSETERMS'
    }
    if ( $null -ne $InstanceDirectory ) {
        $command += " /INSTANCEDIR='${InstanceDirectory}'"
    }
    if ( $ProductKey.Length -gt 0 ) {
        $command += " /PID=${ProductKey}"
    }
    elseif ( $PSCmdlet.ShouldProcess( $InstanceName, $noProductKeyUpgrade ) -ne $true -and $WhatIfPreference -ne $true ) {
        return
    }
    #endregion

    if ( $PSCmdlet.ParameterSetName -eq 'Local' ) {
        Write-Message -Key 'StartingPreFlight' -Mode 'VERBOSE' -Parameters $local

        #region Verify Windows operating system
        Write-Message -Key 'VerifyingOperatingSystem' -Mode 'VERBOSE'
        if ( $PSVersionTable.PSVersion.Major -lt 6 -or ( $IsCoreCLR -eq $true -and $IsWindows -eq $true ) ) {
            Write-Message -Key 'OperatingSystemPass' -Mode 'SUCCESS'
        }
        else {
            Write-Message -Key 'OperatingSystemFail' -Mode 'ERROR'
            return
        }
        #endregion

        #region Verify PowerShell is running with elevated privileges
        Write-Message -Key 'VerifyingCurrentSessionPermissions' -Mode 'VERBOSE'
        $isValid = Test-Role -BuiltInRole $role
        if ( $isValid -eq $true ) {
            Write-Message -Key 'CurrentSessionIsUsingBuiltinRole' -Mode 'SUCCESS' -Parameters $role
        }
        else {
            Write-Message -Key 'CurrentSessionNotUsingBuiltinRole' -Mode 'ERROR' -Parameters $role
            $preflightStatus = $false
        }
        #endregion

        #region Verify installation media
        Write-Message -Key 'VerifyingInstallationMediaExists' -Mode 'VERBOSE'
        if ( $FilePath.Exists -eq $true ) {
            Write-Message -Key 'FileFound' -Mode 'SUCCESS' -Parameters $FilePath.FullName
        }
        else {
            Write-Message -Key 'FileNotFound' -Mode 'ERROR' -Parameters $FilePath.FullName
            $preflightStatus = $false
        }
        #endregion

        #region Verify setup file properties
        Write-Message -Key 'VerifyingFileProperties' -Mode 'VERBOSE'
        if (
            ( $FilePath.VersionInfo.ProductName -eq $productName ) -and
            ( $FilePath.VersionInfo.Comments -eq $comment ) -and
            ( $FilePath.VersionInfo.CompanyName -eq $companyName )
        ) {
            Write-Message -Key 'FilePropertiesPass' -Mode 'SUCCESS' -Parameters $FilePath.FullName
        }
        else {
            Write-Message -Key 'FilePropertiesFail' -Mode 'ERROR' -Parameters $FilePath.FullName
            $preflightStatus = $false
        }
        #endregion

        #region Verify setup file integrity
        Write-Message -Key 'VerifyingFileIntegrity' -Mode 'VERBOSE'
        $hash = & "${Env:SystemRoot}\System32\certutil.exe" -HashFile $FilePath $Algorithm |
            Select-Object -Index 1
        # Remove whitespace and set to lowercase
        $hash = ( $hash -replace '\s', '' ).ToLower()
        if ( $FileHash -eq $hash ) {
            Write-Message -Key 'FileIntegrityPass' -Mode 'SUCCESS' -Parameters $FilePath.FullName
        }
        else {
            Write-Message -Key 'FileIntegrityFail' -Mode 'ERROR' -Parameters $FilePath.FullName, $FileHash, $hash, $Algorithm
            $preflightStatus = $false
        }
        #endregion

        #region Verify SQL Server instance exists
        Write-Message -Key 'VerifyingInstance' -Mode 'VERBOSE'

        $instances = ( Get-ItemProperty -Path 'HKLM:\Software\Microsoft\Microsoft SQL Server' ).InstalledInstances
        if ( $InstanceName -in $instances ) {
            Write-Message -Key 'InstanceFound' -Mode 'SUCCESS' -Parameters $InstanceName
        }
        else {
            Write-Message -Key 'InstanceNotFound' -Mode 'ERROR' -Parameters $InstanceName
            $preflightStatus = $false
        }
        #endregion

        #region Upgrade local SQL Server instance!
        if ( $preflightStatus -eq $true ) {
            if ( $WhatIfPreference -eq $true ) {
                Write-Message -Key 'WhatIf' -Mode 'INFO' -Parameters $command, $Env:COMPUTERNAME
            }
            else {
                $targetWithBackupWarning = Get-LocalizedMessage -Key 'TargetWithBackupWarning' -Parameters $Env:COMPUTERNAME
                if ( $PSCmdlet.ShouldProcess( $targetWithBackupWarning, $command ) -eq $true ) {
                    Write-Message -Key 'StartingUpgrade' -Parameters $local
                    try {
                        $output = Invoke-Expression -Command $command -ErrorAction 'Stop'
                    }
                    catch {
                        Write-Message -Message $_.Exception.Message -Mode 'ERROR'
                    }

                    $output |
                        Out-String |
                        Out-UpgradeResult
                }
            }
        }
        else {
            Write-Message -Key 'PreFlightFailed'
        }
        #endregion
    }
    else {
        Write-Message -Key 'StartingPreFlight' -Mode 'VERBOSE' -Parameters $remote

        # Verify that PowerCLI module is installed
        $powerCliModuleInstallState = Get-Module -Name 'VMware.VimAutomation.Core' -ListAvailable -ErrorAction 'SilentlyContinue'
        # Verify that PowerCLI module is imported
        $powerCliModuleImportState = Get-Module -Name 'VMware.VimAutomation.Core' -ErrorAction 'SilentlyContinue'
        if ( $null -eq $powerCliModuleInstallState ) {
            Write-Message -Key 'InstallModule' -Mode 'ERROR'
            $preflightStatus = $false
        }
        elseif ( $null -eq $powerCliModuleImportState ) {
            Write-Message -Key 'ImportModule' -Mode 'ERROR'
            $preflightStatus = $false
        }
        # Verify that vCenter is connected
        elseif ( $Global:DefaultVIServer.IsConnected -ne $true ) {
            Write-Message -Key 'ConnectvCenter' -Mode 'ERROR'
            $preflightStatus = $false
        }
        else {
            #region Get VM(s)
            $splat = @{
                ErrorAction = 'Stop'
            }
            if ( $PSCmdlet.ParameterSetName -eq $parameterSetNameVmID ) {
                $splat.ID = $VmID
            }
            elseif ( $PSCmdlet.ParameterSetName -eq $parameterSetNameVmName ) {
                $splat.Name = $VmName
            }

            try {
                $vms = Get-VM @splat
            }
            catch {
                Write-Message -Message $_.Exception.Message -Mode 'ERROR'
                $preflightStatus = $false
            }
            #endregion

            #region Verify that at least one VM was found
            $count = ( $vms | Measure-Object ).Count
            if ( $PSCmdlet.ParameterSetName -eq $parameterSetNameVmID -and $count -lt 1 ) {
                $vmIdList = $VmID -join ', '
                Write-Message -Key 'VmNotFound' -Mode 'ERROR' -Parameters $id, $vmIdList
                $preflightStatus = $false
            }
            elseif ( $PSCmdlet.ParameterSetName -eq $parameterSetNameVmName -and $count -lt 1 ) {
                $vmNameList = $VmName -join ', '
                Write-Message -Key 'VmNotFound' -Mode 'ERROR' -Parameters $name, $vmNameList
                $preflightStatus = $false
            }
            else {
                $vmList = $vms |
                    Format-Table -AutoSize -Property $name, @{ Name = $id; Expression = { $_.ID } }, $PowerState |
                    Out-String
                Write-Message -Key 'VmFound' -Mode 'SUCCESS' -Parameters $vmList
            }
            #endregion

            foreach ( $vm in $vms ) {
                $parameters = $vm.Name, $vm.ID

                $splat = @{
                    VM              = $vm
                    GuestCredential = $Credential
                    ScriptType      = 'PowerShell'
                    Confirm         = $false
                    WhatIf          = $false
                    ErrorAction     = 'Stop'
                }

                #region Verify VM power state
                Write-Message -Key 'VerifyingVmStatus' -Mode 'VERBOSE' -Parameters $parameters
                if ( $vm.PowerState -eq 'PoweredOn' ) {
                    Write-Message -Key 'VmPoweredOn' -Mode 'SUCCESS' -Parameters $parameters
                }
                else {
                    Write-Message -Key 'VmPoweredOff' -Mode 'ERROR' -Parameters $parameters
                    continue
                }
                #endregion

                #region Verify VMware Tools
                if ( $vm.ExtensionData.Guest.ToolsRunningStatus -eq 'guestToolsRunning' ) {
                    Write-Message -Key 'VMwareToolsAreRunning' -Mode 'SUCCESS' -Parameters $parameters
                }
                else {
                    Write-Message -Key 'VMwareToolsNotRunning' -Mode 'ERROR' -Parameters $parameters
                    continue
                }
                #endregion

                #region Verify installation media
                Write-Message -Key 'VerifyingInstallationMediaExists' -Mode 'VERBOSE'

                $splat.ScriptText = "Test-Path -Path '${FilePath}' -PathType 'Leaf'"
                try {
                    $output = Invoke-VMScript @splat
                }
                catch {
                    $message = $_.Exception.Message.ToString().Substring( $vmGuestMessagePrefixLength )
                    Write-Message -Message $message -Mode 'ERROR'
                    continue
                }

                if ( $output.ExitCode -eq 0 -and $output.ScriptOutput -match '^True' ) {
                    Write-Message -Key 'FileFound' -Mode 'SUCCESS' -Parameters $FilePath.FullName
                }
                else {
                    Write-Message -Key 'FileNotFound' -Mode 'ERROR' -Parameters $FilePath.FullName
                    continue
                }
                #endregion

                #region Verify setup file properties
                Write-Message -Key 'VerifyingFileProperties' -Mode 'VERBOSE'

                $splat.ScriptText = @"
`$file = Get-Item -Path '${FilePath}'
`$file.VersionInfo.ProductName -eq '${productName}' -and
    `$file.VersionInfo.Comments -eq '${comment}' -and
    `$file.VersionInfo.CompanyName -eq '${companyName}'
"@

                try {
                    $output = Invoke-VMScript @splat
                }
                catch {
                    $message = $_.Exception.Message.ToString().Substring( $vmGuestMessagePrefixLength )
                    Write-Message -Message $message -Mode 'ERROR'
                    continue
                }

                if ( $output.ExitCode -eq 0 -and $output.ScriptOutput -match '^True' ) {
                    Write-Message -Key 'FilePropertiesPass' -Mode 'SUCCESS' -Parameters $FilePath.FullName
                }
                else {
                    Write-Message -Key 'FilePropertiesFail' -Mode 'ERROR' -Parameters $FilePath.FullName
                    continue
                }
                #endregion

                #region Verify setup file integrity
                Write-Message -Key 'VerifyingFileIntegrity' -Mode 'VERBOSE'

                $splat.ScriptText = @"
`$hash = & "`${Env:SystemRoot}\System32\certutil.exe" -HashFile $FilePath $Algorithm |
    Select-Object -Index 1
# Remove whitespace and set to lowercase
'${FileHash}' -eq ( `$hash -replace '\s', '' ).ToLower()
"@

                try {
                    $output = Invoke-VMScript @splat
                }
                catch {
                    $message = $_.Exception.Message.ToString().Substring( $vmGuestMessagePrefixLength )
                    Write-Message -Message $message -Mode 'ERROR'
                    continue
                }

                if ( $output.ExitCode -eq 0 -and $output.ScriptOutput -match '^True' ) {
                    Write-Message -Key 'FileIntegrityPass' -Mode 'SUCCESS' -Parameters $FilePath.FullName
                }
                else {
                    Write-Message -Key 'FileIntegrityFail' -Mode 'ERROR' -Parameters $FilePath.FullName, $FileHash, $hash, $Algorithm
                    continue
                }
                #endregion

                #region Verify SQL Server instance exists
                Write-Message -Key 'VerifyingInstance' -Mode 'VERBOSE'

                $instances = ( Get-ItemProperty -Path 'HKLM:\Software\Microsoft\Microsoft SQL Server' ).InstalledInstances
                if ( $InstanceName -in $instances ) {
                    Write-Message -Key 'InstanceFound' -Mode 'SUCCESS' -Parameters $InstanceName
                }
                else {
                    Write-Message -Key 'InstanceNotFound' -Mode 'ERROR' -Parameters $InstanceName
                    $preflightStatus = $false
                }
                #endregion

                #region Upgrade remote SQL Server instance!
                $splat.ScriptText = $command
                if ( $WhatIfPreference -eq $true ) {
                    Write-Message -Key 'WhatIf' -Mode 'INFO' -Parameters $command, $vm.Name
                }
                else {
                    $targetWithBackupWarning = Get-LocalizedMessage -Key 'TargetWithBackupWarning' -Parameters $vm.Name
                    if ( $PSCmdlet.ShouldProcess( $targetWithBackupWarning, $command ) -eq $true ) {
                        Write-Message -Key 'StartingUpgrade' -Parameters $remote
                        try {
                            $output = Invoke-VMScript @splat
                        }
                        catch {
                            $message = $_.Exception.Message.ToString().Substring( $vmGuestMessagePrefixLength )
                            Write-Message -Message $message -Mode 'ERROR'
                            continue
                        }

                        Out-UpgradeResult -InputObject $output.ScriptOutput
                    }
                }
            }
        }

        if ( $preflightStatus -ne $true ) {
            Write-Message -Key 'PreFlightFailed'
        }
    }
}

end {
    $ProgressPreference = $progressBarAction

    Write-Message -Key 'Ending' -Mode 'VERBOSE' -Parameters $scriptName
}