Some of you might have seen previews on the socials but for the last few months I have been working hard on a GUI based tool to deploy golden images to VMware Horizon Instant Clone Desktop Pools and RDS Farms. This because who isn’t sick and tired of having to go into each and every Pod admin interface to o a million clicks to deploy a few new golden images?
The work started mid December and as usual the first 75% was done pretty quickly so mid January I had a first working version. While I expected a first version that would only support Desktop Pools to be available mid February (Vmug Virtual EUC day someone?) this build actually already had most of the parts in place to support both Desktop Pools and RDS Farms. After this first build my regular work for ControlUp started picking up again so I had less time to fix the numerous bugs that I encountered. Well bugs? Most where logic errors on my part but most of them have been ironed out by now. All in all the version that you can use today has cost me ~80hrs in building & testing and many many more hours thinking about solutions.
Is the tool perfect? Definitely not but it works and it’s more than what we’ve had before and I will be looking into impriving it even more.
The Tool
I guess you got bored with the talking and would like to see the tool now. The content of thos blog post might get it’s own page later for documentation purposes. The tool itself can be downloaded from this GitHub repository. Make sure to grab both the xaml and ps1 files, put them in a single folder and you should be good. The tool entirely runs on Powershell and is using the WPF Framework for the GUI.
Powershell 7.3 or later (for performance reasons I used some PS 7.3 options so the tool WILL break with an older version.)
Horizon 8 2206 or later (the reason for this is that otherwise the secondary images wouldn’t be available and that’s a feature I wanted in there for sure.)
Deplying a new golden Image
For normal usage you can just start the ps1 file.
This will bring you to this tab, before the first use though you need to go to the configuration page.
Fill in one of your cconnection servers, credentials and hit the test button. If you have a cloud pod setup the other pods will be automatically detected and make sure to check the Ignore Certificate Errors checkbox in case that’s needed!
If the test was successfull you are good to go to either the Desktop Pools or RDS Farms tabs. Both are almost completely the same so I will only show desktop pools
Hit the connect button and all Instant Clone Pools or Farms from all pods will be auto populated in the first pull down menu.
The second pulldown menu allows you to select a new source VM
And the third pull down the snapshot (I guess you guessed that already, I might need to add labels but they are so ugly in WPF)
To the right you have all kinds of options that you should recognize from the regular gui including add vTPM that was added in Horizon 2206. This checkbox isn’t in the RDS Farms as we currently simply don’t have the option to have Horizon add a vTPM there. If the options are valid ( like more cores that cores/socket and if those numbers will work (can’t do 3 cores and 2 cores per socket!) the Deploy Golden Image becomes available. Hit this to start the deployment, you can check the status by hitting the refresh button. (don’t tell anyone but the functions does exactly the same as a connect)
Handling Secondary Images
By default a secondary image will not be pushed to any machines. Just select the Push As Secondary Image checkbox and hit Deploy Golden Image button.
What you can also do is select one or more of the machines and deploy the Golden Images ot those machines
Once a Secondary Image has been deployed the three other buttons come available.
From top to bottom you can either cancel the secondary image completely, apply the golden image to more desktops (selecting ones that already has it won’t break anything and will just deploy it when possible) and promote the Secondary Image to the Primary golden image for the pool. The latter will cause a rebuild of ALL machines including the ones already running on the image. In the future I will also add a button to configure a machine to run the original image.
Settings and logs location
All settings are stored in an xml file in %appdata%\HGIDTool this includes the password configured in the Settings tab as a regular Encrypted PowerShell Credentials object. If you didn’t hit save on the settings tab this will be automatically done when closing the tool.
In case you want to borrow some of my code the log files contain every API call that I do to get date, push an image or handlke secondary images. This is the same output as you’d get when running in -verbose mode so that’s only needed when you’re troubleshooting the tool.
So besides the cpu/memory changes we obviously also have something called selective_push_image. This has to do with the added functionality of pushing a secondary image from Horizon 2111. While the example shows it as true the data model makes clear that it is not required and defaults to false. The array of machine_ids reflects the list of machines where the secondary image has to be applied.
compute_profile_num_cores_per_socket integer($int32)
example: 1
minimum: 1
exclusiveMinimum: false
exclusiveMaximum: false
Indicates the number of cores per socket for the CPU in the compute profile to be configured on clones.
If set, both compute_profile_num_cpus and compute_profile_ram_mb need to be set.
compute_profile_num_cpus integer($int32)
example: 4
minimum: 1
exclusiveMinimum: false
exclusiveMaximum: false
Indicates the number of CPUs in the compute profile to be configured on clones.
If set, this must be a multiple of compute_profile_num_cores_per_socket.
compute_profile_ram_mb integer($int32)
example: 4096
minimum: 1024
exclusiveMinimum: false
exclusiveMaximum: false
Indicates the RAM in MB in the compute profile to be configured on clones.
machine_ids [
example: List [ "816d44cb-b486-3c97-adcb-cf3806d53657", "414927f3-1a3b-3e4c-81b3-d39602f634dc" ]
Set of machines from the desktop pool on which the new image is to be applied. This can be set when selective_push_image is set to true.
selective_push_image boolean
example: true
Indicates whether selective push image is to be applied. If set to true, the new image will be applied to specified machine_ids in the desktop pool. The image published with this option will be held as a pending image, unless it is promoted or cancelled. The default value is false.
To be able to use this I have updated my previous image deployment script for Horizon 2206.
New arguments are:
AddVirtualTPM
Boolean to add a virtual TPM or not
SecondaryImage
Boolean to define the image as secondary (required if you also supply machine_ids)
Machine_Ids
Array with machine_ids to supply the secondary image to
CoresPerSocket
Int with # of Cores per Socket
CPUs
Int for total number of cpus
MemoryinMB
Memory in mb so 4096 or 6192 for example
<#
.SYNOPSIS
Pushes a new Golden Image to a Desktop Pool
.DESCRIPTION
This script uses the Horizon rest api's to push a new golden image to a VMware Horizon Desktop Pool
.EXAMPLE
.\Horizon_Rest_Push_Image.ps1 -ConnectionServerURL https://pod1cbr1.loft.lab -Credentials $creds -vCenterURL "https://pod1vcr1.loft.lab" -DataCenterName "Datacenter_Loft" -baseVMName "W21h1-2021-09-08-15-48" -BaseSnapShotName "Demo Snapshot" -DesktopPoolName "Pod01-Pool02"
.PARAMETER Credential
Mandatory: No
Type: PSCredential
Object with credentials for the connection server with domain\username and password. If not supplied the script will ask for user and password.
.PARAMETER ConnectionServerURL
Mandatory: Yes
Default: String
URL of the connection server to connect to
.PARAMETER vCenterURL
Mandatory: Yes
Username of the user to look for
.PARAMETER DataCenterName
Mandatory: Yes
Domain to look in
.PARAMETER BaseVMName
Mandatory: Yes
Domain to look in
.PARAMETER BaseSnapShotName
Mandatory: Yes
Domain to look in
.PARAMETER DesktopPoolName
Mandatory: Yes
Domain to look in
.PARAMETER StoponError
Mandatory: No
Boolean to stop on error or not
.PARAMETER logoff_policy
Mandatory: No
String FORCE_LOGOFF or WAIT_FOR_LOGOFF to set the logoff policy.
.PARAMETER Scheduledtime
Mandatory: No
Time to schedule the image push in [DateTime] format.
.PARAMETER AddVirtualTPM
Mandatory: No
Default: $False
Boolean FORCE_LOGOFF or WAIT_FOR_LOGOFF to set the logoff policy.
.PARAMETER SecondaryImage
Mandatory: No (Yes if machine_ids is supplied)
Default: $False
Boolean FORCE_LOGOFF or WAIT_FOR_LOGOFF to set the logoff policy.
.PARAMETER Machine_Ids
Mandatory: No
Array Array of Machine_ids to apply the secondary image to.
.PARAMETER CoresPerSocket
Mandatory: No (unless CPUs or MemoryinMB is supplies)
Int Amount of cores per socket.
.PARAMETER CPUs
Mandatory: No (unless MemoryinMB or CoresPerSocket is supplies)
Int Total number of cores.
.PARAMETER MemoryinMB
Mandatory: No (unless CPUs or CoresPerSocket is supplies)
Int New memory in MB
.NOTES
Minimum required version: VMware Horizon 8 2206
Created by: Wouter Kursten
First version: 03-11-2021
Changes: 05-09-2022 - Added resizing of cpu/memory
- Added secondary image functionality
- Added option to add Virtual TPM
.COMPONENT
Powershell Core
#>
[CmdletBinding(DefaultParameterSetName = 'Generic')]
param (
[Parameter(Mandatory=$false,
HelpMessage='Credential object as domain\username with password' )]
[PSCredential] $Credentials,
[Parameter(Mandatory=$true,
HelpMessage='FQDN of the connectionserver' )]
[ValidateNotNullOrEmpty()]
[string] $ConnectionServerURL,
[parameter(Mandatory = $true,
HelpMessage = "URL of the vCenter to look in i.e. https://vcenter.domain.lab")]
[ValidateNotNullOrEmpty()]
[string]$vCenterURL,
[parameter(Mandatory = $true,
HelpMessage = "Name of the Datacenter to look in.")]
[ValidateNotNullOrEmpty()]
[string]$DataCenterName,
[parameter(Mandatory = $true,
HelpMessage = "Name of the Golden Image VM.")]
[ValidateNotNullOrEmpty()]
[string]$BaseVMName,
[parameter(Mandatory = $true,
HelpMessage = "Name of the Snapshot to use for the Golden Image.")]
[ValidateNotNullOrEmpty()]
[string]$BaseSnapShotName,
[parameter(Mandatory = $true,
HelpMessage = "Name of the Desktop Pool.")]
[ValidateNotNullOrEmpty()]
[string]$DesktopPoolName,
[parameter(Mandatory = $false,
HelpMessage = "True or false for stop on error.")]
[ValidateNotNullOrEmpty()]
[bool]$StoponError = $true,
[parameter(Mandatory = $false,
HelpMessage = "Use WAIT_FOR_LOGOFF or FORCE_LOGOFF.")]
[ValidateSet('WAIT_FOR_LOGOFF','FORCE_LOGOFF', IgnoreCase = $false)]
[string]$logoff_policy = "WAIT_FOR_LOGOFF",
[parameter(Mandatory = $false,
HelpMessage = "DateTime object for the moment of scheduling the image push.Defaults to immediately")]
[datetime]$Scheduledtime,
[parameter(Mandatory = $false,
HelpMessage = "Bool for adding a Virtual TPM or not.")]
[ValidateNotNullOrEmpty()]
[bool]$AddVirtualTPM = $False,
[parameter(Mandatory = $false,
HelpMessage = "True or false to set this image as secondary image.")]
[ValidateNotNullOrEmpty()]
[bool]$SecondaryImage = $False,
[parameter(Mandatory = $false,
HelpMessage = "Array of machine_ids to apply the secondary image to.")]
[ValidateNotNullOrEmpty()]
[array]$Machine_Ids,
[parameter(Mandatory = $false,
HelpMessage = "New Number of cores per socket.")]
[ValidateNotNullOrEmpty()]
[int]$CoresPerSocket,
[parameter(Mandatory = $false,
HelpMessage = "New Number of CPU's.")]
[ValidateNotNullOrEmpty()]
[int]$CPUs,
[parameter(Mandatory = $false,
HelpMessage = "New amount of memory in MB.")]
[ValidateNotNullOrEmpty()]
[int]$MemoryinMB
)
if($Credentials){
$username=($credentials.username).split("\")[1]
$domain=($credentials.username).split("\")[0]
$password=$credentials.password
}
else{
$credentials = Get-Credential
$username=($credentials.username).split("\")[1]
$domain=($credentials.username).split("\")[0]
$password=$credentials.password
}
$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password)
$UnsecurePassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
function Get-HRHeader(){
param($accessToken)
return @{
'Authorization' = 'Bearer ' + $($accessToken.access_token)
'Content-Type' = "application/json"
}
}
function Open-HRConnection(){
param(
[string] $username,
[string] $password,
[string] $domain,
[string] $url
)
$Credentials = New-Object psobject -Property @{
username = $username
password = $password
domain = $domain
}
return invoke-restmethod -Method Post -uri "$ConnectionServerURL/rest/login" -ContentType "application/json" -Body ($Credentials | ConvertTo-Json)
}
function Close-HRConnection(){
param(
$accessToken,
$ConnectionServerURL
)
return Invoke-RestMethod -Method post -uri "$ConnectionServerURL/rest/logout" -ContentType "application/json" -Body ($accessToken | ConvertTo-Json)
}
if($CPUs -AND $CoresPerSocket -AND $MemoryinMB){
$resize = $true
}
elseif($CPUs -OR $CoresPerSocket -OR $MemoryinMB){
throw "If either CPUs, CoresPerSOcket or MemoryinGB is supplied, all must be supplied."
}
else{
$resize = $false
}
if($Machine_Ids -AND !($SecondaryImage)){
throw "If either Machine_Ids is supplied SecondaryImage also needs to be supplied."
}
try{
$accessToken = Open-HRConnection -username $username -password $UnsecurePassword -domain $Domain -url $ConnectionServerURL
}
catch{
throw "Error Connecting: $_"
}
$vCenters = Invoke-RestMethod -Method Get -uri "$ConnectionServerURL/rest/monitor/v2/virtual-centers" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)
$vcenterid = ($vCenters | where-object {$_.name -like "*$vCenterURL*"}).id
$datacenters = Invoke-RestMethod -Method Get -uri "$ConnectionServerURL/rest/external/v1/datacenters?vcenter_id=$vcenterid" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)
$datacenterid = ($datacenters | where-object {$_.name -eq $DataCenterName}).id
$basevms = Invoke-RestMethod -Method Get -uri "$ConnectionServerURL/rest/external/v1/base-vms?datacenter_id=$datacenterid&filter_incompatible_vms=false&vcenter_id=$vcenterid" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)
$basevmid = ($basevms | where-object {$_.name -eq $baseVMName}).id
$basesnapshots = Invoke-RestMethod -Method Get -uri "$ConnectionServerURL/rest/external/v1/base-snapshots?base_vm_id=$basevmid&vcenter_id=$vcenterid" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)
$basesnapshotid = ($basesnapshots | where-object {$_.name -eq $BaseSnapShotName}).id
$desktoppools = Invoke-RestMethod -Method Get -uri "$ConnectionServerURL/rest/inventory/v1/desktop-pools" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)
$desktoppoolid = ($desktoppools | where-object {$_.name -eq $DesktopPoolName}).id
$datahashtable = [ordered]@{}
$datahashtable.add('add_virtual_tpm',$AddVirtualTPM)
if($resize){
$datahashtable.add('compute_profile_num_cores_per_socket',$CoresPerSocket)
$datahashtable.add('compute_profile_num_cpus',$CPUs)
$datahashtable.add('compute_profile_ram_mb',$MemoryinMB)
}
$datahashtable.add('logoff_policy',$logoff_policy)
if($Machine_Ids){
$datahashtable.add('machine_ids',$Machine_Ids)
}
$datahashtable.add('parent_vm_id',$basevmid)
if($SecondaryImage){
$datahashtable.add('selective_push_image',$SecondaryImage)
}
$datahashtable.add('snapshot_id',$basesnapshotid)
if($Scheduledtime){
$starttime = get-date $Scheduledtime
$epoch = ([DateTimeOffset]$starttime).ToUnixTimeMilliseconds()
$datahashtable.add('start_time',$epoch)
}
$datahashtable.add('stop_on_first_error',$StoponError)
$json = $datahashtable | convertto-json
Invoke-RestMethod -Method Post -uri "$ConnectionServerURL/rest/inventory/v2/desktop-pools/$desktoppoolid/action/schedule-push-image" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken) -body $json
For those of you who haven’t seen my demo last week at the VMUG EUC day I have created a new script that will push a new Golden Image to a RDS farm. The same API call can also be used to schedule a recurring maintenance but that will have to wait for a future post.
What you need to run it are the following parameters:
-Credentials: credentials object from get-credential
-ConnectionServerURL: Full url to the connection server https://pod1cbr01.loft.lab for example
-vCenterURL : Full url to the vCenter Server: https://pod1vcr1.loft.lab
-DataCenterName : name of the Datacenter in vCenter
-BaseVMName : VM name of the new golden image
-BaseSnapShotName : name of the snapshot to be used
-FarmName : name of the RDS farm to push the image to
-StoponError : Boolean $True or $False to stop on error or not. Defaults to $true
-logoff_policy : String in capitals to wait for the users to logoff (WAIT_FOR_LOGOFF) or to forcefully log them off (FORCE_LOGOFF). Defaults to WAIT_FOR_LOGOFF
-Scheduledtime : datetime object for the time to schedule the image push. If not provided it will push the image immediately.
In my Lab I have this RDS Farm:
The last Image push actually failed
And It’s using this Golden Image with the Created by Packer Snapshot
With the following command I will push the new image:
Today I got the question from someone that they where trying to send messages to users but had issues with adding the session id’s to the json. I decided to make a quick and nice script that’s able to send a message to all sessions. This uses the standard functions that I always use, the standard Get-HorizonRestData function that I created in this blog post and two custom functions that I created for this script.
The first new function is get-horizonsessions this function gets all local sessions utilizing the get-horizonrestdata function. As you can see pretty basic nothing fancy about it.
function get-horizonsessions(){
[CmdletBinding(DefaultParametersetName='None')]
param(
[Parameter(Mandatory=$true,
HelpMessage='url to the server i.e. https://pod1cbr1.loft.lab' )]
[string] $ServerURL,
[Parameter(Mandatory=$true,
HelpMessage='Part after the url in the swagger UI i.e. /external/v1/ad-users-or-groups' )]
[PSCustomObject] $accessToken
)
try{
Get-HorizonRestData -ServerURL $url -RestMethod "/inventory/v1/sessions/" -accessToken $accessToken
}
catch{
throw $_
}
return $results
}
The second one isn’t that more advanced besides that it has a few more parameters including one called $session_ids that requires an array of the session id’s to where you want to send a message. It creates an ordered hashtable that stores the message, message_type and the array of id’s. This hashtable is than converted to a json file and used as the body for the rest call.
function send-horizonmessage(){
[CmdletBinding(DefaultParametersetName='None')]
param(
[Parameter(Mandatory=$true,
HelpMessage='url to the server i.e. https://pod1cbr1.loft.lab' )]
[string] $ServerURL,
[Parameter(Mandatory=$true,
HelpMessage='Part after the url in the swagger UI i.e. /external/v1/ad-users-or-groups' )]
[PSCustomObject] $accessToken,
[Parameter(Mandatory=$true,
HelpMessage='url to the server i.e. https://pod1cbr1.loft.lab' )]
[array] $Session_Ids,
[Parameter(Mandatory=$true,
HelpMessage='url to the server i.e. https://pod1cbr1.loft.lab' )]
[string] $Message,
[Parameter(Mandatory=$true,
HelpMessage='url to the server i.e. https://pod1cbr1.loft.lab' )]
[validateset("ERROR","WARNING","INFO", IgnoreCase = $false)]
[string] $Message_Type
)
$jsonhashtable = [ordered]@{}
$jsonhashtable.add('message',$message)
$jsonhashtable.add('message_type',$Message_Type)
$jsonhashtable.add('session_ids',$Session_Ids)
$json = $jsonhashtable | convertto-json
try{
$results = Invoke-RestMethod -Method Post -uri "$ServerURL/rest/inventory/v1/sessions/action/send-message" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken) -body $json
}
catch{
throw $_
}
return $results
}
Than I use these 2 lines to actually send the message (after authenticating first, duh). If you want to filter the sessions that can be added to the get-horizonsessions or manually do it on the $sessions array. Be aware that there are no names in this array so you need to gather any of the optional id’s first.
For years people have been asking, when can we start creating Horizon Desktop Pools using REST? The first years the answers was that there was no REST Api at all but since they added REST in 7.10 Horizon the answer was whenever VMware feels like adding this option. With the recent release of Horizon 8 2111 they have finally added the option to do this. I have created a script that uses a json file as base I grabbed this from the api explorer. But the sample below is already edited down so I can create a simple desktop pool. You can grab my version below or here on Github.
Essentially using only the json file could be enough to create a pool but I decided to create a powershell script that uses the json as a base and where I add functionality to make it more flexible. You cna supply names for things like datacenter, desktop pool name, display name and description & many others. If you want you can hardcode these names or even the id’s if you like but I have only done that in the json for the, access_group_id and the instant_clone_domain_account_id. You can go as crazy as you like and define each and every option as a parameter but I prefer a good base with the things that are changed the most often as parameters.
This is a sample of how I start the script, there is no feedback as the POST command doesn’t give any proper feedback if it ran well. I think the params are clear but you need to be careful to use a Relative Distinguished Name and not a normal Distinguished Name (the DC = parts are missing as you can see). For datastores you need to always need to use an array.
Now the script itself, I am still using the default rest functions and only a slight bit of error handling for checking if the json exists and importing it. For the rest it’s a list of api calls that you (partially) have seen before in other scripts. The datastoressobjects array is filled with separate objects per datastore. The json is converted to a regular array at the beginning so it’s very easy to replace all variables that need replacing. To convert the object to a usable json file you need to change the max depth as this is default only 2 an that’s not enough for this json file. I set it to 100 just to keep things easy. If you want to download the script I would recommend grabbing it from Github.
In a future blog post I will cover adding different VM Networks to the desktop pool. With this json file the default nic of the Golden Image is used.
I already blogged about pushing a new image using the Python module for Horizon but I decided it was time to also have a reusable script that is able to push a new image using powershell and the rest api for Horizon. The script that I created has 10(!) arguments of which 6 are required:
ConnectionServerURL: https://server.domain.dom
vCenterURL: https://vcenter.domain.dom
DataCenterName: Name of the datacenter the source VM resides in
BaseVMName : name of the source VM
BaseSnapShotName: name of the source Snapshot
DesktopPoolName: name of the source Desktop Pool to apply the snapshot to
The datacenter name is required as that’s an requirement to grab the Source VM details.
The optional arguments are:
Credentials: PSCRedential object (get-credential for example) when not provided it will ask for user and password. The user should also contain the domain i.e. domain\user
StoponError: $true or $false depending on if you want to stop on errors, defaults to $true if not provided
logoff_policy: Optional WAIT_FOR_LOGOFF or FORCE_LOGOFF depending on the logoff policy you want
Scheduledtime: [DateTime] object in case you want to push for the future
The script itself was fairly easy to create, from the api explorer it was easy what id’s I needed and it was a matter of working back so I had all of them. In the end the hardest part was getting the scheduling to work. I used the (standard?) 10 digit epoch that most people use but it turns out that VMware wanted to schedule it very exactly in milliseconds! For the rest I used the well known functions for authentication but not my generic function for filtering or pagination as almost none of the api calls I use in this script support that.
<#
.SYNOPSIS
Pushes a new Golden Image to a Desktop Pool
.DESCRIPTION
This script uses the Horizon rest api's to push a new golden image to a VMware Horizon Desktop Pool
.EXAMPLE
.\Horizon_Rest_Push_Image.ps1 -ConnectionServerURL https://pod1cbr1.loft.lab -Credentials $creds -vCenterURL "https://pod1vcr1.loft.lab" -DataCenterName "Datacenter_Loft" -baseVMName "W21h1-2021-09-08-15-48" -BaseSnapShotName "Demo Snapshot" -DesktopPoolName "Pod01-Pool02"
.PARAMETER Credential
Mandatory: No
Type: PSCredential
Object with credentials for the connection server with domain\username and password. If not supplied the script will ask for user and password.
.PARAMETER ConnectionServerURL
Mandatory: Yes
Default: String
URL of the connection server to connect to
.PARAMETER vCenterURL
Mandatory: Yes
Username of the user to look for
.PARAMETER DataCenterName
Mandatory: Yes
Domain to look in
.PARAMETER BaseVMName
Mandatory: Yes
Domain to look in
.PARAMETER BaseSnapShotName
Mandatory: Yes
Domain to look in
.PARAMETER DesktopPoolName
Mandatory: Yes
Domain to look in
.PARAMETER StoponError
Mandatory: No
Boolean to stop on error or not
.PARAMETER logoff_policy
Mandatory: No
String FORCE_LOGOFF or WAIT_FOR_LOGOFF to set the logoff policy.
.PARAMETER Scheduledtime
Mandatory: No
Time to schedule the image push in [DateTime] format.
.NOTES
Minimum required version: VMware Horizon 8 2012
Created by: Wouter Kursten
First version: 03-11-2021
.COMPONENT
Powershell Core
#>
[CmdletBinding()]
param (
[Parameter(Mandatory=$false,
HelpMessage='Credential object as domain\username with password' )]
[PSCredential] $Credentials,
[Parameter(Mandatory=$true, HelpMessage='FQDN of the connectionserver' )]
[ValidateNotNullOrEmpty()]
[string] $ConnectionServerURL,
[parameter(Mandatory = $true,
HelpMessage = "URL of the vCenter to look in i.e. https://vcenter.domain.lab")]
[ValidateNotNullOrEmpty()]
[string]$vCenterURL,
[parameter(Mandatory = $true,
HelpMessage = "Name of the Datacenter to look in.")]
[ValidateNotNullOrEmpty()]
[string]$DataCenterName,
[parameter(Mandatory = $true,
HelpMessage = "Name of the Golden Image VM.")]
[ValidateNotNullOrEmpty()]
[string]$BaseVMName,
[parameter(Mandatory = $true,
HelpMessage = "Name of the Snapshot to use for the Golden Image.")]
[ValidateNotNullOrEmpty()]
[string]$BaseSnapShotName,
[parameter(Mandatory = $true,
HelpMessage = "Name of the Desktop Pool.")]
[ValidateNotNullOrEmpty()]
[string]$DesktopPoolName,
[parameter(Mandatory = $false,
HelpMessage = "Name of the Desktop Pool.")]
[ValidateNotNullOrEmpty()]
[bool]$StoponError = $true,
[parameter(Mandatory = $false,
HelpMessage = "Name of the Desktop Pool.")]
[ValidateSet('WAIT_FOR_LOGOFF','FORCE_LOGOFF', IgnoreCase = $false)]
[string]$logoff_policy = "WAIT_FOR_LOGOFF",
[parameter(Mandatory = $false,
HelpMessage = "DateTime object for the moment of scheduling the image push.Defaults to immediately")]
[datetime]$Scheduledtime
)
if($Credentials){
$username=($credentials.username).split("\")[1]
$domain=($credentials.username).split("\")[0]
$password=$credentials.password
}
else{
$credentials = Get-Credential
$username=($credentials.username).split("\")[1]
$domain=($credentials.username).split("\")[0]
$password=$credentials.password
}
$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password)
$UnsecurePassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
function Get-HRHeader(){
param($accessToken)
return @{
'Authorization' = 'Bearer ' + $($accessToken.access_token)
'Content-Type' = "application/json"
}
}
function Open-HRConnection(){
param(
[string] $username,
[string] $password,
[string] $domain,
[string] $url
)
$Credentials = New-Object psobject -Property @{
username = $username
password = $password
domain = $domain
}
return invoke-restmethod -Method Post -uri "$ConnectionServerURL/rest/login" -ContentType "application/json" -Body ($Credentials | ConvertTo-Json)
}
function Close-HRConnection(){
param(
$accessToken,
$ConnectionServerURL
)
return Invoke-RestMethod -Method post -uri "$ConnectionServerURL/rest/logout" -ContentType "application/json" -Body ($accessToken | ConvertTo-Json)
}
try{
$accessToken = Open-HRConnection -username $username -password $UnsecurePassword -domain $Domain -url $ConnectionServerURL
}
catch{
throw "Error Connecting: $_"
}
$vCenters = Invoke-RestMethod -Method Get -uri "$ConnectionServerURL/rest/monitor/v2/virtual-centers" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)
$vcenterid = ($vCenters | where-object {$_.name -like "*$vCenterURL*"}).id
$datacenters = Invoke-RestMethod -Method Get -uri "$ConnectionServerURL/rest/external/v1/datacenters?vcenter_id=$vcenterid" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)
$datacenterid = ($datacenters | where-object {$_.name -eq $DataCenterName}).id
$basevms = Invoke-RestMethod -Method Get -uri "$ConnectionServerURL/rest/external/v1/base-vms?datacenter_id=$datacenterid&filter_incompatible_vms=false&vcenter_id=$vcenterid" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)
$basevmid = ($basevms | where-object {$_.name -eq $baseVMName}).id
$basesnapshots = Invoke-RestMethod -Method Get -uri "$ConnectionServerURL/rest/external/v1/base-snapshots?base_vm_id=$basevmid&vcenter_id=$vcenterid" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)
$basesnapshotid = ($basesnapshots | where-object {$_.name -eq $BaseSnapShotName}).id
$desktoppools = Invoke-RestMethod -Method Get -uri "$ConnectionServerURL/rest/inventory/v1/desktop-pools" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)
$desktoppoolid = ($desktoppools | where-object {$_.name -eq $DesktopPoolName}).id
$startdate = (get-date -UFormat %s)
$datahashtable = [ordered]@{}
$datahashtable.add('logoff_policy',$logoff_policy)
$datahashtable.add('parent_vm_id',$basevmid)
$datahashtable.add('snapshot_id',$basesnapshotid)
if($Scheduledtime){
$starttime = get-date $Scheduledtime
$epoch = ([DateTimeOffset]$starttime).ToUnixTimeMilliseconds()
$datahashtable.add('start_time',$epoch)
}
$datahashtable.add('stop_on_first_error',$StoponError)
$json = $datahashtable | convertto-json
Invoke-RestMethod -Method Post -uri "$ConnectionServerURL/rest/inventory/v1/desktop-pools/$desktoppoolid/action/schedule-push-image" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken) -body $json
A while ago Robin Stolpe (Twitter) asked me if it was possible to find what machines a user is assigned to in a Horizon environment. To answer this I first started messing with the soap api’s and had a really hard time to filter for the user id with the various machine related queries. When looking for the assignedUser property this was no problem but this has been deprecated and replaced by assignedUsers because of the added functionality for assigning multiple users to a machine. Instead of becoming too frustrated I decided to switch paths and user Powershell with the rest api’s.
TLDR: I have defined a broadly usable function for just about all Horizon REST GET api calls with or without filtering that also works for GET calls without any additions and for GET calls that require an Id for example. You can scroll down to the bottom to get that function and a script that uses it.
Warning: the sample code in the script & example function require PowerShell 7!
Filtering
One of the things I hadn’t done before with these was filtering and pagination in a more useful way than just writing the entire url out. VMware has a guide available for filtering that can be found here. This was a good way to get started but I found it easiest to skip the single searches entirely and always use the And or Or filtering types for chained filtering.
The method I am using to create the filter is to first define an ordered hashtable. Why ordered? The api calls require the Name/value pairs in a certain order and if you just add them to a regular hashtable this order will change.
$filterhashtable = [ordered]@{}
Next I add the first Name/value pair for the filtertype, this is either And or Or
$filterhashtable.add('type', 'And')
Next I add another pair with name filters and value an array. I could use .add again or just set the name like I do here:
$filterhashtable.filters = @()
The filters name array members again need to be ordered hashtable’s (as you can see I search for a user here)
For the pagination I needed the HAS_MORE_RECORDS property of the returned headers. If this is TRUE there are more records to be found, sadly this is not available in the classic invoke-restmethod from Powershell v5. With Powershell 7 you can add -ResponseHeadersVariable responseheader to store the headers in a variable called $responseheader. With this variable you can easily create a do while loop.
Please be advised that without some additional parameters this code isn’t usable yet, scroll down for something you can really use.
[sta_anchor id=”function” /]
The function
To combine the above 2 items I have created a function that can use all of the above but is also able to do regular get calls and get calls that require an id in the url.
function Get-HorizonRestData(){
[CmdletBinding(DefaultParametersetName='None')]
param(
[Parameter(Mandatory=$true,
HelpMessage='url to the server i.e. https://pod1cbr1.loft.lab' )]
[string] $ServerURL,
[Parameter(Mandatory=$true,
ParameterSetName="filteringandpagination",
HelpMessage='Array of ordered hashtables' )]
[array] $filters,
[Parameter(Mandatory=$true,
ParameterSetName="filteringandpagination",
HelpMessage='Type of filter Options: And, Or' )]
[ValidateSet('And','Or')]
[string] $Filtertype,
[Parameter(Mandatory=$false,
ParameterSetName="filteringandpagination",
HelpMessage='Page size, default = 500' )]
[int] $pagesize = 500,
[Parameter(Mandatory=$true,
HelpMessage='Part after the url in the swagger UI i.e. /rest/external/v1/ad-users-or-groups' )]
[string] $RestMethod,
[Parameter(Mandatory=$true,
HelpMessage='Part after the url in the swagger UI i.e. /rest/external/v1/ad-users-or-groups' )]
[PSCustomObject] $accessToken,
[Parameter(Mandatory=$false,
ParameterSetName="filteringandpagination",
HelpMessage='$True for rest methods that contain pagination and filtering, default = False' )]
[switch] $filteringandpagination,
[Parameter(Mandatory=$false,
ParameterSetName="id",
HelpMessage='To be used with single id based queries like /monitor/v1/connection-servers/{id}' )]
[string] $id
)
if($filteringandpagination){
if ($filters){
$filterhashtable = [ordered]@{}
$filterhashtable.add('type',$filtertype)
$filterhashtable.filters = @()
foreach($filter in $filters){
$filterhashtable.filters+=$filter
}
$filterflat=$filterhashtable | convertto-json -Compress
$urlstart= $ServerURL+"/rest/"+$RestMethod+"?filter="+$filterflat+"&page="
}
else{
$urlstart= $ServerURL+"/rest/"+$RestMethod+"?page="
}
$results = [System.Collections.ArrayList]@()
$page = 1
$uri = $urlstart+$page+"&size=$pagesize"
$response = Invoke-RestMethod $uri -Method 'GET' -Headers (Get-HRHeader -accessToken $accessToken) -ResponseHeadersVariable responseheader
$response.foreach({$results.add($_)}) | out-null
if ($responseheader.HAS_MORE_RECORDS -contains "TRUE") {
do {
$page++
$uri = $urlstart+$page+"&size=$pagesize"
$response = Invoke-RestMethod $uri -Method 'GET' -Headers (Get-HRHeader -accessToken $accessToken) -ResponseHeadersVariable responseheader
$response.foreach({$results.add($_)}) | out-null
} until ($responseheader.HAS_MORE_RECORDS -notcontains "TRUE")
}
}
elseif($id){
$uri= $ServerURL+"/rest/"+$RestMethod+"/"+$id
$results = Invoke-RestMethod $uri -Method 'GET' -Headers (Get-HRHeader -accessToken $accessToken) -ResponseHeadersVariable responseheader
}
else{
$uri= $ServerURL+"/rest/"+$RestMethod
$results = Invoke-RestMethod $uri -Method 'GET' -Headers (Get-HRHeader -accessToken $accessToken) -ResponseHeadersVariable responseheader
}
return $results
}
As you can see there are several arguments:
ServerURL
This is the url to the connection server i.e. https://server.domain
Filters
An Array of ordered hashtables as you can find in the filtering paragraph
filtertype
Sets the filter type, this needs to be And or Or
PageSize
This is optional if you want to change from the default 500 results that I have set
RestMethod
This is the RestMethod that you can copy from the Swagger URL or API Explorer.
AccessToken
This is the accesstoken you get as a result when using open-hrconnection from previous samples to authenticate (see the sample script below)
Filteringandpagination
Add this argument to use the filtering and/or pagination options
Id
Use this for REST API Get calls where an Id is required in the URI
The script below (and available on Github here) aks for credentials if you don’t supply the object, connectionserver FQDN (no url needed), user and domain to search for and returns an array of machines the user is assigned to. It uses the default functions Andrew Morgan created a long time ago and my function to use the get methods.
<#
.SYNOPSIS
Retreives all machines a user is assigned to
.DESCRIPTION
This script uses the Horizon rest api's to query the Horizon database for all machines a user is assigned to.
.EXAMPLE
.\find_user_assigned_desktops.ps1 -Credential $creds -ConnectionServerFQDN pod2cbr1.loft.lab -UserName "User2"
.PARAMETER Credential
Mandatory: No
Type: PSCredential
Object with credentials for the connection server with domain\username and password
.PARAMETER ConnectionServerFQDN
Mandatory: Yes
Default: String
FQDN of the connection server to connect to
.PARAMETER User
Mandatory: Yes
Username of the user to look for
.PARAMETER Domain
Mandatory: Yes
Domain to look in
.NOTES
Created by: Wouter Kursten
First version: 02-10-2021
.COMPONENT
Powershell Core
#>
[CmdletBinding()]
param (
[Parameter(Mandatory=$false,
HelpMessage='Credential object as domain\username with password' )]
[PSCredential] $Credential,
[Parameter(Mandatory=$true, HelpMessage='FQDN of the connectionserver' )]
[ValidateNotNullOrEmpty()]
[string] $ConnectionServerFQDN,
[parameter(Mandatory = $true,
HelpMessage = "Username of the user to look for.")]
[string]$User = $false,
[parameter(Mandatory = $true,
HelpMessage = "Domain where the user object exists.")]
[string]$Domain = $false
)
function Get-HRHeader(){
param($accessToken)
return @{
'Authorization' = 'Bearer ' + $($accessToken.access_token)
'Content-Type' = "application/json"
}
}
function Open-HRConnection(){
param(
[string] $username,
[string] $password,
[string] $domain,
[string] $url
)
$Credentials = New-Object psobject -Property @{
username = $username
password = $password
domain = $domain
}
return invoke-restmethod -Method Post -uri "$url/rest/login" -ContentType "application/json" -Body ($Credentials | ConvertTo-Json)
}
function Close-HRConnection(){
param(
$accessToken,
$url
)
return Invoke-RestMethod -Method post -uri "$url/rest/logout" -ContentType "application/json" -Body ($accessToken | ConvertTo-Json)
}
function Get-HorizonRestData(){
[CmdletBinding(DefaultParametersetName='None')]
param(
[Parameter(Mandatory=$true,
HelpMessage='url to the server i.e. https://pod1cbr1.loft.lab' )]
[string] $ServerURL,
[Parameter(Mandatory=$true,
ParameterSetName="filteringandpagination",
HelpMessage='Array of ordered hashtables' )]
[array] $filters,
[Parameter(Mandatory=$true,
ParameterSetName="filteringandpagination",
HelpMessage='Type of filter Options: And, Or' )]
[ValidateSet('And','Or')]
[string] $Filtertype,
[Parameter(Mandatory=$false,
ParameterSetName="filteringandpagination",
HelpMessage='Page size, default = 500' )]
[int] $pagesize = 500,
[Parameter(Mandatory=$true,
HelpMessage='Part after the url in the swagger UI i.e. /external/v1/ad-users-or-groups' )]
[string] $RestMethod,
[Parameter(Mandatory=$true,
HelpMessage='Part after the url in the swagger UI i.e. /external/v1/ad-users-or-groups' )]
[PSCustomObject] $accessToken,
[Parameter(Mandatory=$false,
ParameterSetName="filteringandpagination",
HelpMessage='$True for rest methods that contain pagination and filtering, default = False' )]
[switch] $filteringandpagination,
[Parameter(Mandatory=$false,
ParameterSetName="id",
HelpMessage='To be used with single id based queries like /monitor/v1/connection-servers/{id}' )]
[string] $id
)
if($filteringandpagination){
if ($filters){
$filterhashtable = [ordered]@{}
$filterhashtable.add('type',$filtertype)
$filterhashtable.filters = @()
foreach($filter in $filters){
$filterhashtable.filters+=$filter
}
$filterflat=$filterhashtable | convertto-json -Compress
$urlstart= $ServerURL+"/rest/"+$RestMethod+"?filter="+$filterflat+"&page="
}
else{
$urlstart= $ServerURL+"/rest/"+$RestMethod+"?page="
}
$results = [System.Collections.ArrayList]@()
$page = 1
$uri = $urlstart+$page+"&size=$pagesize"
$response = Invoke-RestMethod $uri -Method 'GET' -Headers (Get-HRHeader -accessToken $accessToken) -ResponseHeadersVariable responseheader
$response.foreach({$results.add($_)}) | out-null
if ($responseheader.HAS_MORE_RECORDS -contains "TRUE") {
do {
$page++
$uri = $urlstart+$page+"&size=$pagesize"
$response = Invoke-RestMethod $uri -Method 'GET' -Headers (Get-HRHeader -accessToken $accessToken) -ResponseHeadersVariable responseheader
$response.foreach({$results.add($_)}) | out-null
} until ($responseheader.HAS_MORE_RECORDS -notcontains "TRUE")
}
}
elseif($id){
$uri= $ServerURL+"/rest/"+$RestMethod+"/"+$id
$results = Invoke-RestMethod $uri -Method 'GET' -Headers (Get-HRHeader -accessToken $accessToken) -ResponseHeadersVariable responseheader
}
else{
$uri= $ServerURL+"/rest/"+$RestMethod
$results = Invoke-RestMethod $uri -Method 'GET' -Headers (Get-HRHeader -accessToken $accessToken) -ResponseHeadersVariable responseheader
}
return $results
}
if($Credential){
$creds = $credential
}
else{
$creds = get-credential
}
$ErrorActionPreference = 'Stop'
$username=($creds.username).split("\")[1]
$domain=($creds.username).split("\")[0]
$password=$creds.password
$url = "https://$ConnectionServerFQDN"
$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password)
$UnsecurePassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
$accessToken = Open-HRConnection -username $username -password $UnsecurePassword -domain $Domain -url $url
$userfilters = @()
$userfilter= [ordered]@{}
$userfilter.add('type','Equals')
$userfilter.add('name','name')
$userfilter.add('value',$User)
$userfilters+=$userfilter
$domainfilter= [ordered]@{}
$domainfilter.add('type','Equals')
$domainfilter.add('name','domain')
$domainfilter.add('value',$Domain)
$userfilters+=$domainfilter
$userobject = Get-HorizonRestData -ServerURL $url -filteringandpagination -Filtertype "And" -filters $userfilters -RestMethod "/external/v1/ad-users-or-groups" -accessToken $accessToken
$machinefilters = @()
$machinefilter= [ordered]@{}
$machinefilter.add('type','Contains')
$machinefilter.add('name','user_ids')
$machinefilter.add('value',($userobject).id)
$machinefilters+=$machinefilter
$machines = Get-HorizonRestData -ServerURL $url -filteringandpagination -Filtertype "And" -filters $machinefilters -RestMethod "/inventory/v1/machines" -accessToken $accessToken
return $machines
I use it like this to only display the machine names
One of the challenges with the Horizon REST API’s is that they are not feature complete yet and if you ain’t on the latest version you need to scroll trough the api explorer or Swagger UI to find if the URL you need is available. I have created a short script for both python and powershell that will show all the available urls.
If you’ve taken a good look at the Swagger page you’ll see there’s a link to the api docs almost at the top
If you open this you get something that looks like a json but it’s not readable (yet!)
and with another select -expandproperty you see all the details
$json.paths | select -expandproperty "/inventory/v1/rds-servers/{id}" | select -ExpandProperty get
With Python you can start with something similar
import json,requests,urllib
requests.packages.urllib3.disable_warnings()
response = requests.get("https://pod2cbr1.loft.lab/rest/v1/api-docs?group=Default" , verify=False)
data = response.json()
for i in data["paths"]:
print(i)
but this will just give the url’s
To be able to drill down I decided to bring the url, method and the description into a list and print that if needed. This example is just with the method and url but you can add the description as well. The list is to make it easier to filter on.
import json,requests,urllib
requests.packages.urllib3.disable_warnings()
response = requests.get("https://pod2cbr1.loft.lab/rest/v1/api-docs?group=Default" , verify=False)
data = response.json()
list=[]
paths=data["paths"]
for i in paths:
for method in paths[i]:
obj = {}
obj["method"] = method
obj["url"] = i
obj["description"] = paths[i][method]
list.append(obj)
for i in list:
print(i["method"], i["url"])
Ok maybe it’s more me than ControlUp but the usual audience on this blogs knows that I work for ControlUp and that I love the VMware flings. Recently we received the question from several South-America based customers if we where able to start the Horizon Session Recordings using a Script Based Action. Technically we would have been able to but with the current version we would have to add the user to an ad group that was configured for this and than would be at the mercy of AD replication for the recording to actually start. Que Trentent Tye who reached out to the one and only Andrew Morgan (imho king of the VMwareHorizon related flings) if it would be possible to add an API or PowerShell module to start the recordings. I guess Andrew’s reaction was that he saw the use case for this and within no-time he had a beta version ready for us that had a Powershell module. By the time you read this the new version has been added to the flings site that includes all of these goodies and you can find it here.
<I wanted to insert a picture of Andrew here wearing a crown but that was too much>
VMware Horizon Session Recording allows administrators of a VMware Horizon environment to record their users activity in their Blast Extreme virtual desktop and application sessions. Recordings are uploaded from the agent devices to the central web service, for central storage and ease of viewing.
The Session Recordings are stored as MP4 files for watching via the web console or downloading to play in a local player.
So we have an agent> server application where the videos are stored on the server. During the setup I found that this was very easy but configuring a proper SSL certificate makes things a lot easier as you otherwise need to supply the thumbprint of the self-signed certificate during installation of the agent or in the registry.
The server interface is easy enough with the standard dashboard showing recent recordings while on the Recordings tab you can search for for specific recordings.
The agent is a plain installer that only asks for the server location and the SSL certificate thumbprint. The last one is only needed when the certificate itself is not trusted, if you used a VA signed certificate for the server there’s no need to enter the thumbprint.
Starting a recording
As said in the previous versions of the recorder it was required to configure an ad group and add users to that group to record their sessions. With this version we get a powershell module installed with the agent. It gets even better because if you copy the entire /api folder from the agent installation folder to another system you’re perfectly able to start recordings from there.
Starting a recording using powershell is a matter of importing the proper dll
And now the user can start the recording themselves if they have too many rights by using Start-HSRSessionRecording -SessionID %sessionid%. The session ID can be found as a metric in ControlUp or in the task manager if you add the column under users.
Start-HSRSessionRecording -SessionID 1
My user clearly didn’t have enough rights on the system so I got an error, actually not a bad thing so users can’t easily overload the server this way.
Update: Andrew confirmed this is by design to stop users from recording each other.
Starting the recording remote is a matter of adding the computername to the command
To integrate the Horizon session recording with ControlUp I have created two basic Script Actions. both use three arguments, besides the required SessionId I add the username and machine name in the output so it’s obvious for whom the recording was started.
$sessionId=$args[0]
$username=$args[1]
$computer=$args[2]
try{
$InstallDir = Get-ItemPropertyValue -path "hklm:\SOFTWARE\VMware, Inc.\VMware Blast\SessionRecordingAgent" -Name installdir
}
catch{
write-host "Error determining the Horizon Session recording installation location. Please make sure the Horizon Sesison recording Agent is Installed."
}
try{
import-module "$($InstallDir)\api\horizon.sessionrecording.powershell.dll"
}
catch{
write-host "Error loading the Horizon Session Recording PowerShell Module. Make sure the latest vesrion of the Horizon Session Recording Agent is installed"
}
try{
Start-HSRSessionRecording -SessionID $sessionId
}
Catch{
write-host "Error starting the recording"
}
write-host "Started session recording for $username on $computer"
$sessionId=$args[0]
$username=$args[1]
$computer=$args[2]
try{
$InstallDir = Get-ItemPropertyValue -path "hklm:\SOFTWARE\VMware, Inc.\VMware Blast\SessionRecordingAgent" -Name installdir
}
catch{
write-host "Error determining the Horizon Session recording installation location. Please make sure the Horizon Sesison recording Agent is Installed."
}
try{
import-module "$($InstallDir)\api\horizon.sessionrecording.powershell.dll"
}
catch{
write-host "Error loading the Horizon Session Recording PowerShell Module. Make sure the latest vesrion of the Horizon Session Recording Agent is installed"
}
try{
stop-HSRSessionRecording -SessionID $sessionId
}
Catch{
write-host "Error stopping the recording"
}
write-host "Stopped session recording for $username on $computer"
Both these script will get better error handling and notifications in the future but you still see the old error in the background but with the notification that the recording was started.
And stopping the recording
And that shows how easy it is to control the Horizon Session Recording using a Script Based Action. If you combine this with a trigger, cpu usage for example you might be able to capture what is causing that spike, just remember to also create a trigger than stops it after the cpu goes down again.
Changelog
Version 2.2.0
Server Changes:
Added the ability to import server settings easily from another running server.
Added the ability to filter searches based on farm / pool.
Added the ability to lock and unlock sessions from the session view page.
Agent Changes:
1: Misc bugfixes.
2: PowerShell API to interact with the Recording agent, to perform such tasks as:
Last night Paul Grevink posted a nice post about the basic setup for Check MK and i am really looking forward to the rest of the series. At my current customer we are also using Check MK so i decided to use the script I made for the VMware Login monitor fling to give output usable for Check MK. At first I was messing with the plugin folder in the check mk folder on the windows server hosting the txt files but a colleague pointed me at the local folder. The big difference is that with the local folder Check MK directly uses the output and the plugin monitor it needs another python file on the check mk server to use the data.
The script:
# This script was created by Wouter Kursten
# contact: wouter.kursten@detron.nl or w.kursten@gmail.com or https://www.retouw.nl or @Magneet_NL on twitter
#
# Feel free to grab/copy/alter the script no need to mention me
# But if you create a better / more complete version please send me a mail so I can use that script also
#
# This script is meant to use with the VMware Logon Monitor FLing
# https://labs.vmware.com/flings/vmware-logon-monitor
# This awesome tools actually shows how long it takes to login to your systems
#
# And yes the info block is longer then the script itself
#
# There are only 5 variables you can set
#
# $filefolder for where the Logon Monitor Output files are stored
# $filefilter for when you want to filter what files are being read
# $fileage for how far back in time you want to go
# $warning for the warning value above wich Check MK will give a Warning.
# $critical Gues what, this is the value above wich Check MK will give a critical report.
#Region Variables
$filefolder= "d:\logonmonitor\"
$filefilter="*.txt"
$fileage="5"
$warning="20"
$critical="30"
#endregion
#region Run
$filelocation="$filefolder"+"$filefilter"
$filelist=get-childitem "$filelocation" | where-object {$_.LastWriteTime -gt (get-date).addminutes(-$fileage)}
$count=($filelist).count
$timing=@()
foreach ($file in $filelist)
{
$duration=(get-content $file | select-string -pattern "LogSummary] Logon Time:" | %{$_ -split " "})[6]
$timing += $duration
}
$avg= $timing | measure-object -average
$average = [System.Math]::Round($Avg.average,2)
if ($average -le "$warning")
{
write-output "0 VMware_Horizon_View_Logon_Time LogonTime=$average|Logons=$count Logon time : $average sec for $count logons in the last $fileage minutes."
}
elseif ($average -gt "$warning" -and $average -lt "$critical")
{
write-output "1 VMware_Horizon_View_Logon_Time LogonTime=$average|Logons=$count Logon time : $average sec for $count logons in the last $fileage minutes."
}
elseif ($average -ge "$critical")
{
write-output "2 VMware_Horizon_View_Logon_Time LogonTime=$average|Logons=$count Logon time : $average sec for $count logons in the last $fileage minutes."
}
#endregion
As you can see I am not only using the average logontime as before, I also count the amount of logons in the time where we measure this time. Offcourse you can create lots more data to use in Check MK this way
The output I create:
"2 VMware_Horizon_View_Logon_Time LogonTime=$average|Logons=$count Logon time : $average sec for $count logons in the last $fileage minutes."
The first digit is the status, 0 for ok 1 for warning and 2 for critical.
After that the service name that shows in Check MK
The come the 2 numbers we created with their own description separated by | This is used by Check MK to create a diagram
then separated by a space (and after this you can use spaces) the text that wil show in Check MK.
Privacy & Cookies: This site uses cookies. By continuing to use this website, you agree to their use.
To find out more, including how to control cookies, see here:
Cookie Policy