My portable Lab

For a long time I have been wanting to have some kind of portable lab but I had a few requirements that where limiting me:

  • has kvm (for troubleshooting as things always break for me)
  • low power consumption
  • Does’t require a dedicated suitcase for transportation

Not so long ago I got a newer laptop from ControlUp to replace my (not that old) annoying heat and noise generating Dell Lattitude 5401. I never could get this thing even a bit quiet without hurting performance. While I found it annoying to work daily on I thought it might be a good match to turn this laptop in a portable lab.

The Host

One of the advantages was that the laptop did have an intel nic built in so I decided to see if I could get ESXi installed on it. After some tinkering I found out that the only thing required for this was to change the disk mode in the bios from raid to ahci. (Why dell? why? the thing has only one nvme…) and after that change ESXi installed without issues.

Now with the 16GB that my machine had I could run ESXi but it was far from enough for what I was going to need and neither was the storage at 512GB. I did have an intel nvme 1TB 660P nvme in one of my servers that I honestly wasn’t even using so that was swapped in quickly. I also had a Gigabite Brix box with 64GB that I had planned to run 24/7 but with the rising energy costs I never did that and it was mostly gathering dust as my regular homelab has more than enough cpu/memory. These where 2 Lexar Modules and after another quick swap you might have seen this picture on twitter.

Specs

Connectivity

Routing

So I had my host with build-in KVM & UPS, now I also needed connectivity. When using it at home it has it’s own dedicated vlan but on the go I needed some kind of router that can use the same vlan. I did not wat to go for a virtual router as I do not want to potentially connect an ESXi host to a for me unknown network. Again I had a few requirements:

  • USB powered (Requiring a single socket is already enough)
  • Small
  • 2 ports (wan and lan side)
  • gbit
  • wifi (so my regular laptop can connect)
  • Bonus : usb tethering for my phone in case all other connection options fail

In the end I decided to order the GL.iNet GL-AR300M16. While there might have been cheaper options there aren’t that many that have multi rj45 ports and also have openwrt as that is a very flexible OS for routers like these. The only thing I have had to change oimn the router was the ip addresses that it uses + I had to disablke dhcp as I wanted the domain controller to do that. It switches very easily between wired and tethering so it’s checkeng all the boxes that I needed.

Storage

Yes I know I said this machine is a mobile lab but I did want to have an option to connect shared storage in case I want to do maintenance etc. I did not want to use the built-in nic for this so I went looking for a usb to rj45 adapter that supports 2,5gbps as I have a Qnap TS-464 with 2,5gbps connectivity. The first step was to check the usb nic fling for what adapters will work. Now this list doesn’t include the speed but a quick look at amazon showed me that the RealTek 8156 will work and the images (not the text!) in this amazon CY USB-C to 2,5GBPS adapter showed that it should work. So another quick amazon order later I was able to prove it worked and I had connectivity to my NAS.

The Setup

The host

On Host level I didn’t need to make too much changes besides network configuration. The only things done so far was to install the USB NIC Fling, configure NTP, add storage and last but not least enable TPS! For a LAB this small any gain in memory availability is essential so TPS is really needed.

vCenter

I went with the smalles vCenter option available and for now it’s running ok with 2cpu’s and 14GB of ram.

The domain

The first VM to be deployed was my domain controller as that’s needed for just about everything I do. I went for a domain called LoaL.lab wich is an abbreviation for Lab On A Laptop. As always I name my vm’s with the environment name in it so the first VM to be deployed was LoaLDC which was quickly transformed into a domain controller, dns and dhcp server.

VM’s

The goal for this portable lab was to have something that can showcase ControlUp Integrated with vSphere, Horizon, App Volumes & DEM so I ended up with these VM’s. As you can see the connection server more or less is the bitch of this setup and has multiple functions:

NameFunctionCPUMemory
LoaLVCvCenter (Tiny)214
LoalDCDomain Controller26
LoaLCSVMware Horizon Connection Server, SQL Server, File server212
LoaLAPPApp Volumes Server26
LoalCUControlUp Monitor26
LoalRecVMware Horizon Session Recorder (needed for demo)24
LoalW10Static management VM, also available via Horizon28

Desktop Pools & RDS Farms

So besides the manual desktop pool for the mgmt VM I also have an Instant Clone Pool and and RDS Farm. For both I have an App Volume available with notepad ++ and one of the test users also has a writable volume. All iof this still works while I have made the desktops and rds machines as small as possible: 1 cpu and 1gb of ram! Don’t expect that you are able to do a lot but I can login and start notepad++ and that’s enough here. In the VDI pool there are 2 machines while there is a single RDS host deployed. Both Golden Images where optimized to dead with just about everything selected in the VMware OS Optimization Tool.

On the RDS Host the app volume is published using the per user on-demand integration that was added for Horizon 2206. See this article: Revolutionize virtual apps by publishing apps on demand on generic RDSH servers – VMware End-User Computing Blog

Notepad ++ as on-demand App Volume
The login sequence
And as seen from App Volumes

Resource Consumption

So with 64GB of ram this is clearly the bottleneck for this system but how is it looking after it has been running for a few hours? I am receiving a warning about memory consumption but the current usage is 58GB but I don’t see any swapping or ballooning so that’s nice. I also like the Shared Common metric

Power Consumption

While it might be less relevant for a mobile lab I am interested in the power consumption and this is the graph for the current period. This data is coming from a Blitzwolf smart plug connected to my Homey. So the peak at boot time comes close to 80W but seems to stabalize after that.

The ControlUp setup

As a ControlUp employee one of this lab’s usecases was to display what we can do with ControlUp. This is a brand new CU environment that is mainly using Real-Time DX but for fun I have also deployed the Edge DX Agent to a few servers.

PowerCLI Script to Horizon Desktop Pool machine counts & provisioning type

A long time ago in a galaxy far way I used to be a freelancer for ControlUp creating Script Actions and that actually helped me in securing a job with this great company. One of the first SBA’s that I made was one to change the amount of machines in a desktop pool. Recently one of our customers asked if it was possible to also control the minimum amount and powered on machines. Today I have updated this sba and it will be published shortly (if it hasn’t been published when you read this hit me up for a preview sba xml file). I took it a step further though and added the option to change the provisioning type. With a small security piece in place to prevent you from accidentally changing the type. Besides this being published as an sba I have also published a script that can be used from any computer using PowerCLI.

To be clear: this script uses PowerCLI with the SOAP api’s so it should work with almost all Horizon Versions since 7.5. If I find the time I will create a REST version but that will only work with Horizon 8 2111 and above.

The parameters:

  • Credentials : This optional parameter needs a credential object from get-credential. If you don’t supply it you will get a popup for credentials
  • HVDesktopPoolname: Required parameter with the name of the Desktop Pool to change
  • HVConnectionServerFQDN: Required parameter with the FQDN for a connection server to connect to
  • Provisioningtype: Optional Parameter if you want to change the provisioning type. Has to be either UP_FRONT or ON_DEMAND
  • ChangeProvisioningtype: optional parameter that needs either $true or $false and defaults to $false if not provided. The script will error if you set this to false while the provisionintype is different from the current one.
  • maxNumberOfMachines: required parameter with the maximum amount of machines
  • minNumberOfMachines: required parameter when using ON_DEMAND as provisioning type for the minimum amount of machines. Validation is done later in the script so it will not ask for an amount if not provided.
  • numberOfSpareMachines: required parameter when using ON_DEMAND as provisioning type for the minimum amount of powered on machines. Validation is done later in the script so it will not ask for an amount if not provided.

Usage:

Set-Desktoppoolmachinecountandtype.ps1 -Credentials $creds  -HVDesktopPoolname Pod01-Pool02 -HVConnectionServerFQDN pod1cbr1.loft.lab -Provisioningtype ON_DEMAND -maxNumberOfMachines 10 -minNumberOfMachines 3 -ChangeProvisioningtype $true -numberOfSpareMachines 4

or

Set-Desktoppoolmachinecountandtype.ps1 -Credentials $creds  -HVDesktopPoolname Pod01-Pool02 -HVConnectionServerFQDN pod1cbr1.loft.lab -Provisioningtype UP_FRONT -maxNumberOfMachines 10 -ChangeProvisioningtype $false

there’s an option to add -verbose for a bit more visibility, I will use this in my screenshots:

Changing the count for an pool that provisions all desktops up front

Changing the count & type but not setting the changeprovisioningtype to $true

Corrected changeprovisioningtype

As usual the script is available on Github or down below

<#
    .SYNOPSIS
    Changes the amount of Desktops in a Horizon Desktop Pool

    .DESCRIPTION
    This script changes the amount of Desktops in a Horizon Desktop Pool.

    .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 HVDesktopPoolname
    Name of the Desktop Pool to update

    .PARAMETER Provisioningtype
    Use ON_DEMAND to provision all desktops up front (will ignore minNumberOfMachines and numberOfSpareMachines

    .PARAMETER ChangeProvisioningtype
    User either True or False to enable or disable the changing of the provisioning type

    .PARAMETER maxNumberOfMachines
    Maximum number of desktops in the pool

    .PARAMETER minNumberOfMachines
    Minimum number of desktops in the pool

    .PARAMETER numberOfSpareMachines
    Minimum number of powered on desktops in the pool

    .PARAMETER HVConnectionServerFQDN
    FQDN for a connectionserver in the pod the pool belongs to.

    .EXAMPLE
    Set-Desktoppoolmachinecountandtype.ps1 -Credentials $creds  -HVDesktopPoolname Pod01-Pool02 -HVConnectionServerFQDN pod1cbr1.loft.lab -Provisioningtype ON_DEMAND -maxNumberOfMachines 10 -minNumberOfMachines 3 -ChangeProvisioningtype $true -numberOfSpareMachines 4
    
    .EXAMPLE
    Set-Desktoppoolmachinecountandtype.ps1 -Credentials $creds  -HVDesktopPoolname Pod01-Pool02 -HVConnectionServerFQDN pod1cbr1.loft.lab -Provisioningtype UP_FRONT -maxNumberOfMachines 10 -ChangeProvisioningtype $false

    .EXAMPLE
    Set-Desktoppoolmachinecountandtype.ps1 -Credentials $creds  -HVDesktopPoolname Pod01-Pool02 -HVConnectionServerFQDN pod1cbr1.loft.lab -maxNumberOfMachines 10

    .NOTES
    This script requires VMWare PowerCLI to be installed on the machine running the script.
    PowerCLI can be installed through PowerShell (PowerShell version 5 or higher required) by running the command 'Install-Module VMWare.PowerCLI -Force -AllowCLobber -Scope AllUsers' Or by using the 'Install VMware PowerCLI' script.
    Credentials can be set using the 'Prepare machine for Horizon View scripts' script.

    Modification history:   12/12/2019 - Wouter Kursten - First version
                            26/03/2022 - Wouter Kursten - Added options for on demand provisioning

    .LINK
    https://code.vmware.com/web/tool/11.3.0/vmware-powercli


    .COMPONENT
    VMWare PowerCLI

#>

[CmdletBinding()]
Param
(
    [Parameter(Mandatory=$false,
    HelpMessage='Credential object as domain\username with password' )]
    [PSCredential] $Credentials,

    [Parameter(
        Mandatory=$true,
        HelpMessage='Name of the Desktop Pool'
    )]
    [ValidateNotNullOrEmpty()]
    [string] $HVDesktopPoolname,

    [Parameter(
        Mandatory=$true,
        HelpMessage='FQDN for the connection server'
    )]
    [ValidateNotNullOrEmpty()]
    [string] $HVConnectionServerFQDN,

    [Parameter(
        Mandatory=$false,
        HelpMessage='Provisioning type'
    )]
    [ValidateSet("UP_FRONT","ON_DEMAND")]
    [string] $Provisioningtype,

    [Parameter(
        Mandatory=$false,
        HelpMessage='Change Provisioning type?'
    )]
    [ValidateSet("True","False")]
    [bool] $ChangeProvisioningtype = $false,

    [Parameter(
        Mandatory=$true,
        HelpMessage='Maximum number of machines in the desktop.'
    )]
    [ValidateNotNullOrEmpty()]
    [int] $maxNumberOfMachines,

    [Parameter(
        Mandatory=$false,
        ParameterSetName = 'ondemand',
        HelpMessage='The minimum number of machines to have provisioned if on demand provisioning is selected. Will be ignored if provisioningtype is set to UP_FRONT.'
    )]
    [ValidateNotNullOrEmpty()]
    [int] $minNumberOfMachines,

    [Parameter(
        Mandatory=$false,
        ParameterSetName = 'ondemand',
        HelpMessage='Number of spare powered on machines. Will be ignored if provisioningtype is set to UP_FRONT.'
    )]
    [ValidateNotNullOrEmpty()]
    [int] $numberOfSpareMachines
)

$ErrorActionPreference = 'Stop'

function Load-VMWareModules {
    <# Imports VMware modules
    NOTES:
    - The required modules to be loaded are passed as an array.
    - In versions of PowerCLI below 6.5 some of the modules can't be imported (below version 6 it is Snapins only) using so Add-PSSnapin is used (which automatically loads all VMWare modules)
    #>

    param (
        [parameter(Mandatory = $true,
            HelpMessage = "The VMware module to be loaded. Can be single or multiple values (as array).")]
        [array]$Components
    )

    # Try Import-Module for each passed component, try Add-PSSnapin if this fails (only if -Prefix was not specified)
    # Import each module, if Import-Module fails try Add-PSSnapin
    foreach ($component in $Components) {
        try {
            $null = Import-Module -Name VMware.$component
        }
        catch {
            try {
                $null = Add-PSSnapin -Name VMware
            }
            catch {
                write-error 'The required VMWare modules were not found as modules or snapins. Please check the .NOTES and .COMPONENTS sections in the Comments of this script for details.'
                exit
            }
        }
    }
}

function Connect-HorizonConnectionServer {
    param (
        [parameter(Mandatory = $true,
            HelpMessage = "The FQDN of the Horizon View Connection server. IP address may be used.")]
        [string]$HVConnectionServerFQDN,
        [parameter(Mandatory = $true,
            HelpMessage = "The PSCredential object used for authentication.")]
        [PSCredential]$Credential
    )
    # Try to connect to the Connection server
    try {
        Connect-HVServer -Server $HVConnectionServerFQDN -Credential $Credential
    }
    catch {
        write-error "There was a problem connecting to the Horizon View Connection server: $_."
        exit
    }
}

function Disconnect-HorizonConnectionServer {
    param (
        [parameter(Mandatory = $true,
            HelpMessage = "The Horizon View Connection server object.")]
        [VMware.VimAutomation.HorizonView.Impl.V1.ViewObjectImpl]$HVConnectionServer
    )
    # Try to connect from the connection server
    try {
        Disconnect-HVServer -Server $HVConnectionServer -Confirm:$false
    }
    catch {
        write-error  "There was a problem disconnecting from the Horizon View Connection server: $_"
        exit
    }
}

function Get-HVDesktopPool {
    param (
        [parameter(Mandatory = $true,
        HelpMessage = "Name of the Desktop Pool.")]
        [string]$HVPoolName,
        [parameter(Mandatory = $true,
        HelpMessage = "The Horizon View Connection server object.")]
        [VMware.VimAutomation.HorizonView.Impl.V1.ViewObjectImpl]$HVConnectionServer
    )
    # Try to get the Desktop pools in this pod
    try {
        # create the service object first
        [VMware.Hv.QueryServiceService]$queryService = New-Object VMware.Hv.QueryServiceService
        # Create the object with the definiton of what to query
        [VMware.Hv.QueryDefinition]$defn = New-Object VMware.Hv.QueryDefinition
        # entity type to query
        $defn.queryEntityType = 'DesktopSummaryView'
        # Filter oud rds desktop pools since they don't contain machines
        $defn.Filter = New-Object VMware.Hv.QueryFilterEquals -property @{'memberName'='desktopSummaryData.displayName'; 'value' = "$HVPoolname"}
        # Perform the actual query
        [array]$queryResults= ($queryService.queryService_create($HVConnectionServer.extensionData, $defn)).results
        # Remove the query
        $queryService.QueryService_DeleteAll($HVConnectionServer.extensionData)
        # Return the results
        if (!$queryResults){
            write-error  "Can't find $HVPoolName, exiting"
            exit
        }
        elseif (($queryResults).desktopsummarydata.type -eq "MANUAL"){
            write-output  "This a manual Horizon View Desktop Pool, cannot change the amount of desktops"
            exit
        }
        elseif (($queryResults).desktopsummarydata.source -eq "VIRTUAL_CENTER"){
            write-output  "This a Full Clone Horizon View Desktop Pool, if the amount of desktops has been reduced the extra systems need to be removed manually"
            return $queryResults
        }
        else {
            return $queryResults
        }
    }
    catch {
        write-error  "There was a problem retreiving the Horizon View Desktop Pool: $_"
        exit
    }
}

function get-hvpoolspec{
    param (
        [parameter(Mandatory = $true,
            HelpMessage = "ID of the Desktop Pool.")]
        [VMware.Hv.DesktopId]$HVPoolID,
        [parameter(Mandatory = $true,
            HelpMessage = "The Horizon View Connection server object.")]
        [VMware.VimAutomation.HorizonView.Impl.V1.ViewObjectImpl]$HVConnectionServer
    )
    try {
        $HVConnectionServer.ExtensionData.Desktop.Desktop_Get($HVPoolID)
    }
    catch {
        write-error "There was a problem retreiving the desktop pool details: $_"
        exit
    }
}

function Set-HVPool {
    param (
        [parameter(Mandatory = $true,
            HelpMessage = "ID of the Desktop Pool.")]
        [VMware.Hv.DesktopId]$HVPoolID,
        [parameter(Mandatory = $true,
        HelpMessage = "Provisioning type UP_FRONT or ON_DEMAND")]
        [ValidateSet("UP_FRONT","ON_DEMAND")]
        [string] $Provisioningtype,
        [parameter(Mandatory = $true,
            HelpMessage = "Desired amount of desktops in the pool.")]
        [int]$maxNumberOfMachines,
        [parameter(Mandatory = $false,
        HelpMessage = "Desired amount of spare desktops in the pool.")]
        [int]$numberOfSpareMachines,
        [parameter(Mandatory = $false,
        HelpMessage = "Desired minimum amount of desktops in the pool.")]
        [int]$minNumberOfMachines,
        [parameter(Mandatory = $true,
            HelpMessage = "The Horizon View Connection server object.")]
        [VMware.VimAutomation.HorizonView.Impl.V1.ViewObjectImpl]$HVConnectionServer
    )
    if($Provisioningtype -eq "UP_FRONT"){
        try {
            # First define the Service we need
            [VMware.Hv.DesktopService]$desktopservice=new-object vmware.hv.DesktopService
            # Fill the helper for this service with the application information
            $desktophelper=$desktopservice.read($HVConnectionServer.extensionData, $HVPoolID)
            # Change the state of the application in the helper
            $desktophelper.getAutomatedDesktopDataHelper().getVmNamingSettingsHelper().getPatternNamingSettingsHelper().setMaxNumberOfMachines($maxNumberOfMachines)
            $desktophelper.getAutomatedDesktopDataHelper().getVmNamingSettingsHelper().getPatternNamingSettingsHelper().setProvisioningTime("UP_FRONT")
            # Apply the helper to the actual object
            $desktopservice.update($HVConnectionServer.extensionData, $desktophelper)
        }
        catch {
            write-error "There was a problem changing the desktop count: $_"
            exit
        }
    }
    else{
        try {
            # First define the Service we need
            [VMware.Hv.DesktopService]$desktopservice=new-object vmware.hv.DesktopService
            # Fill the helper for this service with the application information
            $desktophelper=$desktopservice.read($HVConnectionServer.extensionData, $HVPoolID)
            # Change the state of the application in the helper
            $desktophelper.getAutomatedDesktopDataHelper().getVmNamingSettingsHelper().getPatternNamingSettingsHelper().setminNumberOfMachines($minNumberOfMachines)
            $desktophelper.getAutomatedDesktopDataHelper().getVmNamingSettingsHelper().getPatternNamingSettingsHelper().setMaxNumberOfMachines($maxNumberOfMachines)
            $desktophelper.getAutomatedDesktopDataHelper().getVmNamingSettingsHelper().getPatternNamingSettingsHelper().setnumberOfSpareMachines($numberOfSpareMachines)
            $desktophelper.getAutomatedDesktopDataHelper().getVmNamingSettingsHelper().getPatternNamingSettingsHelper().setProvisioningTime("ON_DEMAND")
            # Apply the helper to the actual object
            $desktopservice.update($HVConnectionServer.extensionData, $desktophelper)
        }
        catch {
            write-error "There was a problem changing the desktop count: $_"
            exit
        }
    }
}

write-verbose "Script will change this Desktop Pool: $HVDesktopPoolName"
write-verbose "Script will connect to this Connection Server: $HVConnectionServerFQDN "
if($Provisioningtype){
    write-verbose "Provisioningtype was set to $Provisioningtype"
}
else{
    write-verbose "No ProvisioningType was provided"
}

write-verbose "ChangeProvisioningtype was set to $ChangeProvisioningtype"
write-verbose "New Maximum Desktop Count is $maxNumberOfMachines "
if($minNumberOfMachines){
    write-verbose "minNumberOfMachines was set to $minNumberOfMachines"
}
else{
    write-verbose "No minNumberOfMachines was provided"
}

if($numberOfSpareMachines){
    write-verbose "numberOfSpareMachines was set to $numberOfSpareMachines"
}
else{
    write-verbose "No numberOfSpareMachines was provided"
}

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



# Connect to the Horizon View Connection Server

[VMware.VimAutomation.HorizonView.Impl.V1.ViewObjectImpl]$objHVConnectionServer = Connect-HorizonConnectionServer -HVConnectionServerFQDN $HVConnectionServerFQDN -Credential $creds

# Retreive the desktop pool
$HVPool=Get-HVDesktopPool -HVPoolName $HVDesktopPoolname -HVConnectionServer $objHVConnectionServer
write-verbose  "Retreived information about $HVDesktopPoolname"

# But we only need the ID
$HVPoolID=($HVPool).id

# Retreive the pool spec
$hvpoolspec=Get-HVPoolSpec -HVConnectionServer $objHVConnectionServer -HVPoolID $HVPoolID
$ProvisioningTime=($hvpoolspec).AutomatedDesktopData.VmNamingSettings.PatternNamingSettings.ProvisioningTime
write-verbose "Current provisioningtype = $ProvisioningTime"
write-verbose "Checking if provisioningtype matches the current setting and if I am allowed to change it."
if($Provisioningtype){
    if($ProvisioningTime -ne $provisioningtype -and $changeprovisioningtype -eq $False){
        write-error "Provisioningtype of $provisioningtype does not match the current provisioningtype. Set changeprovisioningtype to True to change the provisioningtype"
        exit
    }
    elseif($ProvisioningTime -ne $provisioningtype -and $changeprovisioningtype -eq $true){
        $Provisioningtype=$Provisioningtype.toupper()
        write-verbose "Changing Provisioningtype to $Provisioningtype"
    }
}
else{
    $Provisioningtype = $ProvisioningTime
}

if($Provisioningtype -eq "ON_DEMAND"){
    write-verbose "Checking if numberOfSpareMachines or minNumberOfMachines is missing"
    if(!$minNumberOfMachines -or !$numberOfSpareMachines){
        write-error "numberOfSpareMachines and minNumberOfMachines are required when using provisioningtype: $provisioningtype"
        exit
    }
}

# We cannot change manual pools so we give a warning about this and exit the script.
if ($hvpoolspec.Type -eq "MANUAL"){
    write-error "Could not execute, this a manual Horizon View Desktop Pool, cannot change the amount of desktops."
    exit
}

# When not all vm's are provisioned up front the max amount of machines can't be lower that the minimum amount or the number of spare machines.
if ($Provisioningtype -eq "ON_DEMAND"){
    if ($numberOfSpareMachines -ge $maxNumberOfMachines -or $minNumberOfMachines -ge $maxNumberOfMachines){
        write-error "Could not execute, the number of desktops cannot be smaller than the minimum amount of desktops or the number of spare desktops"
        exit
    }
}

# Change the desktop count in the pool

if($Provisioningtype -eq "UP_FRONT"){
    write-verbose "Provisioningtype is $Provisioningtype so ignoring minNumberOfMachines and numberOfSpareMachines if they have been added."
    write-verbose  "Trying to change $HVDesktopPoolname to $maxNumberOfMachines desktops."
    Set-HVPool -HVConnectionServer $objHVConnectionServer -HVPoolID $HVPoolID -maxNumberOfMachines $maxNumberOfMachines -Provisioningtype $Provisioningtype
    write-output  "Changed $HVDesktopPoolname to $maxNumberOfMachines desktops all provisioned up front."
}
else{
    write-verbose "Provisioningtype is $Provisioningtype so using minNumberOfMachines and numberOfSpareMachines."
    write-verbose  "Trying to change $HVDesktopPoolname to $maxNumberOfMachines desktops with a minimum of $minNumberOfMachines machines and $numberOfSpareMachines spares."
    Set-HVPool -HVConnectionServer $objHVConnectionServer -HVPoolID $HVPoolID -maxNumberOfMachines $maxNumberOfMachines -Provisioningtype $Provisioningtype -minNumberOfMachines $minNumberOfMachines -numberOfSpareMachines $numberOfSpareMachines
    write-output  "Changed $HVDesktopPoolname to $maxNumberOfMachines desktops with a minimum of $minNumberOfMachines machines and $numberOfSpareMachines spares."
}

# Disconnect from the connection server
Disconnect-HorizonConnectionServer -HVConnectionServer $objHVConnectionServer

 

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

ControlUp loves Horizon Session Recording!

Ok maybe it’s more me than ControlUp but the usual audience on this blogs knows that I work for ControlUp and that I love the VMware flings. Recently we received the question from several South-America based customers if we where able to start the Horizon Session Recordings using a Script Based Action. Technically we would have been able to but with the current version we would have to add the user to an ad group that was configured for this and than would be at the mercy of AD replication for the recording to actually start. Que Trentent Tye who reached out to the one and only Andrew Morgan (imho king of the VMware Horizon related flings) if it would be possible to add an API or PowerShell module to start the recordings. I guess Andrew’s reaction was that he saw the use case for this and within no-time he had a beta version ready for us that had a Powershell module. By the time you read this the new version has been added to the flings site that includes all of these goodies and you can find it here.

<I wanted to insert a picture of Andrew here wearing a crown but that was too much>

The Horizon Session Recording fling

First let’s look at what the Horizon Session Recording fling actually does.

VMware Horizon Session Recording allows administrators of a VMware Horizon environment to record their users activity in their Blast Extreme virtual desktop and application sessions. Recordings are uploaded from the agent devices to the central web service, for central storage and ease of viewing.

The Session Recordings are stored as MP4 files for watching via the web console or downloading to play in a local player.

So we have an agent> server application where the videos are stored on the server. During the setup I found that this was very easy but configuring a proper SSL certificate makes things a lot easier as you otherwise need to supply the thumbprint of the self-signed certificate during installation of the agent or in the registry.

The server interface is easy enough with the standard dashboard showing recent recordings while on the Recordings tab you can search for for specific recordings.

The agent is a plain installer that only asks for the server location and the SSL certificate thumbprint. The last one is only needed when the certificate itself is not trusted, if you used a VA signed certificate for the server there’s no need to enter the thumbprint.

Starting a recording

As said in the previous versions of the recorder it was required to configure an ad group and add users to that group to record their sessions. With this version we get a powershell module installed with the agent. It gets even better because if you copy the entire /api folder from the agent installation folder to another system you’re perfectly able to start recordings from there.

Starting a recording using powershell is a matter of importing the proper dll

import-module Horizon.SessionRecording.PowerShell.dll

And now the user can start the recording themselves if they have too many rights by using Start-HSRSessionRecording -SessionID %sessionid%. The session ID can be found as a metric in ControlUp or in the task manager if you add the column under users.

Start-HSRSessionRecording -SessionID 1

My user clearly didn’t have enough rights on the system so I got an error, actually not a bad thing so users can’t easily overload the server this way.

Update: Andrew confirmed this is by design to stop users from recording each other.

Starting the recording remote is a matter of adding the computername to the command

Start-HSRSessionRecording -ComputerName pod02-2001 -SessionID 1

Here you also see in the bottom right corner the message that the user receives when a recording is started.

And stopping is a matter of this command.

Stop-HSRSessionRecording -ComputerName pod02-2001 -SessionID 1

Other Cmdlets in the PS module

Integrating with ControlUp

To integrate the Horizon session recording with ControlUp I have created two basic Script Actions. both use three arguments, besides the required SessionId I add the username and machine name in the output so it’s obvious for whom the recording was started.

$sessionId=$args[0]
$username=$args[1]
$computer=$args[2]

try{
    $InstallDir = Get-ItemPropertyValue -path "hklm:\SOFTWARE\VMware, Inc.\VMware Blast\SessionRecordingAgent" -Name installdir
}
catch{
    write-host "Error determining the Horizon Session recording installation location. Please make sure the Horizon Sesison recording Agent is Installed."
}
try{
    import-module "$($InstallDir)\api\horizon.sessionrecording.powershell.dll"
}
catch{
    write-host "Error loading the Horizon Session Recording PowerShell Module. Make sure the latest vesrion of the Horizon Session Recording Agent is installed"
}
try{
    Start-HSRSessionRecording -SessionID $sessionId
}
Catch{
    write-host "Error starting the recording"
}

write-host "Started session recording for $username on $computer"
$sessionId=$args[0]
$username=$args[1]
$computer=$args[2]

try{
    $InstallDir = Get-ItemPropertyValue -path "hklm:\SOFTWARE\VMware, Inc.\VMware Blast\SessionRecordingAgent" -Name installdir
}
catch{
    write-host "Error determining the Horizon Session recording installation location. Please make sure the Horizon Sesison recording Agent is Installed."
}
try{
    import-module "$($InstallDir)\api\horizon.sessionrecording.powershell.dll"
}
catch{
    write-host "Error loading the Horizon Session Recording PowerShell Module. Make sure the latest vesrion of the Horizon Session Recording Agent is installed"
}
try{
    stop-HSRSessionRecording -SessionID $sessionId
}
Catch{
    write-host "Error stopping the recording"
}

write-host "Stopped session recording for $username on $computer"

Both these script will get better error handling and notifications in the future but you still see the old error in the background but with the notification that the recording was started.

And stopping the recording

And that shows how easy it is to control the Horizon Session Recording using a Script Based Action. If you combine this with a trigger, cpu usage for example you might be able to capture what is causing that spike, just remember to also create a trigger than stops it after the cpu goes down again.

Changelog

Version 2.2.0

Server Changes:

  • Added the ability to import server settings easily from another running server.
  • Added the ability to filter searches based on farm / pool.
  • Added the ability to lock and unlock sessions from the session view page.

Agent Changes:

1: Misc bugfixes.
2: PowerShell API to interact with the Recording agent, to perform such tasks as:

  • Start a session recording.
  • Stop a session recording.
  • Troubleshoot connectivity issues.
  • View machine configuration
  • Add a trusted certificate.