Public/InstallUninstall/Install-CWAA.ps1

function Install-CWAA {
    <#
    .SYNOPSIS
        Installs the ConnectWise Automate Agent on the local computer.
    .DESCRIPTION
        Downloads and installs the ConnectWise Automate agent from the specified server URL.
        Supports authentication via InstallerToken (preferred) or ServerPassword. The function handles
        .NET Framework 3.5 prerequisite checks, MSI download with file integrity validation, proxy
        configuration, TrayPort conflict resolution, and post-install agent registration verification.
 
        If a previous installation is detected, the function will automatically call Uninstall-LTService
        before proceeding. The -Force parameter allows installation even when services are already present
        or when only .NET 4.0+ is available without 3.5.
    .PARAMETER Server
        One or more ConnectWise Automate server URLs to download the installer from.
        Example: https://automate.domain.com
        The function tries each server in order until a successful download occurs.
    .PARAMETER ServerPassword
        The server password that agents use to authenticate with the Automate server.
        Used for legacy deployment method. InstallerToken is preferred.
    .PARAMETER InstallerToken
        An installer token for authenticated agent deployment. This is the preferred
        authentication method over ServerPassword.
        See: https://forums.mspgeek.org/topic/5882-contribution-generate-agent-installertoken
    .PARAMETER LocationID
        The LocationID of the location the agent will be assigned to.
    .PARAMETER TrayPort
        The local port LTSvc.exe listens on for communication with LTTray processes.
        Defaults to 42000. If the port is in use, the function auto-selects the next available port.
    .PARAMETER Rename
        Renames the agent entry in Add/Remove Programs after installation by calling Rename-CWAAAddRemove.
    .PARAMETER Hide
        Hides the agent entry from Add/Remove Programs after installation by calling Hide-CWAAAddRemove.
    .PARAMETER SkipDotNet
        Skips .NET Framework 3.5 and 2.0 prerequisite checks. Use when .NET 4.0+ is already installed.
    .PARAMETER Force
        Disables safety checks including existing service detection and .NET version requirements.
    .PARAMETER NoWait
        Skips the post-install health check that waits for agent registration.
        The function exits immediately after the installer completes.
    .PARAMETER SkipCertificateCheck
        Bypasses SSL/TLS certificate validation for server connections.
        Use in lab or test environments with self-signed certificates.
    .EXAMPLE
        Install-CWAA -Server https://automate.domain.com -InstallerToken 'GeneratedToken' -LocationID 42
        Installs the agent using an InstallerToken for authentication.
    .EXAMPLE
        Install-CWAA -Server https://automate.domain.com -ServerPassword 'encryptedpass' -LocationID 1
        Installs the agent using a legacy server password.
    .EXAMPLE
        Install-CWAA -Server https://automate.domain.com -InstallerToken 'token' -LocationID 42 -NoWait
        Installs the agent without waiting for registration to complete.
    .NOTES
        Author: Chris Taylor
        Alias: Install-LTService
    .LINK
        https://github.com/christaylorcodes/ConnectWiseAutomateAgent
    #>

    [CmdletBinding(SupportsShouldProcess = $True, DefaultParameterSetName = 'deployment')]
    [Alias('Install-LTService')]
    Param(
        [Parameter(ParameterSetName = 'deployment')]
        [Parameter(ParameterSetName = 'installertoken')]
        [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $True)]
        [ValidateScript({
            if ($_ -match '^(https?://)?(([12]?[0-9]{1,2}\.){3}[12]?[0-9]{1,2}|[a-z0-9][a-z0-9_-]*(\.[a-z0-9][a-z0-9_-]*)*)$') { $true }
            else { throw "Server address '$_' is not valid. Expected format: https://automate.domain.com" }
        })]
        [string[]]$Server,
        [Parameter(ParameterSetName = 'deployment')]
        [Parameter(ValueFromPipelineByPropertyName = $True)]
        [AllowNull()]
        [Alias('Password')]
        [string]$ServerPassword,
        [Parameter(ParameterSetName = 'installertoken')]
        [ValidatePattern('(?s:^[0-9a-z]+$)')]
        [string]$InstallerToken,
        [Parameter(ValueFromPipelineByPropertyName = $True)]
        [AllowNull()]
        [int]$LocationID,
        [Parameter(ValueFromPipelineByPropertyName = $True)]
        [AllowNull()]
        [int]$TrayPort,
        [Parameter()]
        [AllowNull()]
        [string]$Rename,
        [switch]$Hide,
        [switch]$SkipDotNet,
        [switch]$Force,
        [switch]$NoWait,
        [switch]$SkipCertificateCheck
    )

    Begin {
        Write-Debug "Starting $($myInvocation.InvocationName)"

        # Lazy initialization of SSL/TLS, WebClient, and proxy configuration.
        # Only runs once per session, skips immediately on subsequent calls.
        $Null = Initialize-CWAANetworking -SkipCertificateCheck:$SkipCertificateCheck

        if (-not $Force) {
            if (Get-Service 'LTService', 'LTSvcMon' -ErrorAction SilentlyContinue) {
                if ($WhatIfPreference -ne $True) {
                    Write-Error "Services are already installed." -ErrorAction Stop
                }
                else {
                    Write-Error "What if: Stopping: Services are already installed." -ErrorAction Stop
                }
            }
        }

        if (-not ([bool](([System.Security.Principal.WindowsIdentity]::GetCurrent() | Select-Object -Expand groups -EA 0) -match 'S-1-5-32-544'))) {
            Throw 'Needs to be ran as Administrator'
        }

        if (-not $SkipDotNet) {
            $DotNET = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -Recurse -EA 0 | Get-ItemProperty -Name Version, Release -EA 0 | Where-Object { $_.PSChildName -match '^(?!S)\p{L}' } | Select-Object -ExpandProperty Version -EA 0
            if (-not ($DotNet -like '3.5.*')) {
                Write-Output '.NET Framework 3.5 installation needed.'
                $OSVersion = [System.Environment]::OSVersion.Version

                if ([version]$OSVersion -gt [version]'6.2') {
                    Try {
                        if ($PSCmdlet.ShouldProcess('NetFx3', 'Enable-WindowsOptionalFeature')) {
                            $Install = Get-WindowsOptionalFeature -Online -FeatureName 'NetFx3'
                            if ($Install.State -ne 'EnablePending') {
                                $Install = Enable-WindowsOptionalFeature -Online -FeatureName 'NetFx3' -All -NoRestart
                            }
                            if ($Install.RestartNeeded -or $Install.State -eq 'EnablePending') {
                                Write-Output '.NET Framework 3.5 installed but a reboot is needed.'
                            }
                        }
                    }
                    Catch {
                        Write-Error ".NET 3.5 install failed." -ErrorAction Continue
                        if (-not $Force) { Write-Error $Install -ErrorAction Stop }
                    }
                }
                Elseif ([version]$OSVersion -gt [version]'6.1') {
                    if ($PSCmdlet.ShouldProcess('NetFx3', 'Add Windows Feature')) {
                        Try { $Result = & "${env:windir}\system32\Dism.exe" /English /NoRestart /Online /Enable-Feature /FeatureName:NetFx3 2>'' }
                        Catch { Write-Output 'Error calling Dism.exe.'; $Result = $Null }
                        Try { $Result = & "${env:windir}\system32\Dism.exe" /English /Online /Get-FeatureInfo /FeatureName:NetFx3 2>'' }
                        Catch { Write-Output 'Error calling Dism.exe.'; $Result = $Null }
                        if ($Result -contains 'State : Enabled') {
                            Write-Warning ".Net Framework 3.5 has been installed and enabled."
                        }
                        Elseif ($Result -contains 'State : Enable Pending') {
                            Write-Warning ".Net Framework 3.5 installed but a reboot is needed."
                        }
                        else {
                            Write-Error ".NET Framework 3.5 install failed." -ErrorAction Continue
                            if (-not $Force) { Write-Error $Result -ErrorAction Stop }
                        }
                    }
                }

                $DotNET = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -Recurse | Get-ItemProperty -Name Version -EA 0 | Where-Object { $_.PSChildName -match '^(?!S)\p{L}' } | Select-Object -ExpandProperty Version
            }

            if (-not ($DotNet -like '3.5.*')) {
                if ($Force) {
                    if ($DotNet -match '(?m)^[2-4].\d') {
                        Write-Error ".NET 3.5 is not detected and could not be installed." -ErrorAction Continue
                    }
                    else {
                        Write-Error ".NET 2.0 or greater is not detected and could not be installed." -ErrorAction Stop
                    }
                }
                else {
                    Write-Error ".NET 3.5 is not detected and could not be installed." -ErrorAction Stop
                }
            }
        }

        $InstallBase = $Script:CWAAInstallerTempPath
        $logfile = 'LTAgentInstall'
        $curlog = "$InstallBase\$logfile.log"
        if ($ServerPassword -match '"') { $ServerPassword = $ServerPassword.Replace('"', '""') }
        if (-not (Test-Path -PathType Container -Path "$InstallBase\Installer")) {
            New-Item "$InstallBase\Installer" -type directory -ErrorAction SilentlyContinue | Out-Null
        }
        if (Test-Path -PathType Leaf -Path $curlog) {
            if ($PSCmdlet.ShouldProcess($curlog, 'Rotate existing log file')) {
                Get-Item -LiteralPath $curlog -EA 0 | Where-Object { $_ } | ForEach-Object {
                    Rename-Item -Path ($_ | Select-Object -Expand FullName -EA 0) -NewName "$logfile-$(Get-Date ($_ | Select-Object -Expand LastWriteTime -EA 0) -Format 'yyyyMMddHHmmss').log" -Force -Confirm:$False -WhatIf:$False
                    Remove-Item -Path ($_ | Select-Object -Expand FullName -EA 0) -Force -EA 0 -Confirm:$False -WhatIf:$False
                }
            }
        }
    }

    Process {
        if (-not ($LocationID -or $PSCmdlet.ParameterSetName -eq 'installertoken')) {
            $LocationID = '1'
        }
        if (-not ($TrayPort) -or -not ($TrayPort -ge 1 -and $TrayPort -le 65535)) {
            $TrayPort = '42000'
        }
        # Resolve the first reachable server and its advertised version
        $serverResult = Resolve-CWAAServer -Server $Server
        if ($serverResult) {
            $serverUrl = $serverResult.ServerUrl
            $serverVersion = $serverResult.ServerVersion
        }

        if ($serverResult) {
            $InstallMSI = 'Agent_Install.msi'

            # Server version detection and installer URL selection:
            # The download URL and installer format vary by server version and auth method.
            # - v240.331+: InstallerToken deployments use a ZIP containing MSI+MST (new format)
            # - v110.374+: Anonymous MSI download changed; direct location targeting removed (LT11 Patch 13)
            # - v200.197+: Fixed a critical API vulnerability (CVE, June 2020) that allowed
            # unauthenticated access to Deployment.aspx. Servers below this version get a warning.
            # - Pre-110.374: Legacy deployment URL with per-location MSI targeting
            if ($PSCmdlet.ParameterSetName -eq 'installertoken') {
                $installer = "$serverUrl/LabTech/Deployment.aspx?InstallerToken=$InstallerToken"
                if ([System.Version]$serverVersion -ge [System.Version]'240.331') {
                    Write-Debug "New MSI Installer Format Needed"
                    $InstallMSI = 'Agent_Install.zip'
                }
            }
            Elseif ($ServerPassword) {
                $installer = "$serverUrl/LabTech/Service/LabTechRemoteAgent.msi"
            }
            Elseif ([System.Version]$serverVersion -ge [System.Version]'110.374') {
                $installer = "$serverUrl/LabTech/Deployment.aspx?Probe=1&installType=msi&MSILocations=1"
            }
            else {
                Write-Warning 'The server version is not supported. Please update your Automate server.'
                $installer = "$serverUrl/LabTech/Deployment.aspx?Probe=1&installType=msi&MSILocations=$LocationID"
            }

            # Vulnerability test June 10, 2020: ConnectWise Automate API Vulnerability
            # Servers below v200.197 may allow unauthenticated access to Deployment.aspx
            if ([System.Version]$serverVersion -lt [System.Version]'200.197') {
                Try {
                    $HTTP_Request = [System.Net.WebRequest]::Create("$serverUrl/LabTech/Deployment.aspx")
                    if ($HTTP_Request.GetResponse().StatusCode -eq 'OK') {
                        $Message = @('Your server is vulnerable!!')
                        $Message += 'https://docs.connectwise.com/ConnectWise_Automate/ConnectWise_Automate_Supportability_Statements/Supportability_Statement%3A_ConnectWise_Automate_Mitigation_Steps'
                        Write-Warning ($Message | Out-String)
                    }
                }
                Catch {
                    if (-not $ServerPassword) {
                        Write-Error 'Anonymous downloads are not allowed. ServerPassword or InstallerToken may be needed.'
                    }
                }
            }

            if ($PSCmdlet.ShouldProcess($installer, 'DownloadFile')) {
                Write-Debug "Downloading $InstallMSI from $installer"
                $Script:LTServiceNetWebClient.DownloadFile($installer, "$InstallBase\Installer\$InstallMSI")
                if (-not (Test-CWAADownloadIntegrity -FilePath "$InstallBase\Installer\$InstallMSI" -FileName $InstallMSI)) {
                    $serverResult = $null
                }
            }

            if ($serverResult) {
                if ($WhatIfPreference -eq $True) {
                    $GoodServer = $serverUrl
                }
                Elseif (Test-Path "$InstallBase\Installer\$InstallMSI") {
                    $GoodServer = $serverUrl
                    Write-Verbose "$InstallMSI downloaded successfully from server $serverUrl."
                    if (($PSCmdlet.ParameterSetName -eq 'installertoken') -and [System.Version]$serverVersion -ge [System.Version]'240.331') {
                        Expand-Archive "$InstallBase\Installer\$InstallMSI" -DestinationPath "$InstallBase\Installer" -Force
                        Remove-Item "$InstallBase\Installer\$InstallMSI" -ErrorAction SilentlyContinue -Force -Confirm:$False
                        $InstallMSI = 'Agent_Install.msi'
                    }
                }
                else {
                    Write-Warning "Error encountered downloading from $serverUrl. No installation file was received."
                }
            }
        }
    }

    End {
        if ($GoodServer) {

            if ($WhatIfPreference -eq $True -and (Get-PSCallStack)[1].Command -in @('Redo-CWAA', 'Redo-LTService', 'Reinstall-CWAA', 'Reinstall-LTService')) {
                Write-Debug "Skipping Preinstall Check: Called by Redo-CWAA with -WhatIf"
            }
            else {
                if ((Test-Path $Script:CWAAInstallPath -EA 0) -or (Test-Path "${env:windir}\temp\_ltupdate" -EA 0) -or (Test-Path registry::HKLM\Software\LabTech\Service -EA 0) -or (Test-Path registry::HKLM\Software\WOW6432Node\Labtech\Service -EA 0)) {
                    Write-Warning "Previous installation detected. Calling Uninstall-CWAA"
                    Uninstall-CWAA -Server $GoodServer -Force
                    Start-Sleep 10
                }
            }

            if ($WhatIfPreference -ne $True) {
                # TrayPort conflict resolution: LTSvc.exe listens on a local TCP port (default 42000)
                # for communication with LTTray.exe (system tray UI). The valid range is 42000-42009.
                # If the requested port is occupied by another process, we scan sequentially through
                # the range, wrapping from 42009 back to 42000, trying up to 10 alternatives.
                $GoodTrayPort = $Null
                $TestTrayPort = $TrayPort
                For ($i = 0; $i -le 10; $i++) {
                    if (-not $GoodTrayPort) {
                        if (-not (Test-CWAAPort -TrayPort $TestTrayPort -Quiet)) {
                            $TestTrayPort++
                            if ($TestTrayPort -gt 42009) { $TestTrayPort = 42000 }
                        }
                        else {
                            $GoodTrayPort = $TestTrayPort
                        }
                    }
                }
                if ($GoodTrayPort -and $GoodTrayPort -ne $TrayPort -and $GoodTrayPort -ge 1 -and $GoodTrayPort -le 65535) {
                    Write-Verbose "TrayPort $TrayPort is in use. Changing TrayPort to $GoodTrayPort"
                    $TrayPort = $GoodTrayPort
                }
                Write-Output 'Starting Install.'
            }

            # Build parameter string
            $installerArguments = ($(
                "/i `"$InstallBase\Installer\$InstallMSI`""
                "SERVERADDRESS=$GoodServer"
                if (($PSCmdlet.ParameterSetName -eq 'installertoken') -and [System.Version]$serverVersion -ge [System.Version]'240.331') { "TRANSFORMS=`"Agent_Install.mst`"" }
                if ($ServerPassword -and $ServerPassword -match '.') { "SERVERPASS=`"$ServerPassword`"" }
                if ($LocationID -and $LocationID -match '^\d+$') { "LOCATION=$LocationID" }
                if ($TrayPort -and $TrayPort -ne 42000) { "SERVICEPORT=$TrayPort" }
                "/qn"
                "/l `"$InstallBase\$logfile.log`""
            ) | Where-Object { $_ }) -join ' '

            Try {
                if ($PSCmdlet.ShouldProcess("msiexec.exe $installerArguments", 'Execute Install')) {
                    $InstallAttempt = 0
                    Do {
                        if ($InstallAttempt -gt 0) {
                            Write-Warning "Service Failed to Install. Retrying in 30 seconds." -WarningAction 'Continue'
                            $timeout = New-TimeSpan -Seconds 30
                            $stopwatch = [diagnostics.stopwatch]::StartNew()
                            Write-Verbose 'Waiting for service to become available...'
                            Do {
                                Start-Sleep 5
                                $runningServiceCount = ('LTService') | Get-Service -EA 0 | Measure-Object | Select-Object -Expand Count
                            } Until ($stopwatch.elapsed -gt $timeout -or $runningServiceCount -eq 1)
                            $stopwatch.Stop()
                            Write-Verbose 'Service wait completed.'
                        }
                        $InstallAttempt++
                        $runningServiceCount = ('LTService') | Get-Service -EA 0 | Measure-Object | Select-Object -Expand Count
                        if ($runningServiceCount -eq 0) {
                            $redactedArguments = ($installerArguments -join '') -replace 'SERVERPASS="[^"]*"', 'SERVERPASS="REDACTED"'
                    Write-Verbose "Launching Installation Process: msiexec.exe $redactedArguments"
                            Start-Process -Wait -FilePath "${env:windir}\system32\msiexec.exe" -ArgumentList $installerArguments -WorkingDirectory $env:TEMP
                            Start-Sleep 5
                        }
                        $runningServiceCount = ('LTService') | Get-Service -EA 0 | Measure-Object | Select-Object -Expand Count
                    } Until ($InstallAttempt -ge 3 -or $runningServiceCount -eq 1)
                    if ($runningServiceCount -eq 0) {
                        Write-Error "LTService was not installed. Installation failed."
                        Return
                    }
                }
                if (($Script:LTProxy.Enabled) -eq $True) {
                    Write-Verbose 'Proxy Configuration Needed. Applying Proxy Settings to Agent Installation.'
                    if ($PSCmdlet.ShouldProcess($Script:LTProxy.ProxyServerURL, 'Configure Agent Proxy')) {
                        $runningServiceCount = ('LTService') | Get-Service -EA 0 | Where-Object { $_.Status -eq 'Running' } | Measure-Object | Select-Object -Expand Count
                        if ($runningServiceCount -ne 0) {
                            $timeout = New-TimeSpan -Minutes 2
                            $stopwatch = [diagnostics.stopwatch]::StartNew()
                            Write-Verbose 'Waiting for service to start...'
                            Do {
                                Start-Sleep 2
                                $runningServiceCount = ('LTService') | Get-Service -EA 0 | Where-Object { $_.Status -eq 'Running' } | Measure-Object | Select-Object -Expand Count
                            } Until ($stopwatch.elapsed -gt $timeout -or $runningServiceCount -eq 1)
                            $stopwatch.Stop()
                            if ($runningServiceCount -eq 1) {
                                Write-Debug "LTService Initial Startup Successful."
                            }
                            else {
                                Write-Debug "LTService Initial Startup failed to complete within expected period."
                            }
                            Write-Verbose 'Service wait completed.'
                        }
                        Set-CWAAProxy -ProxyServerURL $Script:LTProxy.ProxyServerURL -ProxyUsername $Script:LTProxy.ProxyUsername -ProxyPassword $Script:LTProxy.ProxyPassword -Confirm:$False -WhatIf:$False
                    }
                }
                else {
                    Write-Verbose 'No Proxy Configuration has been specified - Continuing.'
                }
                if (-not $NoWait -and $PSCmdlet.ShouldProcess('LTService', 'Monitor For Successful Agent Registration')) {
                    $timeout = New-TimeSpan -Minutes 15
                    $stopwatch = [diagnostics.stopwatch]::StartNew()
                    Write-Verbose 'Waiting for agent to register...'
                    Do {
                        Start-Sleep 5
                        $tempServiceInfo = (Get-CWAAInfo -EA 0 -Verbose:$False -WhatIf:$False -Confirm:$False -Debug:$False | Select-Object -Expand 'ID' -EA 0)
                    } Until ($stopwatch.elapsed -gt $timeout -or $tempServiceInfo -ge 1)
                    $stopwatch.Stop()
                    Write-Verbose "Agent registration wait completed after $(([int32]$stopwatch.Elapsed.TotalSeconds).ToString()) seconds."
                    $Null = Get-CWAAProxy -ErrorAction Continue
                }
                if ($Hide) { Hide-CWAAAddRemove }
            }

            Catch {
                Write-Error "There was an error during the install process. $_"
                Write-CWAAEventLog -EventId 1002 -EntryType Error -Message "Agent installation failed. Error: $($_.Exception.Message)"
                Return
            }

            if ($WhatIfPreference -ne $True) {
                # Cleanup install files
                Remove-Item "$InstallBase\Installer\$InstallMSI" -ErrorAction SilentlyContinue -Force -Confirm:$False
                Remove-Item "$InstallBase\Installer\Agent_Install.mst" -ErrorAction SilentlyContinue -Force -Confirm:$False
                @($curlog, "$Script:CWAAInstallPath\Install.log") | ForEach-Object {
                    if (Test-Path -PathType Leaf -LiteralPath $_) {
                        $logcontents = Get-Content -Path $_
                        $logcontents = $logcontents -replace '(?<=PreInstallPass:[^\r\n]+? (?:result|value)): [^\r\n]+', ': <REDACTED>'
                        if ($logcontents) { Set-Content -Path $_ -Value $logcontents -Force -Confirm:$False }
                    }
                }

                $tempServiceInfo = Get-CWAAInfo -EA 0 -Verbose:$False -WhatIf:$False -Confirm:$False -Debug:$False
                if ($tempServiceInfo) {
                    if (($tempServiceInfo | Select-Object -Expand 'ID' -EA 0) -ge 1) {
                        Write-Output "Automate agent has been installed successfully. Agent ID: $($tempServiceInfo | Select-Object -Expand 'ID' -EA 0) LocationID: $($tempServiceInfo | Select-Object -Expand 'LocationID' -EA 0)"
                        Write-CWAAEventLog -EventId 1000 -EntryType Information -Message "Agent installed successfully. Agent ID: $($tempServiceInfo | Select-Object -Expand 'ID' -EA 0), LocationID: $($tempServiceInfo | Select-Object -Expand 'LocationID' -EA 0)"
                    }
                    Elseif (-not $NoWait) {
                        Write-Error "Automate agent installation completed but agent failed to register within expected period." -ErrorAction Continue
                        Write-CWAAEventLog -EventId 1001 -EntryType Warning -Message "Agent installed but failed to register within expected period."
                    }
                    else {
                        Write-Warning "Automate agent installation completed but agent did not yet register." -WarningAction Continue
                    }
                }
                else {
                    if ($Error) {
                        Write-Error "There was an error installing Automate agent. Check the log, $InstallBase\$logfile.log"
                        Write-CWAAEventLog -EventId 1002 -EntryType Error -Message "Agent installation failed. Check log: $InstallBase\$logfile.log"
                        Return
                    }
                    Elseif (-not $NoWait) {
                        Write-Error "There was an error installing Automate agent. Check the log, $InstallBase\$logfile.log"
                        Write-CWAAEventLog -EventId 1002 -EntryType Error -Message "Agent installation failed. Check log: $InstallBase\$logfile.log"
                        Return
                    }
                    else {
                        Write-Warning "Automate agent installation may not have succeeded." -WarningAction Continue
                    }
                }
            }
            if ($Rename -and $Rename -notmatch 'False') { Rename-CWAAAddRemove -Name $Rename }
        }
        Elseif ($WhatIfPreference -ne $True) {
            Write-Error "No valid server was reached to use for the install."
        }
        Write-Debug "Exiting $($myInvocation.InvocationName)"
    }
}