From f13f6e1105073204625a54e2e0745e3c89423401 Mon Sep 17 00:00:00 2001 From: Charlie Silverman Date: Fri, 24 Jul 2026 17:37:31 -0700 Subject: [PATCH] Added Horizon Tools --- Horizon Tools/HorizonPoolExport.ps1 | 315 +++++++++++ Horizon Tools/KISHorizon.psm1 | 688 ++++++++++++++++++++++++ Horizon Tools/KISVDIDesktopResolver.ps1 | 210 ++++++++ 3 files changed, 1213 insertions(+) create mode 100644 Horizon Tools/HorizonPoolExport.ps1 create mode 100644 Horizon Tools/KISHorizon.psm1 create mode 100644 Horizon Tools/KISVDIDesktopResolver.ps1 diff --git a/Horizon Tools/HorizonPoolExport.ps1 b/Horizon Tools/HorizonPoolExport.ps1 new file mode 100644 index 0000000..19cd2b5 --- /dev/null +++ b/Horizon Tools/HorizonPoolExport.ps1 @@ -0,0 +1,315 @@ +param( + [string]$HorizonServer = "", + [string]$OutFile = "", + [string]$InputXML = "", + [switch]$SaveCred = $false, + [switch]$debug = $false, + [switch]$help = $false +) + + +Write-Host $global:kishorizondebug + +# Variables +$fileVersion = "1.1.0" +$fileDate = "2026-06-24" +$scriptName = Split-Path -Path $PSCommandPath -Leaf +$mypath = split-path -parent $MyInvocation.MyCommand.Definition +$now = (Get-Date).ToString("yyyy-MM-dd_HH:mm") +$userfolder = $env:USERPROFILE + +# Display Script Banner +Write-Host "--------------------------------------------------" -ForegroundColor White +Write-Host "Export Pool Settings to Excel File" -ForegroundColor Green +Write-Host "Version $($FileVersion) ($($FileDate))" -ForegroundColor Green +Write-Host "Written by Charlie Silverman" -ForegroundColor Green +Write-Host "--------------------------------------------------" -ForegroundColor White + +function CheckModuleInstalled($moduleName, $command) { + Write-Host "Checking that package $moduleName is installed..." -NoNewLine + if (-not(get-command $command -erroraction silentlycontinue)) { + Write-Host " Missing" -ForegroundColor Red + if (-not (Get-Module -ListAvailable -Name $moduleName)) { + $response = "" + while (($response -ne "yes") -and ($response -ne "no")) { + $response = (Read-Host "$moduleName module not instaled. Install now? (yes/no)").ToLower() + } + if ($response -eq "no") { + Write-Host "Operation aborted." -ForegroundColor Yellow + exit + } + Write-Host "Installing PowerShell module $moduleName..." -NoNewLine + Install-Module -Name $moduleName -Scope CurrentUser -Force + Write-Host " Success" -ForegroundColor Green + } + Write-Host "Loading PowerShell module $moduleName..." -NoNewLine + Import-Module $moduleName + Write-Host " Success" -ForegroundColor Green + } else { + Write-Host " Found" -ForegroundColor Green + } +} + +# Check for ImportExcel and install if missing +CheckModuleInstalled -moduleName "VCF.PowerCLI" -command "connect-viserver" +CheckModuleInstalled -moduleName "ImportExcel" -command "Open-ExcelPackage" +Import-Module $mypath\KISHorizon.psm1 +$abyss = Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Scope Session -Confirm:$false + +if ($Help) { + Write-Host + Write-Host "Purpose: Resolve Horizon VMs with errors." + Write-Host "Command Syntax:" + Write-Host " $scriptName (-Horizon ""[cluster]"") (-SaveCred)" -ForegroundColor Green + Write-Host "Command Line Options:" + Write-Host + Write-Host -NoNewLine " -Horizon " -ForegroundColor Green + Write-Host "- The FQDN or IP address of a Horizon connection broker." + Write-Host -NoNewLine " -SaveCred " -ForegroundColor Green + Write-Host "- Ask for credentials and save them to current user's profile directory." + Write-Host + Write-Host "The Horizon server will be queried for all vCenter servers it is configured for and credentials will be required for each." + Write-Host "Any information not provided will be prompted for during script execution." + + exit +} + +if ($OutFile -eq "") { + $OutFile = "KISHorizonExport-$($HorizonServer).xlsx" +} + +if (Test-Path -path $Outfile -PathType Leaf) { + Remove-Item -Path $Outfile +} + +if ($inputXML -eq "") { + if ($HorizonServer -eq "") { + $HorizonServer = Read-Host "Enter FQDN or IP address of Horizon Connection Broker" + } + + if ($SaveCred) { + $hzcred = Get-Credential -Message "Enter credentials for Horizon server $HorizonServer in the format DOMAIN\USERNAME" + $hzcred | Export-Clixml -path $userfolder\kistools-$HorizonServer.cred + Write-Host "Saving credentials." + } + + if (Test-Path -path $userfolder\kistools-$HorizonServer.cred -PathType Leaf) { + $hzcred = Import-Clixml -path $userfolder\kistools-$HorizonServer.cred + Write-Host "- Using stored Horizon credentials" + } else { + $hzcred = Get-Credential -Message "Enter credentials for Horizon server $HorizonServer in the format DOMAIN\USERNAME" + if (!$hzcreds) { + Write-Host "ERROR: No credentials were entered" -foregroundcolor yellow + exit + } + } + + #Establish connection to Horizon + try { + Write-Host -NoNewLine "Connecting to Horizon Connection Broker $HorizonServer... " + $hzconn = Connect-HorizonServer -server $HorizonServer -credential $hzcred + Write-Host "Success" -ForegroundColor green + } catch { + Write-Host "Failure" -ForegroundColor Yellow -BackgroundColor Red -NoNewLine + Write-Host + exit + } + + # Establish connection to all vCenter servers configured on this Horizon system + $vcenterlist = $hzconn | Get-HorizonvCenter + foreach ($thisvc in $vcenterlist) { + $vcname = $thisvc.server_name + + if ($SaveCred) { + $vccred = Get-Credential -Message "Enter credentials for vCenter server $vcname" + $vccred| Export-Clixml -path $userfolder\kistools-$vcname.cred + Write-Host "Saving credentials." + } + + if (Test-Path -path $userfolder\kistools-$vcname.cred -PathType Leaf) { + $vccred = Import-Clixml -path $userfolder\kistools-$vcname.cred + Write-Host "- Using stored vCenter credentials" + } else { + $vccred = Get-Credential -Message "Enter credentials for vCenter server $vcname" + if (!$vccred) { + Write-Host "ERROR: No credentials were entered" -foregroundcolor yellow + exit + } + } + + #Establish connection to vCenter + try { + Write-Host -NoNewLine "Connecting to vCenter $vcname... " + $vcconn = Connect-VIServer -server $vcname -Credential $vccred -Force -ErrorAction Stop + Write-Host "Success" -ForegroundColor green + } catch { + Write-Host "Failure" -ForegroundColor Yellow -BackgroundColor Red -NoNewLine + Write-Host + Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__ + Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription + exit + } + } + + $pools = $hzconn | Get-HorizonDesktopPool +} else { + $pools = Import-CliXml -path $inputXML +} + +foreach ($thisvc in $vcenterlist) { + Write-Host -NoNewLine "Disconnecting from vCenter $vcname... " + try { + $abyss = Disconnect-ViServer $thisvc.server_name -confirm:$false + Write-Host "Success" -ForegroundColor green + } catch { + Write-Host "Failure" -ForegroundColor Yellow -BackgroundColor Red -NoNewLine + Write-Host + } +} + +Write-Host -NoNewLine "Generating Excel File $OutFile... " +$workbook = Open-ExcelPackage -Path $OutFile -Create + +#Create Initial Data Worksheet +$thissheet = Add-Worksheet -ExcelPackage $workbook -WorksheetName "General Horizon Data" +Set-ExcelRange -worksheet $thissheet -Range "A:A" -VerticalAlignment Top +Set-Format -Address $thissheet.Cells["B:B"] -WrapText +$thissheet.column(1).Width = 40 +$thissheet.column(2).Width = 40 +$thissheet.column(3).Width = 40 +Set-ExcelRange -Worksheet $thissheet -Range "A1:E1" -Merge:$true +$domains = Get-HorizonADDomains -Horizon $hzconn +$vcenterservers = Get-HorizonvCenter -Horizon $hzconn +$goldimages = $pools.provisioning_settings.base_vm_name | Select-Object -Unique +$row = 1 +$thissheet.cells["A1"].value = "General data for Horizon server $HorizonServer" +Set-Format -Address $thissheet.Cells["A1"] -Bold +Set-ExcelRange -Worksheet $thissheet -Range "A1" -FontSize 20 +$row += 2 +$thissheet.cells["A" + $row].value = "vCenter Servers" +Set-Format -Address $thissheet.Cells["A" + $row] -Bold +Set-ExcelRange -Worksheet $thissheet -Range "A$row" -FontSize 16 + +$row += 1 +$vctabletop = $row +$thissheet.cells["A" + $row].value = "vCenter Server" +$thissheet.cells["B" + $row].value = "Version" +$thissheet.cells["C" + $row].value = "User Account" +$row += 1 +foreach ($vc in $vcenterlist) { + $thissheet.cells["A" + $row].value = $vc.server_name + $thissheet.cells["B" + $row].value = $vc.version + $thissheet.cells["C" + $row].value = $vc.user_name + $row += 1 +} +$vcTableRange = $thissheet.Cells[$vctabletop, 1, ($row-1), 3] +$vcTable = $thissheet.Tables.Add($vcTableRange, "vCenterList") +$vcTable.StyleName = "TableStyleMedium15" + +$row += 1 +$thissheet.cells["A" + $row].value = "Active Directory Domains" +Set-Format -Address $thissheet.Cells["A" + $row] -Bold +Set-ExcelRange -Worksheet $thissheet -Range "A$row" -FontSize 16 +$row += 1 +$adtabletop = $row +$thissheet.cells["A" + $row].value = "DNS Name" +$thissheet.cells["B" + $row].value = "NetBIOS Name" +$row += 1 +foreach ($add in $domains) { + $thissheet.cells["A" + $row].value = $add.dns_name + $thissheet.cells["B" + $row].value = $add.netbios_name + $row += 1 +} +$adTableRange = $thissheet.Cells[$adtabletop, 1, ($row-1), 2] +$adTable = $thissheet.Tables.Add($adTableRange, "ADDomains") +$adTable.StyleName = "TableStyleMedium15" + +$row += 1 +$thissheet.cells["A" + $row].value = "Published Gold Images" +Set-Format -Address $thissheet.Cells["A" + $row] -Bold +Set-ExcelRange -Worksheet $thissheet -Range "A$row" -FontSize 16 +$row += 1 +$adtabletop = $row +$thissheet.cells["A" + $row].value = "Gold Image VMs" +$thissheet.cells["B" + $row].value = "Pools" +$row += 1 +foreach ($image in $goldimages) { + $thissheet.cells["A" + $row].value = $image + $thissheet.cells["B" + $row].value = (($pools | Where-Object {$_.provisioning_settings.base_vm_name -eq $image}).Name -Join "`r`n") + $row += 1 +} +$giTableRange = $thissheet.Cells[$adtabletop, 1, ($row-1), 2] +$giTable = $thissheet.Tables.Add($giTableRange, "GoldImages") +$giTable.StyleName = "TableStyleMedium15" + +foreach ($pool in $pools) { + $thissheet = Add-Worksheet -ExcelPackage $workbook -WorksheetName $pool.name + $entitlements = $pool.entitlements | ForEach-Object { + $_.Domain + "\" + $_.SamAccountName + } + + $data = @( + [pscustomobject]@{Name = "Pool ID";Value=$pool.name} + [pscustomobject]@{Name = "Display Name";Value=$pool.display_name} + [pscustomobject]@{Name = "Description";Value=$pool.description} + [pscustomobject]@{Name = "Type";Value=$pool.session_type} + [pscustomobject]@{Name = "User Assignment";Value=$pool.user_assignment} + [pscustomobject]@{Name = "vCenter Server";Value=$pool.vcenter} + [pscustomobject]@{Name = "Naming Pattern";Value=$pool.pattern_naming_settings.naming_pattern} + [pscustomobject]@{Name = "Max Machines";Value=$pool.pattern_naming_settings.max_number_of_machines} + [pscustomobject]@{Name = "Spare Machines";Value=$pool.pattern_naming_settings.number_of_spare_machines} + [pscustomobject]@{Name = "Min Machines";Value=$pool.pattern_naming_settings.min_number_of_machines} + [pscustomobject]@{Name = "Datacenter";Value=$pool.provisioning_settings.datacenter_name} + [pscustomobject]@{Name = "Resource Pool";Value=$pool.provisioning_settings.resource_pool_name} + [pscustomobject]@{Name = "Parent VM";Value=$pool.provisioning_settings.base_vm_name} + [pscustomobject]@{Name = "Snapshot Path";Value=$pool.provisioning_settings.snapshot_path} + [pscustomobject]@{Name = "VM Folder Path";Value=$pool.provisioning_settings.base_folder_path} + [pscustomobject]@{Name = "Cluster";Value=$pool.cluster} + [pscustomobject]@{Name = "Resource Pool";Value=$pool.provisioning_settings.resource_pool_name} + [pscustomobject]@{Name = "Datacenter";Value=$pool.provisioning_settings.datacenter_name} + [pscustomobject]@{Name = "Datastores";Value=$pool.storage_settings.datastores.name -join "`r`n"} + [pscustomobject]@{Name = "Network";Value=($pool.machines.managed_machine_data.network_labels.network_label_name | Select-Object -Unique | Sort-Object) -join "`r`n"} + [pscustomobject]@{Name = "Use vSAN";Value=$pool.storage_settings.use_vsan} + [pscustomobject]@{Name = "Add Virtual TPM";Value=$pool.provisioning_settings.add_virtual_tpm} + [pscustomobject]@{Name = "vGPU Enabled";Value=$pool.GpuEnabled} + [pscustomobject]@{Name = "Customization Type";Value=$pool.customization_settings.customization_type} + [pscustomobject]@{Name = "Ad Container";Value=$pool.customization_settings.ad_container_rdn} + [pscustomobject]@{Name = "Reuse Existing AD Object";Value=$pool.customization_settings.reuse_pre_existing_accounts} + [pscustomobject]@{Name = "Cloneprep Script Settings";Value=$pool.customization_settings.cloneprep_script_settings} + [pscustomobject]@{Name = "Power Off Script Name";Value=$pool.customization_settings.cloneprep_customization_settings.power_off_script_name} + [pscustomobject]@{Name = "Power Off Script Parameters";Value=$pool.customization_settings.cloneprep_customization_settings.power_off_script_parameters} + [pscustomobject]@{Name = "Post Sync Script Name";Value=$pool.customization_settings.cloneprep_customization_settings.post_synchronization_script_name} + [pscustomobject]@{Name = "Post Sync Script Parameters";Value=$pool.customization_settings.cloneprep_customization_settings.post_synchronization_script_parameters} + [pscustomobject]@{Name = "Automatic Logoff Policy";Value=$pool.session_settings.disconnected_session_timeout_policy} + [pscustomobject]@{Name = "Automatic Logoff Minutes";Value=$pool.session_settings.disconnected_session_timeout_minutes} + [pscustomobject]@{Name = "Allow Users to Reset Machines";Value=$pool.session_settings.allow_users_to_reset_machines} + [pscustomobject]@{Name = "Allow Multiple Sessions Per User";Value=$pool.session_settings.allow_multiple_sessions_per_user} + [pscustomobject]@{Name = "Used VM Policy";Value=$pool.session_settings.delete_or_refresh_machine_after_logoff} + [pscustomobject]@{Name = "Logoff After Timeout";Value=$pool.session_settings.disconnected_session_timeout_policy} + [pscustomobject]@{Name = "Supported Display Protocols";Value=$pool.display_protocol_settings.display_protocols -join "`r`n"} + [pscustomobject]@{Name = "Default Display Protocol";Value=$pool.display_protocol_settings.default_display_protocol} + [pscustomobject]@{Name = "Allow Users to Choose Protocol";Value=$pool.display_protocol_settings.allow_users_to_choose_protocol} + [pscustomobject]@{Name = "External HTML Access";Value=$pool.display_protocol_settings.html_access_enabled} + [pscustomobject]@{Name = "Enable Collaboration";Value=$pool.display_protocol_settings.session_collaboration_enabled} + [pscustomobject]@{Name = "Entitlements";Value=$entitlements} + ) + + $rowcount = 1 + foreach ($datum in $data) { + $thissheet.cells["A" + $rowcount].Value = $datum.name + $thissheet.cells["B" + $rowcount].Value = $datum.value + $rowcount += 1 + } + + $thissheet.Column(1).Style.HorizontalAlignment = "Right" + $thissheet.Column(1).Width = 40 + $thissheet.Column(2).Width = 200 + Set-Column -ExcelPackage $workbook -WorksheetName $pool.Name -Column 2 -WrapText + Set-Column -ExcelPackage $workbook -WorksheetName $pool.Name -Column 2 -HorizontalAlignment Left + Set-Column -ExcelPackage $workbook -WorksheetName $pool.Name -Column 1 -VerticalAlignment Top + Set-ExcelRange -Worksheet $thissheet -Range "A:B" -FontSize 14 + $thissheet.cells["A:A"].Style.Indent = 2 + Set-ExcelRange -Worksheet $thissheet -Range "B:B" -Bold +} +Close-ExcelPackage $workbook +Write-Host "Success" -ForegroundColor green diff --git a/Horizon Tools/KISHorizon.psm1 b/Horizon Tools/KISHorizon.psm1 new file mode 100644 index 0000000..5115219 --- /dev/null +++ b/Horizon Tools/KISHorizon.psm1 @@ -0,0 +1,688 @@ +$global:request_header = @{ + "accept"="*/*" + "content-type"="application/json" +} + +function Test-Server { + param ( + [Parameter(Mandatory)] [string]$server + ) + + try { + $result = (new-object System.Net.Sockets.TcpClient).ConnectAsync($server,443).Wait(500) + } catch { + $result = $false + } + + return $result +} + +function Get-HorizonKeyAgreement { + param ( + [Parameter(Mandatory)] [string]$server + ) + + $login_headers = $global:request_header + + $kaUri = "https://$server/rest/key-agreement" + + $body = '{"identifier":"string","nonce":"string","public_key":"string","supported_schemes": ["SCHEME-AES2"]}' + + if ($global:kishorizondebug) { + Write-Host $MyInvocation.MyCommand.Name Starting + } + + if (Test-Server -server $server) { + try { + $response = Invoke-RestMethod -Method Post -Uri $kaUri -Body $body -Headers $login_headers -SessionVariable websession -SkipCertificateCheck + } catch { + Write-Host "ERROR accessing $loginUri" + Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__ + Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription + } + if ($response -ne $null) { + $response | Add-Member -MemberType NoteProperty -Name server_name -Value $server + $response | Add-Member -MemberType NoteProperty -Name session -Value $websession + $response | Add-Member -MemberType NoteProperty -Name user_name -Value "$domain\$username" + } + + return $response + } else { + throw "Error: Unable to verify server $server on port 443." + } +} + + +function Connect-HorizonServer { + param ( + [System.Management.Automation.PSCredential]$credential, + [Parameter(Mandatory)] [string]$Server + ) + + $loginUri = "https://$server/rest/login" + + if ($credential) { + $username = $credential.UserName.split("\")[1] + $domain = $credential.UserName.split("\")[0] + $password = $credential.GetNetworkCredential().Password + } + + $login_headers = $global:request_header + + $body = '{"domain":"' + $domain + '","password":"' + $password + '","username":"' + $username + '"}' + + if ($global:kishorizondebug) { + Write-Host $MyInvocation.MyCommand.Name Starting + } + + if (Test-Server -server $server) { + try { + $response = Invoke-RestMethod -Method Post -Uri $loginUri -Body $body -Headers $login_headers -SessionVariable websession -SkipCertificateCheck + } catch { + Write-Host "ERROR accessing $loginUri" + Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__ + Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription + } + $response | Add-Member -MemberType NoteProperty -Name server_name -Value $server + $response | Add-Member -MemberType NoteProperty -Name session -Value $websession + $response | Add-Member -MemberType NoteProperty -Name user_name -Value "$domain\$username" + + return $response + } else { + throw "Error: Unable to verify server $server on port 443." + } +} + +function Refresh-HorizonConnection { + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] $Horizon + ) + + $token = $Horizon.access_token + $server = $Horizon.server_name + + $request_header = $global:request_header + $request_header += @{"authorization"="Bearer $token"} + $body = @{"refresh_token"="$token"} + + $refreshUri = "https://$server/rest/refresh" + + Write-Host $body + try { + $result = Invoke-RestMethod -Method Post -Uri $refreshUri -Headers $request_header -Body $body -SkipCertificateCheck + } catch { + Write-Error -Message "ERROR accessing $refreshUri`nStatusCode: $($_.Exception.Response.StatusCode.value__)`nStatusDescription: $($_.Exception.Response.StatusDescription)" + return + } + + return $result +} + +function Get-HorizonMachine { + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] $Horizon + ) + $token = $Horizon.access_token + $server = $Horizon.server_name + + $request_header = $global:request_header + $request_header += @{"authorization"="Bearer $token"} + + $machineUri = "https://$server/rest/inventory/v5/machines?sort_by=name&order_by=ASC" + + try { + $machines = Invoke-RestMethod -Method Get -Uri $machineUri -Headers $request_header -SkipCertificateCheck + } catch { + throw "ERROR accessing $machineUri`nStatusCode: $($_.Exception.Response.StatusCode.value__)`nStatusDescription: $($_.Exception.Response.StatusDescription)" + } + + return $machines +} + +function Remove-HorizonMachine { + param ( + [string]$id = "", + [parameter(Mandatory = $true, ValueFromPipeline = $true)] $Horizon + ) + + $token = $Horizon.access_token + $server = $Horizon.server_name + $ids = $id.split(",") + if ($ids.length -gt 1) { + $body = "{""machine_ids"":[" + foreach ($thisid in $ids) { + $thisid = $thisid.Trim('"') + if ($body -ne "{""machine_ids"":[") { $body += "," } + $body += '"' + $thisid + '"' + } + $body += ']}' + } else { + $body = ']"{""machine_ids"":[""' + $id + '""]}' + } + + $request_header = $global:request_header + $request_header += @{"authorization"="Bearer $token"} + + $machineUri = "https://$server/rest/inventory/v1/machines" + + if ($global:kishorizondebug) { + Write-Host $MyInvocation.MyCommand.Name Starting + } + + try { + $result = Invoke-RestMethod -Method DELETE -Uri $machineUri -Body $body -Headers $request_header -SkipCertificateCheck -ErrorAction:SilentlyContinue + } catch { + $_.Exception.Response.StatusCode.value + $_.Exception.Response.StatusDescription + throw "ERROR accessing $machineUri`nStatusCode: $($_.Exception.Response.StatusCode.value__)`nStatusDescription: $($_.Exception.Response.StatusDescription)" + } + + return $result +} + +function Get-HorizonSession { + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] $Horizon + ) + $token = $Horizon.access_token + $server = $Horizon.server_name + + $request_header = $global:request_header + $request_header += @{"authorization"="Bearer $token"} + + $sessionUri = "https://$server/rest/inventory/v2/sessions?sort_by=session_type&order_by=ASC" + $userURI = "https://$server/rest/external/v2/ad-users-or-groups/**ID**" + + if ($global:kishorizondebug) { + Write-Host $MyInvocation.MyCommand.Name Starting + } + + try { + $sessions = Invoke-RestMethod -Method Get -Uri $sessionUri -Headers $request_header -SkipCertificateCheck + $sessions | Add-Member -MemberType NoteProperty -Name user_name -Value "" + $sessions | Add-Member -MemberType NoteProperty -Name user_principal_name -Value "" + $sessions | Add-Member -MemberType NoteProperty -Name session_length -Value 0 + $sessions | Add-Member -MemberType NoteProperty -Name connected_length -Value 0 + + } catch { + throw "ERROR accessing $sessionUri`nStatusCode: $($_.Exception.Response.StatusCode.value__)`nStatusDescription: $($_.Exception.Response.StatusDescription)" + } + + foreach($session in $sessions) { + $user = $Horizon | Get-HorizonADUser -id $session.user_id + + $session.user_name = $user.login_name + $session.user_principal_name = $user.user_principal_name + + $session.session_length = Get-HorizonSessionLength -session $session + $session.connected_length = Get-HorizonSessionConnectedLength -session $session + } + + return $sessions +} + +function Get-HorizonVcenter { + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] $Horizon + ) + $token = $Horizon.access_token + $server = $Horizon.server_name + + $request_header = $global:request_header + $request_header += @{"authorization"="Bearer $token"} + + $vcuri = "https://$server/rest/config/v1/virtual-centers" + + if ($global:kishorizondebug) { + Write-Host $MyInvocation.MyCommand.Name Starting + } + + try { + $vcs = Invoke-RestMethod -Method Get -Uri $vcUri -Headers $request_header -SkipCertificateCheck + + } catch { + throw "ERROR accessing $vcUri`nStatusCode: $($_.Exception.Response.StatusCode.value__)`nStatusDescription: $($_.Exception.Response.StatusDescription)" + } + + return $vcs +} + +function Get-HorizonBaseVMs { + param ( + [Parameter(Mandatory)] [string]$vcid, + [parameter(Mandatory = $true, ValueFromPipeline = $true)] $Horizon + ) + $token = $Horizon.access_token + $server = $Horizon.server_name + + $request_header = $global:request_header + $request_header += @{"authorization"="Bearer $token"} + + $bvuri = "https://$server/rest/external/v1/base-vms?vcenter_id=$vcid&filter_incompatible_vms=true" + + if ($global:kishorizondebug) { + Write-Host $MyInvocation.MyCommand.Name Starting + } + + try { + $bvs = Invoke-RestMethod -Method Get -Uri $bvUri -Headers $request_header -SkipCertificateCheck + + } catch { + throw "ERROR accessing $bvUri`nStatusCode: $($_.Exception.Response.StatusCode.value__)`nStatusDescription: $($_.Exception.Response.StatusDescription)" + } + + return $bvs +} + +function Get-HorizonBaseSnapshots { + param ( + [Parameter(Mandatory)] [string]$vcid, + [Parameter(Mandatory)] [string]$vmid, + [parameter(Mandatory = $true, ValueFromPipeline = $true)] $Horizon + ) + $token = $Horizon.access_token + $server = $Horizon.server_name + + $request_header = $global:request_header + $request_header += @{"authorization"="Bearer $token"} + + $bsuri = "https://$server/rest/external/v1/base-vms?vcenter_id=$vcid&base_vm_id=$vmid" + + if ($global:kishorizondebug) { + Write-Host $MyInvocation.MyCommand.Name Starting + } + + try { + $bss = Invoke-RestMethod -Method Get -Uri $bsUri -Headers $request_header -SkipCertificateCheck + + } catch { + throw "ERROR accessing $bsUri`nStatusCode: $($_.Exception.Response.StatusCode.value__)`nStatusDescription: $($_.Exception.Response.StatusDescription)" + } + + return $bss +} + +function Get-HorizonEntitlements { + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] $Horizon + ) + $token = $Horizon.access_token + $server = $Horizon.server_name + + $request_header = $global:request_header + $request_header += @{"authorization"="Bearer $token"} + + $enturi = "https://$server/rest/entitlements/v1/desktop-pools" + + if ($global:kishorizondebug) { + Write-Host $MyInvocation.MyCommand.Name Starting + } + + try { + $entitlements = Invoke-RestMethod -Method Get -Uri $entUri -Headers $request_header -SkipCertificateCheck + + } catch { + throw "ERROR accessing $entUri`nStatusCode: $($_.Exception.Response.StatusCode.value__)`nStatusDescription: $($_.Exception.Response.StatusDescription)" + } + + return $entitlements +} + +Function Get-HorizonEntitlementDetail { + param ( + [pscustomobject] $entitlements, + [parameter(Mandatory = $true, ValueFromPipeline = $true)] $Horizon + ) + + $domains = $Horizon | Get-HorizonAdDomains + + if ($global:kishorizondebug) { + Write-Host $MyInvocation.MyCommand.Name Starting + } + + $result = @() + foreach ($entitlement in $entitlements) { + $entsid = $entitlement + $user = $Horizon | Get-HorizonADUser -id $entitlement + + $object = $null + if ($user) { + if ($user.group) { + $type = "Group" + } else { + $type = "User" + } + $object = [PSCustomObject]@{ + Type = $type + Domain = $user.domain + SID = $entitlement + SamAccountName = $user.login_name + GivenName = $user.first_name + Surname = $user.last_name + Name = $user.name + UserPrincipalName = $user.user_principal_name + } + } + $result += $object + } + + return $result +} + +function Get-HorizonDesktopPool { + param ( + [string]$poolname = "", + [parameter(Mandatory = $true, ValueFromPipeline = $true)] $Horizon + ) + $token = $Horizon.access_token + $server = $Horizon.server_name + + $request_header = $global:request_header + $request_header += @{"authorization"="Bearer $token"} + + $pooluri = "https://$server/rest/inventory/v8/desktop-pools?sort_by=name&order_by=ASC" + + if ($global:kishorizondebug) { + Write-Host $MyInvocation.MyCommand.Name Starting + } + + if (Test-Server -server $server) { + try { + $pools = Invoke-RestMethod -Method Get -Uri $pooluri -Headers $request_header -SkipCertificateCheck + if ($poolname) { + $pools = $pools | Where-Object {$_.Name -eq $poolname} + } + } catch { + Write-Host "ERROR accessing $poolUri" + Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__ + Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription + return $null + } + + if ($pools) { + $pools | Add-Member -MemberType NoteProperty -Name machines -Value @() + $pools | Add-Member -MemberType NoteProperty -Name sessions -Value @() + $pools | Add-Member -MemberType NoteProperty -Name vCenter -Value "" + $pools | Add-Member -MemberType NoteProperty -Name cluster -Value "" + $pools | Add-Member -MemberType NoteProperty -Name GpuEnabled -Value 0 + $pools | Add-Member -MemberType NoteProperty -Name Entitlements -Value @() + $pools.provisioning_settings | Add-Member -MemberType NoteProperty -Name base_vm_name -Value "" + $pools.provisioning_settings | Add-Member -MemberType NoteProperty -Name snapshot_path -Value "" + $pools.provisioning_settings | Add-Member -MemberType NoteProperty -Name datacenter_name -Value "" + $pools.provisioning_settings | Add-Member -MemberType NoteProperty -Name resource_pool_name -Value "" + $pools.provisioning_settings | Add-Member -MemberType NoteProperty -Name base_folder_path -Value "" + $pools.storage_settings.datastores | Add-Member -MemberType NoteProperty -Name name -Value "" + + $machines = $Horizon | Get-HorizonMachine + $sessions = $Horizon | Get-HorizonSession + + $counter = 1 + foreach ($pool in $pools) { + Write-Progress -Activity "Processing Pools" -Status "Processing $counter of $($pools.length) - $($pool.name)" -PercentComplete (($counter / $pools.length) *100) -Id 1 + $mymachines = $machines | where {$_.desktop_pool_id -eq $pool.id} + $pool.machines += $mymachines + $mysessions = $sessions | where {$_.desktop_pool_id -eq $pool.id} + $pool.sessions += $mysessions + foreach($session in $pool.sessions) { + $machineDNSName = ($mymachines | where {$_.id -eq $session.machine_id}).dns_name + $machineName = ($mymachines | where {$_.id -eq $session.machine_id}).name + $session | Add-Member -MemberType NoteProperty -Name machine_name -Value $machineName + $session | Add-Member -MemberType NoteProperty -Name machine_dns_name -Value $machineDNSName + } + $pool.vCenter = ($hzconn | Get-HorizonVcenter | where-object {$_.id -eq $pool.vcenter_id}).server_name + if (-not ($global:DefaultVIServer.Name -contains $pool.vCenter)) { + throw "Not Connected to vCenter $($pool.vCenter)" + } + $pool.cluster = (Get-Cluster | where {$_.id -eq "ClusterComputeResource-$($pool.provisioning_settings.host_or_cluster_id)"}).name + $pool.provisioning_settings.base_vm_name = (Get-View -Id "VirtualMachine-$($pool.provisioning_settings.parent_vm_id)").Name + $snappath = "" + $goldVM = Get-VM $pool.provisioning_settings.base_vm_name + $thissnap = $goldVM | Get-Snapshot | Where-Object {$_.Id -eq "VirtualMachineSnapshot-$($pool.provisioning_settings.base_snapshot_id)"} + while ($thissnap.parentsnapshotid.length -gt 0) { + if ($snappath -eq "") { + $snappath = $thissnap.Name + } else { + $snappath = $thissnap.Name + "\" + $snappath + } + $thissnap = Get-VM $pool.provisioning_settings.base_vm_name | Get-Snapshot | Where-Object {$_.Id -eq $thissnap.ParentSnapshotId} + } + if ($snappath -eq "") { + $snappath = $thissnap.Name + } else { + $snappath = $thissnap.Name + "\" + $snappath + } + $pool.provisioning_settings.snapshot_path = $snappath + $pool.provisioning_settings.resource_pool_name = $goldVM.ResourcePool + $pool.provisioning_settings.datacenter_name = (Get-View "Datacenter-$($pool.provisioning_settings.datacenter_id)").Name + $basefolderpath = "" + $thisfolder = Get-View "Folder-$($pool.provisioning_settings.vm_folder_id)" + while ($thisfolder.parentsnapshotid.length -gt 0) { + if ($basefolderpath -eq "") { + $basefolderpath = $thisfolder.Name + } else { + $basefolderpath = $thisfolder.Name + "\" + $basefolderpath + } + $thisfolder = Get-View "Folder-$($thisfolder.Parent)" + } + if ($basefolderpath -eq "") { + $basefolderpath = $thisfolder.Name + } else { + $basefolderpath = $thisfolder.Name + "\" + $basefolderpath + } + $pool.provisioning_settings.base_folder_path = $basefolderpath + $datastores = @() + foreach ($datastore in $pool.storage_settings.datastores) { + $thisds = Get-View -Id "Datastore-$($datastore.datastore_id)" + $datastore.name = $thisds.Name + $datastores += $thisds.Name + } + + $pool.GpuEnabled = ($pool.display_protocol_settings.vgpu_grid_profile.Length -gt 0); + $entsids = ($Horizon | Get-HorizonEntitlements | Where-Object {$_.id -eq $pool.id}).ad_user_or_group_ids + if ($entsids.length -gt 0) { + $entitlements = $Horizon | Get-HorizonEntitlementDetail -entitlements $entsids + $pool.entitlements = $entitlements + } + ++$counter + } + Write-Progress -Activity "Processing Pools" -Completed -Id 1 + return $pools + } + } else { + return $false + } +} + +function Get-HorizonAdDomains { + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] $Horizon + ) + $token = $Horizon.access_token + $server = $Horizon.server_name + + $request_header = $global:request_header + $request_header += @{"authorization"="Bearer $token"} + + $domainURI = "https://$server/rest/external/v2/ad-domains" + + if ($global:kishorizondebug) { + Write-Host $MyInvocation.MyCommand.Name Starting + } + + try { + $domains = Invoke-RestMethod -Method Get -Uri $domainUri -Headers $request_header -SkipCertificateCheck + } catch { + throw "ERROR accessing $domainUri`nStatusCode: $($_.Exception.Response.StatusCode.value__)`nStatusDescription: $($_.Exception.Response.StatusDescription)" + } + + return $domains +} + +Function Get-HorizonADUser { + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] $Horizon, + [string]$Domain = "", + [string]$Id = "", + [string]$Name = "", + [int]$Page = 0, + [int]$PageSize = 0 + ) + $token = $Horizon.access_token + $server = $Horizon.server_name + + $request_header = $global:request_header + $request_header += @{"authorization"="Bearer $token"} + + $userlookupURI = "https://$server/rest/external/v1/ad-users-or-groups" + + $extension = ""; + + if ($domain -ne "") { + $extension = "?group_only=false&filter={`"type`":`"Equals`",`"name`":`"domain`",`"value`":`"$domain`"}" + if ($Page -ne 0) { + $extension += "&page=$Page" + if ($PageSize -ne 0) { + $extension += "&size=$PageSize" + } + } + } + + if ($Id -ne "") { + $extension = "/$($Id)?group_only=false" + } + + if ($Name -ne "") { + $extension = "?group_only=false&filter={`"type`":`"Equals`",`"name`":`"user_principal_name`",`"value`":`"$Name`"}" + } + + $userlookupURI += $extension + + if ($global:kishorizondebug) { + Write-Host $MyInvocation.MyCommand.Name Starting + } + + try { + $users = Invoke-RestMethod -Method Get -Uri $userlookupURI -Headers $request_header -SkipCertificateCheck + } catch { + throw "ERROR accessing $userlookupURI`nStatusCode: $($_.Exception.Response.StatusCode.value__)`nStatusDescription: $($_.Exception.Response.StatusDescription)" + } + + return $users +} + +<# Function Get-HorizonADGroup { + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] $Horizon, + [string]$Domain = "", + [string]$Id = "", + [string]$Name = "", + [int]$Page = 0, + [int]$PageSize = 0 + ) + $token = $Horizon.access_token + $server = $Horizon.server_name + + $request_header = $global:request_header + $request_header += @{"authorization"="Bearer $token"} + + $grouplookupURI = "https://$server/rest/external/v1/ad-users-or-groups" + + $extension = ""; + + if ($domain -ne "") { + $extension = "?group_only=true&filter={`"type`":`"Equals`",`"name`":`"domain`",`"value`":`"$domain`"}" + if ($Page -ne 0) { + $extension += "&page=$Page" + if ($PageSize -ne 0) { + $extension += "&size=$PageSize" + } + } + } + + if ($Id -ne "") { + $extension = "/$($Id)?group_only=true" + } + + if ($Name -ne "") { + $extension = "?group_only=true&filter={`"type`":`"Equals`",`"name`":`"name`",`"value`":`"$Name`"}" + } + + $grouplookupURI += $extension + + if ($global:kishorizondebug) { + Write-Host $MyInvocation.MyCommand.Name Starting + } + + try { + $users = Invoke-RestMethod -Method Get -Uri $grouplookupURI -Headers $request_header -SkipCertificateCheck + } catch { + throw "ERROR accessing $grouplookupURI`nStatusCode: $($_.Exception.Response.StatusCode.value__)`nStatusDescription: $($_.Exception.Response.StatusDescription)" + } + + return $users +} #> + +function Invoke-SSHCommand { + param ( + [Parameter(Mandatory)] + $ComputerName, + [Parameter(Mandatory)] + $Command, + [Parameter(Mandatory)] + $Credential + ) + + $username = $($Credential.UserName) + $password = $($Credential.GetNetworkCredential().password) + $done = $false + $count = 0; + while (!$done) { + $result = invoke-command {sshpass -p $password ssh $username@$ComputerName "$command"} + if ($result) { + $done = $true + } else { + ++$count + if ($count >=5) { $done = $true } + } + } + + return $result +} + +function Get-HorizonSessionLength { + param ( + [Parameter(Mandatory)] $session + ) + $now = [int](get-date -UFormat %s -Millisecond 0) + + $start = $session.start_time + + $retval = $now - ($start/ 1000) + + return $retval; +} + +function Get-HorizonSessionConnectedLength { + param ( + [Parameter(Mandatory)] $session + ) + $retval = ($session.session_state -eq "CONNECTED") ? [math]::round($session.last_session_duration_ms/1000) : 0 + + return $retval; +} + + +Export-ModuleMember -Function Connect-HorizonServer +Export-ModuleMember -Function Get-HorizonVcenter +Export-ModuleMember -Function Get-HorizonDesktopPool +Export-ModuleMember -Function Get-HorizonMachine +Export-ModuleMember -Function Remove-HorizonMachine +Export-ModuleMember -Function Get-HorizonSession +Export-ModuleMember -Function Get-HorizonAdDomains +Export-ModuleMember -Function Get-HorizonAdUser +Export-ModuleMember -Function Invoke-SSHCommand +Export-ModuleMember -Function Get-HorizonSessionLength +Export-ModuleMember -Function Get-HorizonKeyAgreement +Export-ModuleMember -Function Get-HorizonEntitlements +Export-ModuleMember -Function Get-HorizonEntitlementDetail + diff --git a/Horizon Tools/KISVDIDesktopResolver.ps1 b/Horizon Tools/KISVDIDesktopResolver.ps1 new file mode 100644 index 0000000..ce19901 --- /dev/null +++ b/Horizon Tools/KISVDIDesktopResolver.ps1 @@ -0,0 +1,210 @@ +param ( + [string]$vCenter = "", + [string]$Horizon = "", + [string]$Pool = "", + [switch]$Help = $false, + [switch]$SaveCred = $false +) + +# Variables +$fileVersion = "1.1.0" +$fileDate = "2026-06-15" +$scriptName = Split-Path -Path $PSCommandPath -Leaf +$mypath = split-path -parent $MyInvocation.MyCommand.Definition +$now = (Get-Date).ToString("yyyy-MM-dd_HH:mm") +$LogFile = "$mypath\KISVDIDesktopResolver.log" + +# Display Script Banner +Write-Host "--------------------------------------------------" -ForegroundColor White +Write-Host "Resolve Failed Horizon Desktops" -ForegroundColor Green +Write-Host "Version $($FileVersion) ($($FileDate))" -ForegroundColor Green +Write-Host "Written by Charlie Silverman" -ForegroundColor Green +Write-Host "--------------------------------------------------" -ForegroundColor White + +function CheckModuleInstalled($moduleName, $command) { + Write-Host "Checking that package $moduleName is installed..." -NoNewLine + if (-not(get-command $command -erroraction silentlycontinue)) { + Write-Host " Missing" -ForegroundColor Red + if (-not (Get-Module -ListAvailable -Name $moduleName)) { + $response = "" + while (($response -ne "yes") -and ($response -ne "no")) { + $response = (Read-Host "ImportExcel module not instaled. Install now? (yes/no)").ToLower() + } + if ($response -eq "no") { + Write-Host "Operation Cancelled." -ForegroundColor Yellow + exit + } + Write-Host "Installing PowerShell module $moduleName..." -NoNewLine + Install-Module -Name $moduleName -Scope CurrentUser -Force + Write-Host " Success" -ForegroundColor Green + } + Write-Host "Loading PowerShell module $moduleName..." -NoNewLine + Import-Module $moduleName + Write-Host " Success" -ForegroundColor Green + } else { + Write-Host " Found" -ForegroundColor Green + } +} + +# Check for ImportExcel and install if missing +CheckModuleInstalled -moduleName "VCF.PowerCLI" -command "connect-viserver" +$void = Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false +import-module $mypath/KISHorizon.psm1 -DisableNameChecking + +if ($Help) { + Write-Host + Write-Host "Purpose: Resolve Horizon VMs with errors." + Write-Host "Command Syntax:" + Write-Host " $scriptName (-vCenter [vcenter host name]) (-Horizon ""[cluster]"") (-Pool ""[filename]"")" -foregroundcolor green + Write-Host "Command Line Options:" + Write-Host + Write-Host -NoNewLine " -vCenter " -foregroundcolor Green + Write-Host "- The FQDN or IP address of a vCenter server." + Write-Host -NoNewLine " -Horizon " -foregroundcolor Green + Write-Host "- The FQDN or IP address of a Horizon connection broker." + Write-Host -NoNewLine " -Pool " -foregroundcolor Green + Write-Host "- A comma-delimited list of pools to check. If not specified, all pools will be checked." + Write-Host + Write-Host "If any of the information is not provided, you will be prompted with the exception of the pool list. If a credential has been" + Write-Host "previously stored, it will be used again unless the -savecred option is specified." + Write-Host + + exit +} + +if ($vCenter -eq "") { + $vCenter = Read-Host "Enter FQDN or IP address of vCenter Server" +} + +if ($SaveCred) { + $vccred = Get-Credential -Message "Enter credentials for vCenter server $vCenter" + $vccred| Export-Clixml -path .\kistools-$vcenter.cred + Write-Host "Saving credentials." +} + +if (Test-Path -path .\kistools-$vcenter.cred -PathType Leaf) { + $vccred = Import-Clixml -path .\kistools-$vcenter.cred + Write-Host "- Using stored vCenter credentials" +} else { + $vccred = Get-Credential -Message "Enter credentials for vCenter server $vCenter" + if (!$vccred) { + Write-Host "ERROR: No credentials were entered" -foregroundcolor yellow + exit + } +} + +#Establish connection to vCenter +try { + Write-Host -NoNewLine "Connecting to vCenter $vcenter... " + $vcconn = Connect-VIServer -server $vcenter -Credential $vccred -ErrorAction Stop + Write-Host "Success" -ForegroundColor green +} catch { + Write-Host "Failure" -ForegroundColor Yellow -BackgroundColor Red -NoNewLine + Write-Host + Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__ + Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription + exit +} + +if ($Horizon -eq "") { + $Horizon = Read-Host "Enter FQDN or IP address of Horizon Connection Broker" +} + +if ($SaveCred) { + $hzcred = Get-Credential -Message "Enter credentials for Horizon Broker $Horizon" + $hzcred | Export-Clixml -path .\kistools-$Horizon.cred + Write-Host "Saving credentials." +} + +if (Test-Path -path .\kistools-$Horizon.cred -PathType Leaf) { + $hzcred = Import-Clixml -path .\kistools-$Horizon.cred + Write-Host "- Using stored Horizon credentials" +} else { + $hzcred = Get-Credential -Message "Enter credentials for Horizon server $Horizon" + if (!$hzcreds) { + Write-Host "ERROR: No credentials were entered" -foregroundcolor yellow + exit + } +} + +#Establish connection to Horizon +try { + Write-Host -NoNewLine "Connecting to Horizon Connection Broker $Horizon... " + $hzconn = Connect-HorizonServer -server $Horizon -credential $hzcred + Write-Host "Success" -ForegroundColor green +} catch { + Write-Host "Failure" -ForegroundColor Yellow -BackgroundColor Red -NoNewLine + Write-Host + exit +} + +if ($Pool -ne "") { + $Pools = $Pool.split(",") + $poolsvalid = @() + foreach($thisPool in $Pools) { + Write-Host -NoNewLine "Verifying existence of pool $thisPool... " + $thispool = $hzconn | Get-HorizonDesktopPool -poolname $thisPool + if ($thispool) { + Write-Host "Success" -ForegroundColor green + $poolsvalid += $thisPool + } else { + Write-Host "Failure - Skipping" -ForegroundColor Yellow -BackgroundColor Red -NoNewLine + Write-Host + } + } +} else { + $poolsvalid = $hzconn | Get-HorizonDesktopPool +} + +$totalfixed = 0 +$totalerror = 0 +$poolreport = "" +foreach ($thisPool in $poolsvalid) { + $unreachable = $thispool.Machines | Where-Object {$_.state -eq "AGENT_UNREACHABLE"} + if ($unreachable.length -gt 0) { + $label = "desktops" + if ($unreachable.length -eq 1) { $label = "desktop" } + Write-Host "Pool $($thispool.Name) has $($unreachable.length) $label in an Unreachable state." + } + $poolfixed = 0 + foreach ($machine in $unreachable) { + $thisvm = Get-VM $machine.Name + Write-Host "- Resetting NIC for VM $($thisvm.Name) in pool $($thisPool.Name)... " -NoNewLine + $void = $thisvm | Get-NetworkAdapter | Set-NetworkAdapter -connected:$false -confirm:$false + Start-Sleep -Seconds 2 + $void = $thisvm | Get-NetworkAdapter | Set-NetworkAdapter -connected:$true -confirm:$false + Write-Host "Success" -ForegroundColor Green + $poolfixed += 1 + } + + if ($poolfixed -gt 0) { + $totalfixed += $poolfixed + $label = "VMs" + if ($poolfixed -eq 1) { $label = "VM" } + $poolreport += "`r`n - Pool $($thispool.Name): $poolfixed $label resolved" + } +} +try { + Write-Host -NoNewLine "Disconnecting from vCenter $vcenter... " + $void = Disconnect-VIServer -server $vcenter -confirm:$false + Write-Host "Success" -ForegroundColor green +} catch { + Write-Host "Failure" -ForegroundColor Yellow -BackgroundColor Red -NoNewLine + Write-Host + Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__ + Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription + exit +} + +Write-Host "`r`nTotal Desktops fixed in this run: $totalfixed" +Write-Host $poolreport + +$label = "desktops" +if ($totalfixed -eq 1) { $label = "desktop" } +Add-Content $LogFile "$Now - Total $label fixed in this run: $totalfixed" +$reportlines = $poolreport.split("`r`n") +foreach ($line in $reportlines) { + if ($line -ne "") { + Add-Content $LogFile "$Now - $line" + } +} \ No newline at end of file