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.
{
"access_group_id": "daef8c91-fa65-4a39-93aa-24d87d5aca94",
"allow_multiple_user_assignments": false,
"customization_settings": {
"ad_container_rdn": "OU=Pool01,OU=VDI,OU=Pod1,OU=VMware,OU=EUC",
"customization_type": "CLONE_PREP",
"do_not_power_on_vms_after_creation": false,
"instant_clone_domain_account_id": "9f83ed94-4d44-4c9a-abd3-ffc23e7389de",
"reuse_pre_existing_accounts": true
},
"description": "Desktop pool description",
"display_assigned_machine_name": false,
"display_machine_alias": false,
"display_name": "instantclonepool",
"display_protocol_settings": {
"allow_users_to_choose_protocol": true,
"default_display_protocol": "BLAST",
"grid_vgpus_enabled": false,
"renderer3d": "MANAGE_BY_VSPHERE_CLIENT",
"session_collaboration_enabled": true
},
"enable_client_restrictions": false,
"enable_provisioning": false,
"enabled": false,
"name": "demo_pool",
"naming_method": "PATTERN",
"pattern_naming_settings": {
"max_number_of_machines": 5,
"naming_pattern": "Pool-{n:fixed=2}",
"number_of_spare_machines": 1,
"provisioning_time": "UP_FRONT"
},
"provisioning_settings": {
"add_virtual_tpm": false,
"base_snapshot_id": "snapshot-1",
"datacenter_id": "datacenter-1",
"host_or_cluster_id": "domain-s425",
"parent_vm_id": "vm-2",
"resource_pool_id": "resgroup-1",
"vm_folder_id": "group-v1"
},
"session_settings": {
"allow_multiple_sessions_per_user": false,
"allow_users_to_reset_machines": true,
"delete_or_refresh_machine_after_logoff": "DELETE",
"disconnected_session_timeout_minutes": 5,
"disconnected_session_timeout_policy": "AFTER",
"logoff_after_timeout": false,
"power_policy": "ALWAYS_POWERED_ON",
},
"session_type": "DESKTOP",
"source": "INSTANT_CLONE",
"stop_provisioning_on_error": false,
"storage_settings": {
"datastores": [
{
"datastore_id": "datastore-1",
"sdrs_cluster": false
},
{
"datastore_id": "datastore-1",
"sdrs_cluster": false
}
],
"reclaim_vm_disk_space": false,
"use_separate_datastores_replica_and_os_disks": false,
"use_vsan": false
},
"transparent_page_sharing_scope": "VM",
"type": "AUTOMATED",
"user_assignment": "FLOATING",
"vcenter_id": "f148f3e8-db0e-4abb-9c33-7e5205ccd360"
}
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.
D:\GIT\Various_Scripts\Horizon_Rest_create_Desktop_Pool.ps1 -Credentials $creds -ConnectionServerURL https://pod1cbr1.loft.lab -jsonfile 'D:\homelab\new-pool-rest.json' -vCenterURL pod1vcr1.loft.lab -DataCenterName "Datacenter_Loft" -ClusterName "Dell 620" -BaseVMName "W21h1-2021-11-05-13-00" -BaseSnapShotName "Created by Packer" -DatastoreNames ("vdi-200","vdi-500") -VMFolderPath "/Datacenter_Loft/vm" -DesktopPoolName "Rest_Pool_demo" -DesktopPoolDisplayName "Rest Display name" -DesktopPoolDescription "rest description" -namingmethod "Restdemo{n:fixed=2}" -ADOUrdn "OU=Pool01,OU=VDI,OU=Pod1,OU=VMware,OU=EUC"
this gives this result:
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.
<#
.SYNOPSIS
Creates a new Golden Image to a Desktop Pool
.DESCRIPTION
This script uses the Horizon rest api's to create a new VMware Horizon Desktop Pool
.EXAMPLE
Horizon_Rest_create_Desktop_Pool.ps1 -Credentials $creds -ConnectionServerURL https://pod1cbr1.loft.lab -jsonfile 'D:\homelab\new-pool-rest.json' -vCenterURL pod1vcr1.loft.lab -DataCenterName "Datacenter_Loft" -ClusterName "Dell 620" -BaseVMName "W21h1-2021-11-05-13-00" -BaseSnapShotName "Created by Packer" -DatastoreNames ("vdi-200","vdi-500") -VMFolderPath "/Datacenter_Loft/vm" -DesktopPoolName "Rest_Pool_demo2" -DesktopPoolDisplayName "Rest DIsplay name" -DesktopPoolDescription "rest description" -namingmethod "Rest-{n:fixed=2}" -ADOUrdn "OU=Pool01,OU=VDI,OU=Pod1,OU=VMware,OU=EUC"
.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
Name of the datacenter
.PARAMETER BaseVMName
Mandatory: Yes
Name of the Golden Image VM
.PARAMETER BaseSnapShotName
Mandatory: Yes
Name of the Golden Image Snapshot
.PARAMETER DesktopPoolName
Mandatory: Yes
Name of the Desktop Pool to ctreate
.PARAMETER jsonfile
Mandatory: Yes
Full path to the JSON file to use as base
.PARAMETER ClusterName
Mandatory: Yes
Name of the vCenter Cluster to place the vm's in
.PARAMETER DatastoreNames
Mandatory: Yes
Array of names of the datastores to use
.PARAMETER VMFolderPath
Mandatory: Yes
Path to the folder where the folder with pool vm's will be placed including the datacenter with forward slashes so /datacenter/folder
.PARAMETER DesktopPoolDisplayName
Mandatory: Yes
Display name of the desktop pool
.PARAMETER DesktopPoolDescription
Mandatory: Yes
Description of the desktop pool
.PARAMETER NamingMethod
Mandatory: Yes
Naming method of the vm's
.PARAMETER ADOUrdn
Mandatory: Yes
Relative Distinguished Name for the OU where the vm's will be placed
.NOTES
Minimum required version: VMware Horizon 8 2111
Created by: Wouter Kursten
First version: 08-12-2021
.COMPONENT
Powershell Core
#>
<#
Copyright © 2021 Wouter Kursten
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#>
[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 = $true,
HelpMessage = "Display Name of the Desktop Pool.")]
[ValidateNotNullOrEmpty()]
[string]$DesktopPoolDisplayName,
[parameter(Mandatory = $true,
HelpMessage = "Description of the Desktop Pool.")]
[ValidateNotNullOrEmpty()]
[string]$DesktopPoolDescription,
[parameter(Mandatory = $true,
HelpMessage = "Name of the cluster where the Desktop Pool will be placed.")]
[ValidateNotNullOrEmpty()]
[string]$ClusterName,
[parameter(Mandatory = $true,
HelpMessage = "Array of names for the datastores where the Desktop will be placed.")]
[ValidateNotNullOrEmpty()]
[array]$DatastoreNames,
[parameter(Mandatory = $true,
HelpMessage = "Path to the folder where the folder for the Desktop Pool will be placed i.e. /Datacenter_Loft/vm")]
[ValidateNotNullOrEmpty()]
[string]$VMFolderPath,
[parameter(Mandatory = $true,
HelpMessage = "Relative Distinguished Name for the OU where the vm's will be placed i.e. OU=Pool01,OU=VDI,OU=Pod1,OU=VMware,OU=EUC")]
[ValidateNotNullOrEmpty()]
[string]$ADOUrdn,
[parameter(Mandatory = $true,
HelpMessage = "Naming method for the VDI machines.")]
[ValidateNotNullOrEmpty()]
[string]$NamingMethod,
[parameter(Mandatory = $true,
HelpMessage = "Full path to the Json with Desktop Pool details.")]
[ValidateNotNullOrEmpty()]
[string]$jsonfile
)
try{
test-path $jsonfile | out-null
}
catch{
throw "Json file not found"
}
try{
$sourcejson = get-content $jsonfile | ConvertFrom-Json
}
catch{
throw "Error importing json file"
}
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
$clusters = Invoke-RestMethod -Method Get -uri "$ConnectionServerURL/rest/external/v1/hosts-or-clusters?datacenter_id=$datacenterid&vcenter_id=$vcenterid" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)
$clusterid = ($clusters | where-object {$_.details.name -eq $ClusterName}).id
$datastores = Invoke-RestMethod -Method Get -uri "$ConnectionServerURL/rest/external/v1/datastores?host_or_cluster_id=$clusterid&vcenter_id=$vcenterid" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)
$datastoreobjects = @()
foreach ($datastoreName in $DatastoreNames){
$datastoreid = ($datastores | where-object {$_.name -eq $datastoreName}).id
[PSCustomObject]$dsobject=[ordered]@{
datastore_id = $datastoreid
}
$datastoreobjects+=$dsobject
}
$resourcepools = Invoke-RestMethod -Method Get -uri "$ConnectionServerURL/rest/external/v1/resource-pools?host_or_cluster_id=$clusterid&vcenter_id=$vcenterid" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)
$resourcepoolid = $resourcepools[0].id
$vmfolders = Invoke-RestMethod -Method Get -uri "$ConnectionServerURL/rest/external/v1/vm-folders?datacenter_id=$datacenterid&vcenter_id=$vcenterid" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)
$vmfolderid = ($vmfolders | where-object {$_.path -eq $VMFolderPath}).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
$sourcejson.provisioning_settings.base_snapshot_id = $basesnapshotid
$sourcejson.provisioning_settings.datacenter_id = $datacenterid
$sourcejson.provisioning_settings.host_or_cluster_id = $clusterid
$sourcejson.provisioning_settings.parent_vm_id = $basevmid
$sourcejson.provisioning_settings.vm_folder_id = $vmfolderid
$sourcejson.provisioning_settings.resource_pool_id = $resourcepoolid
$sourcejson.vcenter_id = $vcenterid
$sourcejson.storage_settings.datastores = $datastoreobjects
$sourcejson.name = $DesktopPoolName
$sourcejson.display_name = $DesktopPoolDisplayName
$sourcejson.description = $DesktopPoolDescription
$sourcejson.pattern_naming_settings.naming_pattern = $namingmethod
$json = $sourcejson | convertto-json -Depth 100
try{
Invoke-RestMethod -Method Post -uri "$ConnectionServerURL/rest/inventory/v1/desktop-pools" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken) -body $json
}
catch{
throw $_
}