The VMware Labs flings monthly for November 2020: time for a new OSOT

In November only one new fling was released while seven other received update. One that received an update was the Horizon Session Recording fling that I blogged about at the time of release. Also it looks like the VSAN team is letting all its tools loose on the community because yet another tool for VSAN has been released (Storage Simulator Using Cellular Automata). The OS Optimization tool received some nice command-line features to improve your automated Golden images builds.

New Release

Storage Simulator Using Cellular Automata

Updates

VMware OS Optimization Tool

Supernova – Accelerating Machine Learning Inference

vRealize Build Tools

Horizon Session Recording

Sample Data Platform on VMware Cloud Foundation with VMware Tanzu for Kubernetes Provisioning

VMware Appliance for Folding@Home

ESXi Arm Edition

New Release

Storage Simulator Using Cellular Automata

Storage Simulator Using Cellular Automata is loosely based on the principles of cellular automata (CA) to model the performance characteristics of data path in a vSAN cluster. In general, CA can used to model and study any complex system with number of elements operating in parallel having short range relationships that as whole exhibit emergent behavior. When simulating a storage stack, we are modelling transmission of data blocks across a network of hardware resources communicating with each other through various interconnects. These includes processors, caches, DRAM, SSDs HDDs, PCIe links, ethernet links etc.

When modelling an IO request such as read/write, vSAN software stack applies various functions as the data block moves through this network. These functions include, data replication, parity calculation, checksum, encryption, compression etc. Some of these can lead to IO amplification.

This Fling implements a standalone vSAN simulation utility to aid developers in getting ideal speed-of-light (SOL) performance of a given cluster. This can be used as a starting point to rapidly iterate various ideas/features by making small changes to simulator and quantifying its potential performance impact. It can also be used by customers/partners to identify potential bottlenecks of their deployment under various type of workloads.

Updates

VMware OS Optimization Tool

Always good to see the OSOT getting some new features, bug fixes but laso some nice new command line features and also very important extra knobs to use for Office.

Changelog

November, 2020, b2000

Bug Fixes

  • Resolved the issue that stopped automatic logon in Server and WVD edition after Sysprep process.
  • Resolved a reboot prompt problem which displayed in process of generalizing on Win10 1607 LTSB.
  • Resolved the issue of failing to disable anti-virus feature on Windows 10 2004.
  • Fixed issue where re-enabling Windows Update would pull down feature updates by default.

Common Options

  • Common options selections are now remembered between different runs of the OSOT.
  • For all tabs, user now can apply different Common Options settings multiple times on optimized system.
  • Under Update tab, introduce a new option to switch on/off update feature of Office 365, 2016, 2019
  • Under Store Apps tab, disable checkbox for removed built-in apps

Update

  • New option to defer or directly trigger feature updates
  • New option to defer or directly trigger quality updates
  • New option to skip Office Click-to-Run updates
  • Added commands to stop and disable the App Volumes services when re-enabling Windows Update. These are then set back to automatic when Windows Update is disabled again.

Optimizations

  • Added the ability to export and import selected optimization items on the Optimize page (Export Selections and Import Selections).

Changes:

  • Default for “Touch Keyboard and Handwriting Panel Service” is now unselected by default to resolve missed language bar issue.
  • Default for “Connected Devices Platform Service” is now unselected by default.

New:

  • Turn off account privacy notifications in Office 365 and Office 2019

Supernova – Accelerating Machine Learning Inference

Project Supernova is to build a common machine learning inference service framework by enabling machine learning inference accelerators across edge endpoint devices, edge systems and cloud, with or without hardware accelerators.

Changelog

Version 1.1 Update

Support Bitfusion
K8S and docker-compose deployment

vRealize Build Tools

vRealize Build Tools provides tools to development and release teams implementing solutions based on vRealize Automation (vRA) and vRealize Orchestrator (vRO). The solution targets Virtual Infrastructure Administrators and Solution Developers working in parallel on multiple vRealize-based projects who want to use standard DevOps practices.

Changelog

Version 2.10.0 Update

  • [MVN] Improvements in package installer
  • [vROps] Regex support in YAML definitions for vROps content
  • [vRLI] Regex support in YAML definitions for vRLI content
  • [POL] Added Polyglot and ABX support:
    • NodeJS, Python and PowerShell code support
  • new archetype: com.vmware.pscoe.polyglot.archetypes:package-polyglot-archetype
    • two new project types: com.vmware.pscoe.polyglot:polyglot-project; com.vmware.pscoe.serverless:serverless-project
    • tooling for compiling, bundling and packaging: polyglotpkg
  • [TS] Added support for description field for workflow inputs and outputs in the Workflow decorator
  • [vRA-NG] Fixed NPE error during custom resources import

Horizon Session Recording

No need to explain the Horizon Session Recording anymore, just hit the link that I have posted at the beginning of this post.

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.

Sample Data Platform on VMware Cloud Foundation with VMware Tanzu for Kubernetes Provisioning

With this Fling, you will leverage your VMware Cloud Foundation 4.0 deployment and stand a sample data platform on a Tanzu Kubernetes Grid guest cluster in less than 20-minutes comprising of Kafka, Spark, Solr, and ELK.

Changelog

Version Update 1.1

  • Bug fix for storage class for bitnami kafka

VMware Appliance for Folding@Home

This Fling is a vSphere Appliance that contains the Folding@Home client software. Upon deploying the VMware Appliance for Folding@Home, the user will be prompted to enter information to configure the Folding@Home software. Once the appliance is deployed, the Folding@Home client is running and ready for Working Units. The Fling is also pre-configured to allow remote management of the Folding@Home client. For more information on the Folding@Home Project and how we can be a Force for Good against diseases like the Coronavirus, visit the website www.foldingathome.org.

Nov 18, 2020 – v1.0.5

  • F@H software has been updated to latest 7.6.21

VMware-Appliance-FaH_1.0.5.ova
MD5: 31d1a0c3dd4c308694f24cae77baee95

ESXi Arm Edition

Hello ESXi-Arm Fling participants!

Over the past several years, you’ve seen us demonstrate our virtualization technology on the Arm platform across several use cases, everything from running mission critical workloads on a windmill, to running on the SmartNIC, to running on AWS Graviton in the cloud. We realized that the resilient platform created for the datacenter can be equally valuable in non-traditional environments. We’ve learned a lot from exploratory discussions with customers and Arm Silicon Partners.

Changelog

November 30, 2020 – v1.2

Note: Upgrade is NOT possible, only fresh installation is supported. If you select “Preserve VMFS” option, you can re-register your existing Virtual Machines.

  • UI: Disable datastore browsing when no datastores are present
  • PSCI: Fix missing context_id argument for CPU_ON calls
  • GICv2: Always enable SGIs, as GIC-500
  • arm64: Support for big-endian guests
  • Remove requirements/restrictions on initrd for UEFI-less VMs

Build 17230755
VMware-VMvisor-Installer-7.0.0-17230755.aarch64.iso

[HorizonAPI] Disabling Provisioning and/or disabling entire Desktop Pools and RDS Farms

Today I saw the question on the VMware{Code} Slack Channel if anyone ever managed to disabled Desktop Pools using PowerCLI. I was like yeah I have done that and you might need to user the helperservice for that. I offered to create q fast and quick blog post about it so here we go.

First as always I connect to my Connection Server and use a query to retrieve the Pool that I am going to disable.

$creds=import-clixml creds.xml
$hvserver=connect-hvserver pod1cbr1.loft.lab -Credential $creds
$hvservice=$hvserver.ExtensionData
$poolqueryservice=new-object vmware.hv.queryserviceservice
$pooldefn = New-Object VMware.Hv.QueryDefinition
$filter = New-Object VMware.Hv.QueryFilterEquals -Property @{ 'memberName' = 'desktopSummaryData.name'; 'value' = "Pod01_Pool01" }
$pooldefn.filter=$filter
$pooldefn.queryentitytype='DesktopSummaryView'
$pool = ($poolqueryService.QueryService_Create($hvservice, $pooldefn)).results

With this object I can show you the details of the desktop pool

($hvservice.Desktop.Desktop_Get($pool.id)).base
($hvservice.Desktop.Desktop_Get($pool.id)).desktopsettings

Like I said to actually change things I need the helper service so this is what you do to initialize that.

$desktopservice=new-object vmware.hv.DesktopService
$desktophelper=$desktopservice.read($HVservice, $pool.id)
$desktophelper.getdesktopsettingshelper() | gm

As we saw in the second screenshot I need the desktopsettings and than Enabled

$desktophelper.getdesktopsettingshelper().getenabled()

To change the setting in the helper I need to use sethelper($False)

$desktophelper.getdesktopsettingshelper().setEnabled($False)

Now this has not been changed yet on the desktop pool itself, to do that we need to use desktopservice.update and I also show the result of the change.

$desktopservice.update($hvservice, $desktophelper)
($hvservice.Desktop.Desktop_Get($pool.id)).desktopsettings

And to reverse this

$desktophelper.getdesktopsettingshelper().setEnabled($True)
$desktopservice.update($hvservice, $desktophelper)
($hvservice.Desktop.Desktop_Get($pool.id)).desktopsettings

Disabling provisioning uses the same methodology just in another spot.

To disable provisioning ( the | gm is not needed, it’s just there to show you whats’s in there):

($hvservice.Desktop.Desktop_Get($pool.id)).automateddesktopdata.virtualcenterprovisioningsettings
$desktophelper.getAutomatedDesktopDataHelper().getVirtualCenterProvisioningSettingsHelper() | gm
$desktophelper.getAutomatedDesktopDataHelper().getVirtualCenterProvisioningSettingsHelper().getenableprovisioning()
$desktophelper.getAutomatedDesktopDataHelper().getVirtualCenterProvisioningSettingsHelper().setenableprovisioning($False)
$desktopservice.update($hvservice, $desktophelper)
($hvservice.Desktop.Desktop_Get($pool.id)).automateddesktopdata.virtualcenterprovisioningsettings

And to revert it

$desktophelper.getAutomatedDesktopDataHelper().getVirtualCenterProvisioningSettingsHelper().setenableprovisioning($True)
$desktopservice.update($hvservice, $desktophelper)
($hvservice.Desktop.Desktop_Get($pool.id)).automateddesktopdata.virtualcenterprovisioningsettings

For RDSH farms the process is similar some of the naming is just different. First to get the farm object

$farmqueryservice=new-object vmware.hv.queryserviceservice
$farmdefn = New-Object VMware.Hv.QueryDefinition
$filter = New-Object VMware.Hv.QueryFilterEquals -Property @{ 'memberName' = 'data.name'; 'value' = "Pod01-Farm01" }
$farmdefn.filter=$filter
$farmdefn.queryentitytype='FarmSummaryView'
$farm = ($farmqueryservice.QueryService_Create($hvservice, $farmdefn)).results
($hvservice.Farm.farm_get($farm.id)).data

And to create the helper and disable the farm

$farmservice=New-Object VMware.Hv.FarmService
$farmhelper=$farmservice.read($hvservice,$farm.id)
$farmhelper.getDataHelper().setenabled($False)
$farmservice.update($hvservice,$farmhelper)
($hvservice.Farm.farm_get($farm.id)).data

And in reverse 🙂

$farmhelper.getDataHelper().setenabled($True)
$farmservice.update($hvservice,$farmhelper)
($hvservice.Farm.farm_get($farm.id)).data

And now the provisioning part

($hvservice.Farm.farm_get($farm.id)).automatedfarmdata.virtualcenterprovisioningsettings
$farmhelper.getAutomatedFarmDataHelper().getvirtualcenterprovisioningsettingshelper().setenableprovisioning($False)
$farmservice.update($hvservice,$farmhelper)
($hvservice.Farm.farm_get($farm.id)).automatedfarmdata.virtualcenterprovisioningsettings

Guess what?

$farmhelper.getAutomatedFarmDataHelper().getvirtualcenterprovisioningsettingshelper().setenableprovisioning($True)
$farmservice.update($hvservice,$farmhelper)
($hvservice.Farm.farm_get($farm.id)).automatedfarmdata.virtualcenterprovisioningsettings

[HorizonAPI]Using the Datastore service (incl sizing calculation!)

I was looking on my blog for information to use the datastore information using the Horizon api’s but couldn’t find it so here’s a post on that.

This posts uses the soap api’s next time I’ll see what we can do with the REST api.

Index

First I will make a connection like I always do

Now let’s see what methods are available under the Datastore service

Let’s start with the easy 3 first aka the bottom ones

Datastore_ListDatastoresByHostOrCluster

The name says enough with Datastore_ListDatastoresByHostOrCluster you are able to list datastores using the HostOrClusterID.

I am cutting some corners here how to find this out but to get this HostOrClusterID we need to get the DatacenterId and to get that we’ll need the VirtualcenterId.

To get all virtualcenters in a pod you need to use virtualcenters_list() and what I do in this example is listing them first and than putting the first virtualcenter in an variable.

$hvservice.VirtualCenter.VirtualCenter_List()
$VC=$hvservice.VirtualCenter.VirtualCenter_List() | Select-Object -first 1

and the same for the datacenter using the virtualcenterID

$hvservice.Datacenter.Datacenter_List($vc.id)
$DC=$hvservice.Datacenter.Datacenter_List($vc.id) | Select-Object -first 1

With the datacenter ID I’ll retreive the info under HostOrCluster and store it in an variable.

$hvservice.HostOrCluster.HostOrCluster_GetHostOrClusterTree($dc.id)
$tree=$hvservice.HostOrCluster.HostOrCluster_GetHostOrClusterTree($dc.id)

Let’s browse this object and see what we can find

We can clearly see the name here and as I need Cluster_Pod2 I am putting that one in an object

$pod2cluster=$tree.TreeContainer.Children.info | select-object -last 1
$pod2cluster

And with this object I can get to my datastores and again I store them in an object

$hvservice.Datastore.Datastore_ListDatastoresByHostOrCluster($pod2cluster.id)
$datastores=$hvservice.Datastore.Datastore_ListDatastoresByHostOrCluster($pod2cluster.id)

Let’s see what’s in there

So we see most of the basic info in here that we might need including name, capacity and free space. Not sure why the numberofvm’s is empty as all of them have vm’s.

Datastore_ListDatastoresByDesktopOrFarm

Let’s see what we need for this one

So an object is needed of the type VMware.Hv.DatastoreSpec let’s define the object and see what’s in it.

As I am not 100% sure if all are required or not and what might break I’ll have a look at the API explorer article of this.

So it requires either a DesktopID OR a FarmID wile you can provide the hostorclusterId but that will be populated if you don’t provide one.

I am not going to build the query here to get a desktop pool so I’ll just use get-hvpool and get-hvfarm from the vmware.hv.helper powershell module.

Next I put the $pool.id in the spec and get the details

$spec.DesktopId=$pool.id
$hvservice.Datastore.Datastore_ListDatastoresByDesktopOrFarm($spec)
$datastores=$hvservice.Datastore.Datastore_ListDatastoresByDesktopOrFarm($spec)
$datastores.datastoredata

So this lists all the datastores that I have available in this cluster. I know this 100% sure as the ISO datastore is a read-only datastore that doesn’t have any desktops.

Let’s do the same using the farmId

$spec.DesktopId=$null
$spec.FarmId=$farm.id
$datastores=$hvservice.Datastore.Datastore_ListDatastoresByDesktopOrFarm($spec)
$datastores

Same amount of datastores so the same result.

Datastore_ListDatastoreClustersByHostOrCluster

As I don’t have any datastore clusters in my lab I cannot show it but you’ll need the same hostorclusterid as we used for Datastore_ListDatastoresByHostOrCluster

Datastore_GetUsage

This method shows what desktop pools are using a particular datastore. When doing a dry run it shows that a DatastoreId is needed.

I will use one of the items that I still have stored in my $datastores variable

$datastore=$datastores |Select-Object -last 1
$datastore.DatastoreData
$hvservice.Datastore.Datastore_GetUsage($datastore.id)

So this ia a rather boring datastore as it only has 1 pool configured to use it (and it doesn’t even have any vm’s from this pool on it) but you’ll see that there is another datastore configured for this pool as wel. I do have a more used datastore though on a local nvme drive.

$datastore=$datastores | where {$_.datastoredata.name -like "*nvme*"}
$hvservice.Datastore.Datastore_GetUsage($datastore.id)

As you can see it shows the desktop pools and even the single farm I have that use this datastore each with their own disk usage.

Datastore_GetDatastoreRequirements

The Datastore_GetDatastoreRequirements method does a calculation of what disk space might be needed for a desktop pool.

So let’s see what we need

$reqspec=new-object VMware.Hv.DatastoreRequirementSpec
$reqspec

That’s a lof and as a screenshot wouldn’t fit here is the link to the APi explorer page on it: here

To fill these things I will use the $pool variable that I still have stored.

$reqspec.DesktopId=$pool.id
$reqspec.Source="INSTANT_CLONE_ENGINE"
$reqspec.VmId=$pool.AutomatedDesktopData.VirtualCenterProvisioningSettings.VirtualCenterProvisioningData.parentvm
$reqspec.SnapshotId=$pool.AutomatedDesktopData.VirtualCenterProvisioningSettings.VirtualCenterProvisioningData.Snapshot
$reqspec.PoolSize=30
$hvservice.Datastore.Datastore_GetDatastoreRequirements($reqspec)

And when I change the poolsize

[HorizonAPI]Finding VDI or RDS machines based on wrong/old Golden Image

One of the first thing I did years ago when I first learned of the Horizon API’s was to start working on the vCheck for Horizon as I at that point was managing a Horizon 6.2* environment with lots of pools and lots of issues. With the vCheck I didn’t need to log into all pod’s anymore and nor did I need to check each and every pool after a recompose if all desktops had the correct image. Last week Guy Leech asked me if there was a script that could do this for RDS farms as he was working on a script that has to do with App Volumes & RDS hosts. I was like hell yeah we have that but when I looked at the vCheck and had to admit that it was a actually a hell no.

So after creating a new RDS image that could be used with Instant Clones this week it was time to create that vCheck. This morning and I even splashed a bug in the VDI wrong snapshot check when a Desktop Pool doesn’t have any machines in it. This led to this tweet that you might have seen:

SO what is actually the magic behind these checks? To be honest it is rather simple as the names of both the VM and the Snapshot in use are embedded in object both on pool/farm level and in the machine objects themselves.

First I connect to the connection server so we’ll use a credentials file and I also define 2 variables that we will use later

$hvconserver="pod2cbr1.loft.lab"
$credsfile="D:\homelab\creds.xml"

$creds=Import-Clixml $credsfile

$hvserver=connect-hvserver -Server $hvconserver -Credential $creds
$hvservice=$hvserver.ExtensionData
$wrongsnapdesktops=@()
$wrongsnaphosts=@()

After this I use 2 query’s to gather Pool and Farm information. The summaryviews don’t contain the needed information so I have to use farm.farm_get with the id to get what we need.

# --- Get Desktop pools
$poolqueryservice=new-object vmware.hv.queryserviceservice
$pooldefn = New-Object VMware.Hv.QueryDefinition
$pooldefn.queryentitytype='DesktopSummaryView'
$poolqueryResults = $poolqueryService.QueryService_Create($hvservice, $pooldefn)
$pools = foreach ($poolresult in $poolqueryResults.results){$hvservice.desktop.desktop_get($poolresult.id)}
$poolqueryservice.QueryService_DeleteAll($hvservice)
# --- Get RDS Farms

$Farmqueryservice=new-object vmware.hv.queryserviceservice
$Farmdefn = New-Object VMware.Hv.QueryDefinition
$Farmdefn.queryentitytype='FarmSummaryView'
$FarmqueryResults = $FarmqueryService.QueryService_Create($hvservice, $Farmdefn)
$farms = foreach ($farmresult in $farmqueryResults.results){$hvservice.farm.farm_get($farmresult.id)}
$Farmqueryservice.QueryService_DeleteAll($hvservice)

So how does this look?

and inside the automateddesktopdata and automatedfarmdata we find a property called virtualcenternamesdata that has what we need

Next I will create object for both the first farm and the first pool to show what where we need to look for in the vdi/machine objects

$queryservice=new-object vmware.hv.queryserviceservice
$defn = New-Object VMware.Hv.QueryDefinition
$defn.queryentitytype='MachineSummaryView'
$defn.filter = New-Object VMware.Hv.QueryFilterEquals -Property @{ 'memberName' = 'base.desktop'; 'value' = $pool.id }
$queryResults = $queryService.QueryService_Create($hvservice, $defn)
$poolmachines=$hvservice.machine.machine_getinfos($queryResults.results.id)


$queryservice=new-object vmware.hv.queryserviceservice
$defn = New-Object VMware.Hv.QueryDefinition
$defn.queryentitytype='RDSServerInfo'
$defn.filter = New-Object VMware.Hv.QueryFilterEquals -Property @{ 'memberName' = 'base.farm'; 'value' = $farm.ID }
$queryResults = $queryService.QueryService_Create($hvservice, $defn)
$farmmachines=$queryresults.results

As you can see I take an extra step for the desktops as the information that we need is not visible in the MachineSummaryView and the MachineDetailsView is a mess to run query’s for. The VDI machines have the GI and snapshot data stored in $machines.managedachinedata.viewcomposerdata (yes also for Instant Clones) while the rds hosts have it stored in RdsServerMaintenanceData.

After this it’s a matter of combining that information into a nice script that will grab it all for you.

$hvconserver="pod2cbr1.loft.lab"
$credsfile="D:\homelab\creds.xml"

$creds=Import-Clixml $credsfile

$hvserver=connect-hvserver -Server $hvconserver -Credential $creds
$hvservice=$hvserver.ExtensionData
$wrongsnapdesktops=@()
$wrongsnaphosts=@()

# --- Get Desktop pools
$poolqueryservice=new-object vmware.hv.queryserviceservice
$pooldefn = New-Object VMware.Hv.QueryDefinition
$pooldefn.queryentitytype='DesktopSummaryView'
$poolqueryResults = $poolqueryService.QueryService_Create($hvservice, $pooldefn)
$pools = foreach ($poolresult in $poolqueryResults.results){$hvservice.desktop.desktop_get($poolresult.id)}
$poolqueryservice.QueryService_DeleteAll($hvservice)
# --- Get RDS Farms

$Farmqueryservice=new-object vmware.hv.queryserviceservice
$Farmdefn = New-Object VMware.Hv.QueryDefinition
$Farmdefn.queryentitytype='FarmSummaryView'
$FarmqueryResults = $FarmqueryService.QueryService_Create($hvservice, $Farmdefn)
$farms = foreach ($farmresult in $farmqueryResults.results){$hvservice.farm.farm_get($farmresult.id)}
$Farmqueryservice.QueryService_DeleteAll($hvservice)




foreach ($pool in $pools){
  $poolname=$pool.base.name

  if ($pool.type -like "*automated*"){
    $queryservice=new-object vmware.hv.queryserviceservice
    $defn = New-Object VMware.Hv.QueryDefinition
    $defn.queryentitytype='MachineSummaryView'

    $defn.filter = New-Object VMware.Hv.QueryFilterEquals -Property @{ 'memberName' = 'base.desktop'; 'value' = $pool.id }

        $queryResults = $queryService.QueryService_Create($hvservice, $defn)

    if ($queryResults.results.count -ge 1){
      $poolmachines=$hvservice.machine.machine_getinfos($queryResults.results.id)
      $wrongsnaps=$poolmachines | where {$_.managedmachinedata.viewcomposerdata.baseimagesnapshotpath -notlike  $pool.automateddesktopdata.VirtualCenternamesdata.snapshotpath -OR $_.managedmachinedata.viewcomposerdata.baseimagepath -notlike $pool.automateddesktopdata.VirtualCenternamesdata.parentvmpath}
      if ($wrongsnaps){
        foreach ($wrongsnap in $wrongsnaps){
          $wrongsnapdesktops+= New-Object PSObject -Property @{
            "VM Name" = $wrongsnap.base.name;
            "VM Snapshot" = $wrongsnap.managedmachinedata.viewcomposerdata.baseimagesnapshotpath;
            "VM GI" = $wrongsnap.managedmachinedata.viewcomposerdata.baseimagepath;
            "Pool Snapshot" = $pool.automateddesktopdata.VirtualCenternamesdata.snapshotpath;
            "Pool GI" = $pool.automateddesktopdata.VirtualCenternamesdata.parentvmpath;
          }
        }
      }
    }
    $hvservice.QueryService.QueryService_DeleteAll()
  }
}

foreach ($farm in $farms){
  $farmname=$farm.data.name

  if ($farm.type -like "*automated*"){
    $queryservice=new-object vmware.hv.queryserviceservice
    $defn = New-Object VMware.Hv.QueryDefinition
    $defn.queryentitytype='RDSServerInfo'

    $defn.filter = New-Object VMware.Hv.QueryFilterEquals -Property @{ 'memberName' = 'base.farm'; 'value' = $farm.ID }

    $queryResults = $queryService.QueryService_Create($hvservice, $defn)
    $farmmachines=$queryResults.Results
    if ($farmmachines.count -ge 1){
      $wrongsnaps=$farmmachines | where {$_.rdsservermaintenancedata.baseimagesnapshotpath -notlike  $farm.automatedfarmdata.VirtualCenternamesdata.snapshotpath -OR $_.rdsservermaintenancedata.baseimagepath -notlike $farm.automatedfarmdata.VirtualCenternamesdata.parentvmpath}
      if ($wrongsnaps){
        foreach ($wrongsnap in $wrongsnaps){
          $wrongsnaphosts+= New-Object PSObject -Property @{
            "RDS Name" = $wrongsnap.base.name;
            "VM Snapshot" = $wrongsnap.rdsservermaintenancedata.baseimagesnapshotpath;
            "VM GI" = $wrongsnap.rdsservermaintenancedata.baseimagepath;
            "Farm Snapshot" = $farm.automatedfarmdata.VirtualCenternamesdata.snapshotpath;
            "Farm GI" = $farm.automatedfarmdata.VirtualCenternamesdata.parentvmpath;
          }
        }
      
      }
      $hvservice.QueryService.QueryService_DeleteAll()
    }
  }
}
$wrongsnaphosts
$wrongsnapdesktops

Yes this is the same idea as what I use in the vCheck and what I will be using in the ControlUp Script Based Action that I will be creating soon.

 

Using the Horizon 8 swagger page

A couple weeks back when Horizon 8 was released they also made us happy with the Swagger page to browse the rest api methods. One thing it lacks though is a way to easily authenticate to actually try them. There is an Authenticate button but I couldn’t find any information on what it actually needs. While creating my previous blog post I was messing around with things and actually found a way to authenticate. First I tried to authenticate using the actual api method for that but trying any call afterwards still showed me as not being authenticated. You could copy/paste the access token though and you’ll see in the script how that might work, or check the 3rd screenshot.

Let’s have a look at the script.

$url = read-host "url for connectionserver"

$username=read-host "Username"
$domain=Read-host "Domain"
$password=read-host "Password" -AsSecureString

$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 "$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)
}
try{
$accessToken = Open-HRConnection -username $username -password $UnsecurePassword -domain $Domain -url $url
Set-Clipboard (Get-HRHeader -accessToken $accessToken).Authorization
}
catch{
    write-host "Error while authenticating"
}

To make it directly usable I have chosen to ask for web address of the server, username, domain and password and in the end I copy the token you need to the clipboard for you. Let’s have a look at it

No further output but I can paste what I have in the clipboard now in the Authenticate field at the swagger page, hit authorize and close.

And now I can try api calls, pulling machines from the inventory for example.

SO that’s how we can actually use the Swagger page to try api calls.

[HorizonRestAPI] Trying some of those new funky Horizon 8 REST api’s

In my last post I promised to provide some examples of those new REST api’s in Horizon 8. A couple of things that I will show:

I have changed how I run my scripts a bit in that I decided to go even lazier and store my credentials in an xml file:

$credential = Get-Credential
$credential | Export-CliXml -Path 'C:\My\Path\cred.xml'

and in my script I retrieve them

$url = "https://pod1cbr1.loft.lab"

$credentials=Import-Clixml .\creds.xml
$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 "$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)
}

$accessToken = Open-HRConnection -username $username -password $UnsecurePassword -domain $Domain -url $url

Invoke-RestMethod -Method Get -uri "$url/rest/config/v1/ic-domain-accounts" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)

Sessions

The first call I will show is immediately one of the more important ones: session information. Currently only local sessions seem to be available so we’ll have to wait for global session information. This is the call I will use:

$sessions=Invoke-RestMethod -Method Get -uri "$url/rest/inventory/v1/sessions" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)

with this result

So s lot of data but less directly readable results than the soap api’s but we do see all of it including deeper levels. For applications we do see something new though.

Yes that’s actually the local application that the user launched. In this case it was through a Global Entitlement named Global_Notepad so it’s not showing that. According to the soap api’s this should also be shown there but they never do as far as I know.

Messages

One of the other things that we can do is send messages. For this we need to create an variable with the following information:

{
  "message": "Sample Info Message",
  "message_type": "INFO",
  "session_ids": [
    "7cdd624f-37d1-46c1-ab96-695a5d13956f"
  ]
}

To make it more fun I will send a message to all my desktop sessions I put those first into an variable

$desktopsessions=$sessions | where {$_.session_type -eq "DESKTOP"}

and I will create the json like this

$json=@{
  "message"="Wouter is sending a message";
  "message_type"="WARNING";
  "session_ids"=$desktopsessions.id -as [string[]]
}

this I will convert to a json and use the Put method

$json=@{
  "message"="Wouter is sending a message";
  "message_type"="WARNING";
  "session_ids"=$desktopsessions.id -as [string[]]
}
$body = $json | ConvertTo-Json
Invoke-RestMethod -Method Post -uri "$url/rest/inventory/v1/sessions/action/send-message" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken) -body $body

As you see I get all status codes 200 back so I know it was a success and we do see that on the desktops as well.

Machines

So getting all machines is as easy as 1,2,3 with /inventory/v1/machines.

$machines=Invoke-RestMethod -Method Get -uri "$url/rest/inventory/v1/machines" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)

Not a lot of new data, just less things we don’t need.

Reset

If you look good you’ll see that the machine I was showing is in the already used state. In my lab this happens because often I power down the lab while I still have some sessions running. Let’s reset this machine. What do we need for this first the api method and that’s /inventory/v1/machines/action/reset for requires:

Since I am far from fluent in REST api’s and json this took me a while to find out but I did it like this

$body=((Invoke-RestMethod -Method Get -uri "$url/rest/inventory/v1/machines" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)) | where {$_.state -eq "ALREADY_USED"}).id -as [string[]] | convertto-json
Invoke-RestMethod -Method Post -uri "$url/rest/inventory/v1/machines/action/reset" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken) -body $body

so I use the method to pull the machines, filter on the state being “ALREADY_USED”, take the id of this as a string and convert that to json. When select the body I need to add the quotes and straight brackets because if it is a single string the json won’t be usable json. I will show it later with multiple systems that it’s not needed with multiples.

$body=((Invoke-RestMethod -Method Get -uri "$url/rest/inventory/v1/machines" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken))  | where {$_.state -eq "AVAILABLE"}).id -as [string[]] | convertto-json
Invoke-RestMethod -Method Post -uri "$url/rest/inventory/v1/machines/action/reset" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken) -body $body

Horizon 8 released: Moar api’s!!

So yesterday every VMware EUC person was going wild because Horizon 8 was released. I won’t go into all the stuf that’s new because plenty of other folks have already done that (love the parentless instant clones though!). So what exactly are the new things looking from the API perspective? From the good old soap api’s I didn’t expect any changes and couldn’t find any either but a new api explorer page was published anyway. From the REST side a lot as changed. First of all here also a new api explorer page was published. Besides that an explanation of the API’s was actually posted on Techzone over here. On this page some excellent things can be found like there’s a swagger page now on your connection server: https://connectionserverfqdn/rest/swagger-ui.html as of now I haven’t found a way on the page itself to authenticate as you need an api key for the authorize button but that’s something I had requested anyway to make available.

But also a set of postman collections if that’s your preferred method to test api’s it has collections for all the Horizon releases that contained public rest api’s.

In the swagger ui it’s possible to browse all the api calls we can do and I’ll use that in later blog posts to actually do new things because I have seen a shitload of new possibilities!

 

[Horizon]Creating applications using PowerCLI

Something I didn’t handle previously was the creating of applications in Horizon. Since they are always hard wired to a farm or desktop it might happen that you need to re-create these so automation is preferred.

When looking at the api call for creating an application I find that we need to create an objetc of the type VMware.hv.ApplicationSpec

After defining this spec we’ll see that two objects are needed: Data and ExecutionData. This is also visible in the API Explorer.

Let’s define both of these and see what the options are.

Looking at the API explorer for Data only the name is actually required while for Executiondata only the Executablepath and the desktop or farm id is required

(going lazy here and using vmware.hv.helper to get the farmid)

And now I can create the application itself

[HorizonRestAPI] Handling Instant Clone Administrator accounts

One of the options already available using the Horizon REST API‘s is working with Instant Clone Administrators. In total there are 5 API calls available and I will give an explanation for al 5 on how to use them. As you can see you’ll run all of them against /rest/config/v1/ic-domain-accounts.

GET : for all Instant Clone Domain accounts

POST : to create a new Instant Clone Domain accounts

GET : To retreive a specific Instant Clone Domain account with it’s ID

PUT : to update an Instant Clone Domain account.

DELETE : To delete an Instant Clone Domain account

Getting Started

To start showing these I am starting with the same base that I used in my first blog post about the Horizon REST api’s:

$url = read-host -prompt "Connection server url" 
$username = read-host -prompt "Username" 
$password = read-host -prompt "Password" -AsSecureString 
$Domain = read-host -Prompt "Domain" 
$url = "https://pod1cbr1.loft.lab"


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

$accessToken = Open-HRConnection -username $username -password $UnsecurePassword -domain $Domain -url $url

Invoke-RestMethod -Method Get -uri "$url/rest/config/v1/ic-domain-accounts" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)

GET

The regular get is really straight forward, just invoke a get and you get the results.

Invoke-RestMethod -Method Get -uri "$url/rest/config/v1/ic-domain-accounts" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)

As you can see I currently have 2 accounts configured.

POST

With post we can configure a new Instant Clone Domain account. Let’s see what we need. According to the API explorer it looks like we need to supply a domain ID, password and account.

To get the domain ID we’ll actually need to do a GET against another url:

$domains=Invoke-RestMethod -Method Get -uri "$url/rest/external/v1/ad-domains" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)

Now I will create the json that we’ll need to configure the account. The $data variable is just a regular powershell array that  afterwards convert to the actual json

$domainid=$domains |select-object -expandproperty id -first 1

$data=@{
ad_domain_id= $domainid;
password= "password";
username= "username"
}

$body= $data | ConvertTo-Json

Now let’s use the Post method to apply this

Oops, too slow let’s authenticate and try again

Invoke-RestMethod -Method Post -uri "$url/rest/config/v1/ic-domain-accounts" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken) -body $body

There are a few remarks about this: no propper error is returned when a wrong username and password is used. Wen you try to create an account that already exists it will return a 409 conflict.

GET with ID

This is straightforward again, just extend the url for the get with the ID of the account you want to get. I grabbed this from the regular pul request and filtered on the user account I just created

$icaccounts= Invoke-RestMethod -Method Get -uri "$url/rest/config/v1/ic-domain-accounts" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken) 
$accountid=($icaccounts | where {$_.username -eq "username"}).id 
Invoke-RestMethod -Method Get -uri "$url/rest/config/v1/ic-domain-accounts/$accountid" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)

PUT

Put can be used to change a users password. It’s requires a combination of the url with the ID from the get with id and a body like in the Post.

$data=@{password="Demo-02"}
$body = $data | ConvertTo-Json
Invoke-RestMethod -Method Put -uri "$url/rest/config/v1/ic-domain-accounts/$accountid" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken) -Body $body

DELETE

To delete an account simply use the url with the id in it with the DELETE method

Invoke-RestMethod -Method Delete -uri "$url/rest/config/v1/ic-domain-accounts/$accountid" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)

 

[HorizonAPI] Getting started with the Horizon REST api

Until now all of my blogging about the Horizon api’s was about consuming the SOAP api using PowerCLI. Since a couple of releases Horizon also has a REST api and since 7.12 we are also able to change some settings using that. So now it’s time for me to dive into the Horizon REST api’s. I will consume them using Powershell since I am the most comfortable using that but you can use whatever method you prefer..

The REST api is just like the soap api documented at the VMware{CODE} api explorer.

First of all we need to create an accesstoken, we can do this by using some code that I simply stole from Andrew Morgan because why would I re-invent the wheel? From his git repository I grabbed three basic functions: get-HRHeader, Open-HRConnection and close-hrconnection. there’s also a refresh-hrconnection but I won’t need that for now.

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)
}
$accessToken = Open-HRConnection -username $username -password $password -domain $Domain -url $url

But we can’t do anything with only these functions, somehow we also need to supply username and password

$url = read-host -prompt "Connection server url"
$username = read-host -prompt "Username"
$password = read-host -prompt "Password" -AsSecureString
$Domain = read-host -Prompt "Domain"

$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password)
$UnsecurePassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)

(I am grabbing it from the command line here but when I run the scripts I have my creds hardcoded to make my life for the duration of this blog post a bit easier)

Next up is actually getting some data. The first thing that I wil do is show the connection servers. This can be done with the following API call. The part after -uri “$url/rest/ is what you can find int he api explorer. The method is the method also shown in the api explorer.

Invoke-RestMethod -Method Get -uri "$url/rest/monitor/connection-servers" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)

and the result:

Since one of the few things that you can already change using the rest api’s are the general settings I will take those as the next example

Invoke-RestMethod -Method Get -uri "$url/rest/config/v1/settings" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)

This works but I can’t say that it’s really usable. Now this is not the first time I do something with REST api’s (haven’t done it a lot though to be honest) so I know this can easily be converted to json to make it visible. What I will do is that I put it in a variable first.

$settings=Invoke-RestMethod -Method Get -uri "$url/rest/config/v1/settings" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)
$settings | ConvertTo-Json

Now this DOES look usable! Let’s take a look what is under general_settings

$settings.general_settings

Let’s say I want to change the forced logoff message

$settings.general_settings.forced_logoff_message="Get lost, the Bastard Operator From Hell is here."

Now my variable has the change but I need to send this to the server. This can be done using a put method and the settings variable has to be added as json. The second line is to pull the new settings from my connection server showing it directly in a json format.

 

Invoke-RestMethod -Method Put -uri "$url/rest/config/v1/settings" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken) -body ($settings | ConvertTo-Json)
Invoke-RestMethod -Method Get -uri "$url/rest/config/v1/settings" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken) | ConvertTo-Json

and in the admin interface:

That’s it for my 1ste blog post about the horizon REST api’s hopefully it’s useful! Below is an example of the script that I used.

$url = read-host -prompt "Connection server url" 
$username = read-host -prompt "Username" 
$password = read-host -prompt "Password" -AsSecureString 
$Domain = read-host -Prompt "Domain" 

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

$accessToken = Open-HRConnection -username $username -password $password -domain $Domain -url $url

Invoke-RestMethod -Method Get -uri "$url/rest/monitor/connection-servers" -ContentType "application/json" -Headers (Get-HRHeader -accessToken $accessToken)