I have just added a page that lists the Horizon 8 2209 changes in the API’s. If you’re interested in the overall release notes, please see this link.
Some of the changes that stand out are brand new calls related to Radius & SAML settings but also options to bind to a domain, setting up unauthenticated users. licenses and pre-logon settings.
As always, a complete overview of all calls can be found in the API Explorer. But this is a list of the changes.
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
Since several version in each VMware Horizon release notes pages there has been this mention:
This links to this page: VMware Horizon REST APIs (84155) for a while this page was updated with the latest additions to the REST api’s but I guess people didn’t want to do that anymore so that page now simply links to the API Explorer:
Since I used this changelog to decide on what I wanted to blog about I thought it was time to create a changelog myself. What I did was download all the swagger specifications and compare them. For reasons I had to do this in excel but at least I now have a nice source for this. (ping me if you would like to have the excel file). This result in a new menu item at the top of my blog with the changelog for every Horizon 8 version. Yes I know the rest api’s have been available since 7.10 but I decided to start with 8.0. Please use the menu on top of the links below to go to the changelog for the version that you would like to see.
This ia a rather quick one as I already had some of the basics in my last script to send messages to global sessions. I have created a script that will gather all sessions in a Horizon environment. The global sessions api call has only been available since Horizon 2111 so when trying with an older version of Horizon it will return errors. Because some of the methods I use Powershell 7 is required.
There are 4 arguments:
Credential : Optional credentials object from get-credential (wil ask for credentials if not provided)
ConnectionServerFQDN : name says enough, fqdn for a connection server to connect to
global : switch to enable gathering of all Global sessions
pod_name: name of a single pod to collect all sessions from in case of a CPA
So last year I shared a script to send messages to local sessions in a pod regardless of Cloud Pod Architecture being used or not. With Horizon 8 2111 one of the new features is that you can also send messages to Global Sessions in other pods. I have created a script that messages to all those users. I used the same script as base but had to update Get-HorizonRestData to reflect the fact that sometimes some things need to be added to the url like ?pod_id=$podid in this case:
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,
[Parameter(Mandatory=$false,
HelpMessage='Extra additions to the query url that comes before the paging/filtering parts like brokering_pod_id=806ca in /rest/inventory/v1/global-sessions?brokering_pod_id=806ca&page=2&size=100' )]
[string] $urldetails
)
if($filteringandpagination){
if ($filters){
$filterhashtable = [ordered]@{}
$filterhashtable.add('type',$filtertype)
$filterhashtable.filters = @()
foreach($filter in $filters){
$filterhashtable.filters+=$filter
}
$filterflat=$filterhashtable | convertto-json -Compress
if($urldetails){
$urlstart= $ServerURL+"/rest/"+$RestMethod+"?"+$urldetails+"&filter="+$filterflat+"&page="
}
else{
$urlstart= $ServerURL+"/rest/"+$RestMethod+"?filter="+$filterflat+"&page="
}
}
else{
if($urldetails){
$urlstart= $ServerURL+"/rest/"+$RestMethod+"?"+$urldetails+"&page="
}
else{
$urlstart= $ServerURL+"/rest/"+$RestMethod+"?page="
}
}
$results = [System.Collections.ArrayList]@()
$page = 1
$uri = $urlstart+$page+"&size=$pagesize"
write-host $uri
$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{
if($urldetails){
$uri= $ServerURL+"/rest/"+$RestMethod+"?"+$urldetails
}
else{
$uri= $ServerURL+"/rest/"+$RestMethod
}
$results = Invoke-RestMethod $uri -Method 'GET' -Headers (Get-HRHeader -accessToken $accessToken) -ResponseHeadersVariable responseheader
}
return $results
}
I also had to change the get-horizonsessions to get-horizonglobalsessions
function get-horizonglobalsessions(){
[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='Id of the Local pod to query' )]
[string] $podid
)
try{
$results=Get-HorizonRestData -ServerURL $url -RestMethod "/inventory/v1/global-sessions" -accessToken $accessToken -urldetails "pod_id=$podid"
}
catch{
throw $_
}
return $results
}
And a new function get-pods
function Get-Pods(){
[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{
$results=Get-HorizonRestData -ServerURL $url -RestMethod "/federation/v1/pods" -accessToken $accessToken
}
catch{
throw $_
}
return $results
}
while function send-horizongmessage was upgraded to send-horizonglobalmessage
function send-horizonglobalmessage(){
[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,
[Parameter(Mandatory=$true,
HelpMessage='Id of the Local pod to query' )]
[string] $podid
)
$jsonhashtable = [ordered]@{}
$jsonhashtable.global_session_action_specs=@()
$sessiondetailshashtable = [ordered]@{}
$sessiondetailshashtable.ids=$Session_Ids
$sessiondetailshashtable.pod_id=$podid
$jsonhashtable.global_session_action_specs+=$sessiondetailshashtable
$jsonhashtable.message = $message
$jsonhashtable.message_type = $Message_Type
$json = $jsonhashtable | convertto-json -depth 100
try{
$results = Invoke-RestMethod -Method Post -uri "$ServerURL/rest/inventory/v1/global-sessions/action/send-message" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken) -body $json
}
catch{
throw $_
}
return $results
}
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.
So just found out that Horizon 8 2111 dropped today and there have been some welcome changes to the rest api’s. Luckily VMware does have these covered by now in THIS kb article.
In short these are the changes:
Inventory : Desktop Pools
Create desktop pool
Update desktop pool
Delete desktop pool
v5 version of List
v5 version of Get
Inventory : Desktop Actions
Validate Installed Applications
Validate VM Names Info
Resume Task on Desktop pool
Pause task on Desktop pool
Inventory : Farms
v3 version of List
v3 version of Get
v2 version of Create
v2 version of Update
Inventory : Farm Actions
Add RDS servers to farm
Remove RDS servers from farm
Schedule Maintenance (and image management schedule maintenance)
Cancel Schedule Maintenance
Validate Installed Applications
Inventory : Global Application Entitlements
v2 version of List
v2 version of Get
Create Global Application Entitlements
Update Global Application Entitlements
Delete Global Application Entitlements
List Compatible Backup Global Application Entitlements
Inventory : Global Desktop Entitlements
version of List
v2 version of Get
Create Global Desktop Entitlements
Update Global Desktop Entitlements
Delete Global Desktop Entitlements
List Compatible Backup Global Desktop Entitlements
Inventory : Global Sessions
List
Disconnect
Logoff
Reset
Restart
Send Message
so finally we’re able to create desktop pools using REST and to push a new image to rds farms. I will make sure to update the Python module ®Soon and create some blog posts on the new options.
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
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