My recipe for a successful vExpert application

This was also posted on the vExpert blog here.

One of the questions that the vExpert pro’s get is what people need to add to their vExpert application for it to be successful. While the things we do during the year are different for everyone, the first thing you need to do is to write everything down as extensively as possible. I will use my own current application as an example of how you can write everything down. There is no single truth to creating your application so you should see it as inspiration for creating your own.

One remark before you start editing or creating your application: make sure to save as often as possible. The vExpert portal needs to have a timeout and you wouldn’t be the first person where it times out and the progress hasn’t been saved and thus lost. I prefer to keep track of everything in a word document.

Preparation:

  • During the year keep track of your activities, this way you won’t forget anything.

(Required) Ingredients for this recipe:

  • Qualification path
  • Write down activities
  • Add url’s to activities if available
  • Be as extensive as possible
  • Work with a vExpert PRO

Step 1, Qualification path:

You need to select a qualification path. I could try as a partner as I work for ControlUp but I always use evangelist as all of my work in the community is done on my own credentials.VCDX are automatically vExperts but these are always checked against the VCDX directory so there’s no way to cheat there.

Step 2, Checkboxes:

If you created content make sure to select all the relevant checkboxes

Step 3 Other Media:

Give examples of the content you have created. There is no need to list every blog post but I always add links to the ones that I think are the most useful for the community. While I don’t consider views count relevant I do add a total amount of posts and views.

Step 3 Events and speaking:

Been an (online) event speaker or Podcast guest? You can list those here, if possible add a link for proof of these. For vmug events I know a lot of the old links give a 404 these days so make sure they work at the moment of submitting. If you can’t get a direct link to the event maybe you can find tweets about it that you were presenting so add those (I would advise to mention why you did that though). If you are an organizer of something like I do with the EMEA Breakfast events those can also be listed here. I won’t hold it against you when voting if you don’t have an attendance count but if you have it it’s even better.

<I would seriously consider hitting that save button here>

Step 4 Communities, Tools and Resources:

Tools & resources: Here is where you can link that nifty github repo where you share all your scripts. I for example list my Personal github profile but also the links to the vCheck for Horizon and the python module for Horizon as those are the main projects I worked on.

Communities: List every and any related community that you are active on, this includes Discord channels, Slack channels, forums etc even if they are not in English. Make sure to add a link to your profile or post history so the people who are voting can easily find your activity. If there’s no link available you can at least post your username.

Step 5, VMware Programs:

This is where you can list all your vExpert titles but also things like VMware Champions, VMware{Code} CodeCoach, beta programs, work as certification SME, Tanzu heroes, VMware Influencers and what not.

<Hit that save button again>

Step 6 Other Activities:

Here’s where you can list the things that aren’t publicly available so it can be very relevant for Customers & partners. I list the work that I internally do at ControlUp even if it is related to my work.

Step 7, References:

If you have a VMware employee that you worked a lot with and that can vouch for you make sure to check that box and list their email address. The same applies for the vExpert PRO that you worked with.

You don’t need to be a community crazy person like I am to become a vExpert but as said the most successful recipe for a successful application is to write everything down as extensively as possible. Do you have something that you don’t know if it helps? Just add it, it might be the thing that completes the picture for the PRO’s when we start voting.

If you want to reach out to a vExpert pro you can find the directory here and there’s also Facebook and Linkedin Groups.

Sending messages to Horizon Sessions using Powershell & REST api’s

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.

$sessions = get-horizonsessions -accessToken $accessToken -ServerURL $url

send-horizonmessage -accessToken $accessToken -ServerURL $url -Message_Type $Message_Type -message $message -Session_Ids ($sessions).id

And this is how you run the entire script.

.\Horizon_send_messages.ps1 -Credential $credential -ConnectionServerFQDN pod1cbr1.loft.lab -Message "retouw.nl test message" -Message_Type ERROR

And the entire script that’s also available at my Various_Scripts/Horizon_send_messages.ps1 at master · Magneet/Various_Scripts (github.com).

<#
    .SYNOPSIS
    Send a message to all user sessions

    .DESCRIPTION
    This script uses the Horizon rest api's to all sessions in a horizon pod

    .EXAMPLE
    .\find_user_assigned_desktops.ps1 -Credential $creds -ConnectionServerFQDN pod2cbr1.loft.lab -message "test message" -message_type "ERROR"

    .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 message
    Mandatory: Yes
    Message to send to the users

    .PARAMETER message_type
    Mandatory: Yes
    Message type: INFO, ERROR or WARNING

    .NOTES
    Created by: Wouter Kursten
    First version: 23-12-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='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
)

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
}

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
}

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
}

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

$sessions = get-horizonsessions -accessToken $accessToken -ServerURL $url

send-horizonmessage -accessToken $accessToken -ServerURL $url -Message_Type $Message_Type -message $message -Session_Ids ($sessions).id

 

 

 

Creating desktop pools using the Horizon REST Api with Powershell

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 $_
}

 

 

[HorizonAPI]Powershell Script to push a new Desktop image using the REST api.

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

I use it like this for example:

D:\GIT\Various_Scripts\Horizon_Rest_Push_Image.ps1 -ConnectionServerURL https://pod1cbr1.loft.lab -vCenterURL "https://pod1vcr1.loft.lab" -DataCenterName "Datacenter_Loft" -baseVMName "W21h1-2021-09-08-15-48" -BaseSnapShotName "Demo Snapshot" -DesktopPoolName "Pod01-Pool01" -logoff_policy WAIT_FOR_LOGOFF -StoponError $true -Scheduledtime ((get-date).AddMinutes(75))

Except for the question for username and password (in this case) there’s no response

and the push image is scheduled 75 minutes in the future

As always the script itself is also available at Github HERE.

Next time I’ll add more error handling and update the script to also push images to RDS farms 🙂

 

Horizon REST API + Powershell 7: pagination and filtering (with samples)

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)

$userfilter= [ordered]@{}
$userfilter.add('type','Equals')
$userfilter.add('name','name')
$userfilter.add('value',$User)

$domainfilter= [ordered]@{}
$domainfilter.add('type','Equals')
$domainfilter.add('name','domain')
$domainfilter.add('value',$Domain)

and I add both of them to the filters object

$filterhashtable.filters+=$userfilter
$filterhashtable.filters+=$domainfilter

and lets’s show what’s in the $filterhashtable

To be able to use this within the invoke-restmethod url I need to convert this to json and compress it to a single line

$filterflat = $filterhashtable | ConvertTo-Json -Compress
$filterflat

Pagination

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.

$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")
}
return $results

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

Examples

some usable examples would be:

Get-HorizonRestData -ServerURL $url -RestMethod "/monitor/connection-servers" -accessToken $accessToken 
Get-HorizonRestData -ServerURL $url -RestMethod "/monitor/connection-servers" -accessToken $accessToken -id $connectionserverid
Get-HorizonRestData -ServerURL $url -filteringandpagination -Filtertype "And" -filters $machinefilters -RestMethod "/inventory/v1/machines" -accessToken $accessToken

[sta_anchor id=”script” /]

Sample Script

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

(D:\GIT\Scripts\find_user_assigned_desktops.ps1 -Credential $creds -ConnectionServerFQDN "pod1cbr1.loft.lab" -User "user1" -Domain "loft.lab").name

You see some names in the 2*** range double but that is a Desktop Pool with Multiple Assignments

Getting the full machine objects is also possible

D:\GIT\Scripts\find_user_assigned_desktops.ps1 -Credential $creds -ConnectionServerFQDN "pod1cbr1.loft.lab" -User "m_wouter" -Domain "loft.lab"

[API]New way to gather Horizon Events

A good bunch of my audience has probably already noticed it but with Horizon 8 release 2106 VMware has added a new method to gather Horizon Events: the AuditEventSummaryView query. In this post I will describe how to consume this query using the soap API. I have been told by VMware specialists that this updated version of the eventsummaryview is actually safe to use and wont put a burden on the connection servers.

A quick small script to consume this query could look like this:

[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
)

if($Credential){
    $creds = $credential
}
else{
    $creds = get-credential
}

$ErrorActionPreference = 'Stop'

# Loading powercli modules
Import-Module VMware.VimAutomation.HorizonView
Import-Module VMware.VimAutomation.Core

$hvserver1=connect-hvserver $ConnectionServerFQDN -credential $creds
$Services1= $hvServer1.ExtensionData

$queryservice=new-object vmware.hv.queryserviceservice
$defn = New-Object VMware.Hv.QueryDefinition
$defn.queryentitytype='AuditEventSummaryView'


$eventlist = @()
$GetNext = $false
$queryResults = $queryservice.QueryService_Create($Services1, $defn)
do {
    if ($GetNext) {
        $queryResults = $queryservice.QueryService_GetNext($Services1, $queryResults.id) 
    }
    $eventlist += $queryResults.results
    $GetNext = $true
}
while ($queryResults.remainingCount -gt 0)
$queryservice.QueryService_Delete($Services1, $queryResults.id)
return $eventlist

I run it like this, show the event count and the last one

$creds = import-clixml d:\homelab\creds.xml
$events = D:\GIT\Scripts\get-horizon-audit-events.ps1 -ConnectionServerFQDN loftcbr01.loft.lab -Credential $creds
$events.count
$events | select-object -last 1

If you want to filter the data a bit more there are plenty of options for that:

I have added some filtering options to the above script, if you supply the filtertype argument the filterdata and filtervalue are mandatory. Filtertype for now can be either Equals or Contains, filterdata can be any of the data types from the image above and the value is the value you’re going to filter on. To be honest not all of the data types worked when I was creating this post but the message actually did.

[CmdletBinding(DefaultParameterSetName='noFilter')]
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(ParameterSetName='Filter',Mandatory=$true,HelpMessage = "Name of the data type to filter on.")]
    [Parameter(ParameterSetName='noFilter',Mandatory=$false,HelpMessage = "Name of the data type to filter on.")]
    [string]$filterdata,

    [Parameter(ParameterSetName='Filter',Mandatory=$true,HelpMessage = "Value to filter on.")]
    [Parameter(ParameterSetName='noFilter',Mandatory=$false,HelpMessage = "Value to filter on.")]
    [string]$filtervalue,

    [Parameter(ParameterSetName='Filter',HelpMessage = "FIltertype: Equals or Contains.")]
    [validateset("Equals","Contains")]
    [string]$filtertype

)

if($Credential){
    $creds = $credential
}
else{
    $creds = get-credential
}

$ErrorActionPreference = 'Stop'

# Loading powercli modules
Import-Module VMware.VimAutomation.HorizonView
Import-Module VMware.VimAutomation.Core

$hvserver1=connect-hvserver $ConnectionServerFQDN -credential $creds
$Services1= $hvServer1.ExtensionData

$queryservice=new-object vmware.hv.queryserviceservice
$defn = New-Object VMware.Hv.QueryDefinition
$defn.queryentitytype='AuditEventSummaryView'

if($filtertype){
    if($filtertype -eq "Contains"){
        $defn.Filter= New-Object VMware.Hv.QueryFilterContains -property @{'MemberName'=$filterdata; 'value'=$filtervalue}
    }
    else{
        $defn.Filter= New-Object VMware.Hv.QueryFilterEquals -property @{'MemberName'=$filterdata; 'value'=$filtervalue}
    }
}

$eventlist = @()
$GetNext = $false
$queryResults = $queryservice.QueryService_Create($Services1, $defn)
do {
    if ($GetNext) {
        $queryResults = $queryservice.QueryService_GetNext($Services1, $queryResults.id) 
    }
    $eventlist += $queryResults.results
    $GetNext = $true
}
while ($queryResults.remainingCount -gt 0)
$queryservice.QueryService_Delete($Services1, $queryResults.id)
return $eventlist

I run and check it like this:

$events = D:\GIT\Scripts\get-horizon-audit-events.ps1 -ConnectionServerFQDN loftcbr01.loft.lab -Credential $creds -filtertype Contains -filterdata message -filtervalue "has logged in"
$events | Select-Object message -last 10

The last version shown here can be downloaded from my github: Various_Scripts/get-horizon-audit-events.ps1 at master · Magneet/Various_Scripts (github.com)

 

Script to cleanup desktops running on old snapshot

So last year Guy Leech asked if if I had a script to identify machines running on an old snapshot. I Created a script for that here. This week Madan Kumar asked for a script that finds these same VDI desktops but that also cleans them out if needed. For this I have created the Horizon_cleanup_old_images.ps1 script (yes I suck at making up names).

If you run a get-help for the script you’ll see this:

By default the script only requires a Connectionserverfqdn and poolname as it works on a per pool level. It will try to give the users a gracefully logoff and has options to force the logoff ( in case their sessions is locked) or to delete the machine. And if you just want to have a preview there’s an option for that as well.

Optional arguments are:

-credential: this can be created with get-credential or can be retrieved from a stored credentials xml file, just make sure that it looks like domain\username and password

-deletedesktops: if used it will forcefully try to logoff the users but always deletes the desktop

-forcedlogoff: A normal logoff doesn’t work when the sessions is locked so you might need to force it

-preview: no actions are taken, just the information will be displayed to screen.

Let’s use the script

d:\git\scripts\Horizon_cleanup_old_image.ps1 -Credential $creds -ConnectionServerFQDN pod2cbr1.loft.lab -poolname "Pod02-Pool02" -preview

Yes I use write-host but it’s all 1 liners so shouldn’t be too slow and I like colors but as you see with the preview mode it shows what would happen. One of these sessions is locked so let’s see what happens when I log them off.

yes an error but I think it’s clear why, the graceful logoff worked for 2 users but not the third one, I will add the forced option now.

That looks good and when I look at the desktop pool everything is fine there as well.

And that’s being confirmed by the script

Now I will use the delete option for my other desktop pool.

First again with the preview option

and without

and seen from the Horizon Admin

As linked above the script can be found on github but also below this line.

<#
    .SYNOPSIS
    Cleans up desktops running on an image that's not the default for a desktop pool

    .DESCRIPTION
    This script uses the Horizon soap api's to pull data about machines inside a desktop pool that are running on a snapshot or base vm that's not currently configiured on the desktop pool. By default it logs off the users but there are options to forcefully logoff the user or delete the machines.

    .EXAMPLE
    .\Horizon_cleanup_old_image.ps1 -Credential $creds -ConnectionServerFQDN pod2cbr1.loft.lab -poolname "Pod02 Pool02" -delete -preview

    .PARAMETER Credential
    Mandatory: Yes
    Type: PSCredential
    Object with credentials for the connection server with domain\username and password

    .PARAMETER ConnectionServerFQDN
    Mandatory: No
    Default: String
    FQDN of the connection server to connect to

    .PARAMETER Poolname
    Mandatory: Yes
    Type: string
    Display name of the Desktop Pool to check

    .PARAMETER Deletedesktops
    Mandatory: No
    Enables the deleteion of the desktops, this includes an attempt to forcefully logoff the users.

    .PARAMETER Forcedlogoff
    Mandatory: No
    Enables the forcefully logging off of the users.

    .PARAMETER Preview
    Mandatory: No
    Makes the script run in preview mode and not undertake any actions.

    .NOTES
    Created by: Wouter Kursten
    First version: 27-06-2021

    .COMPONENT
    VMWare PowerCLI

#>

[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 = "Display Name of the desktop pool to logoff the users.")]
    [string]$poolname = $false,

    [Parameter(Mandatory=$false, 
    HelpMessage='Deletes the desktops instead of forcing the logoff' )]
    [switch] $deletedesktops,

    [Parameter(Mandatory=$false, 
    HelpMessage='Gives a preview only, no action will be undertaken.' )]
    [switch] $preview,

    [Parameter(Mandatory=$false, 
    HelpMessage='Forcefully logs off the users in case the desktop is locked or disconnected.' )]
    [switch] $forcedlogoff
)

if($Credential){
    $creds = $credential
}
else{
    $creds = get-credential
}

$ErrorActionPreference = 'Stop'

# Preview info
if($preview){
    write-host "Running in preview mode no actions will be taken" -foregroundcolor Magenta
 }

 # Loading powercli modules
 Import-Module VMware.VimAutomation.HorizonView
 Import-Module VMware.VimAutomation.Core

$hvserver1=connect-hvserver $ConnectionServerFQDN -credential $creds
$Services1= $hvServer1.ExtensionData

# --- Get Services for interacting with the Horizon API Service ---
$Services1= $hvServer1.ExtensionData

# --- Get Desktop pool
$poolqueryservice=new-object vmware.hv.queryserviceservice
$pooldefn = New-Object VMware.Hv.QueryDefinition
$pooldefn.queryentitytype='DesktopSummaryView'
$pooldefn.Filter= New-Object VMware.Hv.QueryFilterEquals -property @{'MemberName'='desktopSummaryData.displayName'; 'value'=$poolname}
try{
    $poolqueryResults = $poolqueryService.QueryService_Create($Services1, $pooldefn) 
    $poolqueryservice.QueryService_DeleteAll($services1)
    $results = $poolqueryResults.results
}
catch{
    write-error "There was an error retreiving details for $poolname"
}

# we need more details of the pool though and check if we even got one
if($results.count -eq 1){
    $pool = $Services1.Desktop.Desktop_Get($results.id)
}
else{
    write-host "No pool found with name $poolname" -foregroundcolor Red
    break
}

# Search for machine details
$queryservice=new-object vmware.hv.queryserviceservice
$defn = New-Object VMware.Hv.QueryDefinition
$defn.queryentitytype='MachineDetailsView'
$defn.filter = New-Object VMware.Hv.QueryFilterEquals -Property @{ 'memberName' = 'desktopData.id'; 'value' = $pool.id }
[array]$queryResults = $queryService.QueryService_Create($Services1, $defn)
$services1.QueryService.QueryService_DeleteAll()
# Process the results
if ($queryResults.results.count -ge 1){
    [array]$poolmachines=$queryResults.results
    [array]$wrongsnaps=$poolmachines | where-object {$_.managedmachinedetailsdata.baseimagesnapshotpath -notlike  $pool.automateddesktopdata.VirtualCenternamesdata.snapshotpath -OR $_.managedmachinedetailsdata.baseimagepath -notlike $pool.automateddesktopdata.VirtualCenternamesdata.parentvmpath}
    # If there are desktops on a wrong snapsot we need to do something with that info
    if($wrongsnaps.count -ge 1){
        if($deletedesktops){
            write-host "Removing:" $wrongsnaps.data.name -foregroundcolor yellow
            $deletespec = new-object vmware.hv.machinedeletespec
            $deletespec.DeleteFromDisk = $true
            $deletespec.ForceLogoffSession = $true
            if(!$preview){
                $Services1.Machine.Machine_DeleteMachines($wrongsnaps.id, $deletespec)
            }
        }
        else{
            write-host "Logging users off from:" $wrongsnaps.data.name -foregroundcolor yellow
            [array]$sessiondata = $wrongsnaps.sessiondata
            write-host "Users being logged off are:" $sessiondata.username -foregroundcolor yellow
            if(!$preview){
                if($forcedlogoff){
                    write-host "Forcefully logging off users" -foregroundcolor yellow
                    $services1.session.Session_LogoffSessionsForced($sessiondata.id)
                }
                else{
                    write-host "Gracefully logging off users" -foregroundcolor yellow
                    $services1.session.Session_LogoffSessions($sessiondata.id)
                }
            }
        }
    }
    else{
        write-host "No machines found on a wrong snapshot" -foregroundcolor Green
    }
}
else{
    write-host "No machines found in $poolname" -foregroundcolor red
}

 

Getting Session information using the Python Module for VMware Horizon with filtering

Since Horizon 8 it is possible to retrieve session information using the REST api’s. In the python module for Horizon I have translated that functionality to the get_sessions function.

import requests, getpass, urllib, json

import vmware_horizon

requests.packages.urllib3.disable_warnings()
url = input("URL\n")

username = input("Username\n")

domain = input("Domain\n")

pw = getpass.getpass()

hvconnectionobj = vmware_horizon.Connection(username = username,domain = domain,password = pw,url = url)
hvconnectionobj.hv_connect()
print("connected")

inventory=vmware_horizon.Inventory(url=hvconnectionobj.url, access_token=hvconnectionobj.access_token)
sessions = inventory.get_sessions()
for i in sessions:
    for ii in i:
        print(ii, '=', i[ii] )


end=hvconnectionobj.hv_disconnect()
print(end)

You see a lot of the information is returned using an id but there are plenty of functions that you can use to get the readable information for those.

Since Horizon 2103 VMware has added an option use filtering when getting session information. In the example above you can see that I have a connected and a disconnected session, let’s get the connected one. First I create the filter.

filter = {}
filter["type"] = "And"
filter["filters"] = []
filter1={}

filter1["type"] = "Equals"
filter1["name"] = "session_state"
filter1["value"] = "CONNECTED"

filter["filters"].append(filter1)

Next I run the action with the filter argument.

sessions = inventory.get_sessions(filter=filter)

And now you see only the connected one is returned

And when I switch to disconnected

The new python module has been pushed to github together with example scripts for retrieving all sessions and with filtering.

vCheck for Horizon : small updates & running it (automatically) from ControlUp

Remark : as of the time I was writing this article the vCheck for Horizon  sba hasn’t been published yet. Contact me if you want me to send you the xml file.

Last week I have been doing a few small updates to the vCheck for Horizon. The file with the encrypted password is now replaced by an xml file that holds all of the credentials and is usable directly for Horizon connections. I have been using this for my blog posts and demo’s for a while already. Also I have renamed the security & composer plugins by default to old as those have been deprecated and I don’t want to have them active by default anymore. Further I have done some small changes to other plugins.

Creating the xml file

Running the vCheck from horizon

But but wasn’t the vCheck all about running it automagically?

Creating the xml file

[sta_anchor id=”xml” /]

this can be easily done like this:

$creds = get-credential
$creds | export-clixml filename.xml

as you might have seen during my demo’s you can use this $creds object to make a connection.

$creds = import-clixml filename.xml
$server = connect-hvserver connectionserver.fq.dn -credential $creds

Make sure to save this file to a good location and edit the “00 Connection Plugin for View.ps1” file with the proper location. I haven’t completely removed the old ways yet, they’re still partially there.

Running the vCheck script from ControlUp

[sta_anchor id=”controlup” /]

Before everything else: make sure the machine that will run the vCheck has the latest version of PowerCLI installed. Download it here, and unpack the modules to “C:\Program Files\WindowsPowerShell\Modules” like this:

There are several things that I needed to do before I was able to run the vCheck from the ControlUp console or as an Automated Action (more on this later!). First I needed a way of providing credentials to the script. This is something I do using the Create Credentials for Horizon Scripts script action. If you already use some of the other Horizon related sba’s or the Horizon Sync script you might already have this configured. What it essentially does is creating an xml file as described above in a folder dedicated for ControlUp and usable by the service account that ran the sba. More on this can be found here.

Next was a way to get the vCheck itself. I do this by downloading it from github and unpack the zip file to C:\ProgramData\ControlUp\ScriptSupport\vCheck-HorizonView-master if downloads from the interwebz aren’t available you can download and unzip it yourself, just make sure it looks like this:

Next would be all the settings for email, connection server etcetera. This is all done trough the Script Action Arguments. WHat technically happens is that the Script Action removes both the globalvariables.ps1 and the “00 Connection Plugin for View.ps1” files and recreates them with those settings.

Once you have downloaded the vCheck you can run it manually so right click any random machine and select the vCheck for Horizon.

and fill in all the details you need. If you set the Send Email to false there is no need for the rest of the info.

Once you see this screen the script has completed, make sure to read the last line for the correct status and where you saved to file to.

And on my d:\ I see the file (and some others I used for testing)

And even better I received it in my mail as well

But but wasn’t the vCheck all about running it automagically?

[sta_anchor id=”automated” /]

Well yes it is and you can do that starting with version 8.5 that we have announced today! For now it will need to run hourly but in future releases we can also do this once a day. What you need to do for this is first change the default for the script action. Select that sba and hot the modify button.

Under settings you can keep the execution context as console (this will use the monitor when you run it as an automated action) or select other machine to pick a scripting server. Also make sure to select a shared credential that has a Horizon credentials file created on this machine.

Now go to the arguments tab and edit the default settings to set them to what you need.

Before:

Editing the first one

and done, press ok after this.

Now click finalize so the monitors can also use them and go trough all the steps ( no need to share with the community) an dmake sure the ControlUp Monitors have the permissions to run the sba.

Next we go to triggers > add trigger and select the new type: scheduled and click next

select the hourly schedule and a proper start and end time and click next

On the filter criteria make sure that you set the name to a single machine, otherwise the script will run for all machines in your environment. I recommend the same machine you used for the execution context. Keep in mind that the schedule functions just like any other trigger so you need to filter properly to what machine it applies and if you don’t it will apply to all of them.

You can select the folder this machine resides in and/or select a schedule to run the script in.

At the follow-up actions click add and select run an action from the pulldown menu. Under script name select the vCheck for Horizon, click ok and next

Make sure to create a clear name and click finish

The html should automatically end up in the export location and in your email.

 

How to add Rocky Linux (& Other not supported Linux Flavors) to ControlUp

Disclaimer: I am a ControlUp employee but am posting this on my own. We officially only support the Linux flavors here: https://support.controlup.com/hc/en-us/articles/360001497917-Linux-Integration-with-ControlUp

Disclaimer 2: I am NOT a Linux expert, I consider myself a basic user for most Linux things.

With the recent rise of the truly community driven (beta) releases of Rocky Linux I thought it was time to see what would be needed to add Rocky Linux to the ControlUp console. If you want to know more about how and what with Rocky Linux I recommend reading my good friend Angelo Luciani’s blog post here. TLDR: Rocky Linux is a project from one of the CentOS co-founders and is supposed to be enterprise grade once GA is reached.

Adding the linux machines to ControlUp

Fixing Linux to work with ControlUp

Adding linux machines to ControlUp

[sta_anchor id=”adding” /]

To compare the process with CentOS 7, 8 and Ubuntu 2004 LTS I have deployed fresh vm’s for these distributions. For Rocky and the CentOS vm’s I selected Server with a GUI, for Ubuntu I picked the default installation. All of them got 1 cpu, 2GB ram and 64GB disk space (overkill, yes I know). I also added the same user to all of them during the deployment. Network wise I just gave them a proper host name and selected dhcp as that was enough for this test.

The setup screen for Rocky Linux

To be able to add unsupported Linux Flavors you need to enable this in Settings > Agents

After deploying all 4 of the vm’s I added them to the ControlUp console with the same Linux Data collector.

I could use another credential that I setup in the past for my domoticz & Unify linux machines, after this click add to add the machines.

I entered the ip range to scan, hit scan, selected the 4 linux machines (note: CentOS 8 is not being reported as a not supported flavor, I have reported a bug for this), click add and hit ok twice.

By default the CentOS 7 machine is the only one working correctly.

if you see some weird negative number for CPU make sure to check your locale settings that it is the same as the regional settings on your Console/Monitor.

Now let’s make sure we get proper metrics in the console 🙂

Fixing Linux machines to work with ControlUp

[sta_anchor id=”fixing” /]

Rocky Linux needs the same fixes as CentOS 8, Ubuntu needs another step.

  1. Install gcc : for CentOS / Rock
    sudo yum install gcc

    or for Ubuntu

    sudo apt-get install gcc
  2. run
    ps -V
    1. if the output has ps from procps you need to remember the number 6
    2. If the output has ps from procps-ng you need to remember the number 7
  3. Create a file called a.c
    1. run:
      nano a.c

      (or vi, whatever you like)

    2. Add:
      1. #include <stdio.h> 
        int main() 
        {
          printf("6\n");
          return 0;
        }

        replace the 6 with 7 if you had procps-ng in the previous step

    3. save this file
  4. run:
    gcc a.c
  5. run:
    sudo cp a.out /bin/rpm
  6. Install sysstat: For Rocky Linux / CentOS
    sudo yum install sysstat

    or for Ubuntu

    sudo apt-get install sysstat
  7. Reboot ( yes I needed this)

For Rocky Linux and CentOS you’re done

For Ubuntu you will also need to install ifconfig from net-tools before the reboot

  1. run:
    sudo apt-get install net-tools

For me this was enough to have all four of these machines look properly in the ControlUp Console. Remember we might not officially support this way of working but it works good