Pushing a new image using the VMware Horizon Python Module

One of the REST api calls that where added for Horizon 8 2012 was the ability to push images to Desktop Pools (sadly not for farms yet). This week I added that functionality to the VMware Horizon Python Module. Looking at the swagger UI these are the needed arguments:

So the source can be either the streams from Horizon Cloud or a regular vm/snapshot combo. For the time you will need to use some moment in epoch. The optional items for adding the virtual tpm, stop on error I have set the default for what they are listed. As logoff policy I have chosen to set a default in WAIT_FOR_LOGOFF.

For this blog posts I have to go with the vm/snapshot combo as I don’t have streams setup at the moment. First I need to connect:

import requests, getpass, urllib, json, operator, numpy, time
import vmware_horizon


requests.packages.urllib3.disable_warnings()
url="https://pod2cbr1.loft.lab"
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")

Than I open the ports for the classes I will be using

monitor = obj=vmware_horizon.Monitor(url=hvconnectionobj.url, access_token=hvconnectionobj.access_token)
external=vmware_horizon.External(url=hvconnectionobj.url, access_token=hvconnectionobj.access_token)
inventory=vmware_horizon.Inventory(url=hvconnectionobj.url, access_token=hvconnectionobj.access_token)

Now let’s look at what the desktop_pool_push_image method needs

First I will grab the correct desktop pool, I will use Pod02-Pool02 this time. There are several ways to get the correct pool but I have chosen to use this one.

desktop_pools=inventory.get_desktop_pools()
desktop_pool = next(item for item in desktop_pools if item["name"] == "Pod02-Pool02")
poolid=desktop_pool["id"]

To get the VM and Snapshots I first need to get the vCenter and datacenter id’s

vcenters = monitor.virtual_centers()
vcid = vcenters[0]["id"]
dcs = external.get_datacenters(vcenter_id=vcid)
dcid = dcs[0]["id"]

I created a new golden image last Friday and it has this name: W10-L-2021-03-19-17-27 so I need to get the compatible base vm’s and get the id for this one

base_vms = external.get_base_vms(vcenter_id=vcid,datacenter_id=dcid,filter_incompatible_vms=True)
base_vm = next(item for item in base_vms if item["name"] == "W10-L-2021-03-19-17-27")
basevmid=base_vm["id"]

I had Packer create a snapshot and I can get that in a similar way

base_snapshots = external.get_base_snapshots(vcenter_id=vcid, base_vm_id=base_vm["id"])
base_snapshot = next(item for item in base_snapshots if item["name"] == "Created by Packer")
snapid=base_snapshot["id"]

I get the current time in epoch using the time module (google is your best friend to define a moment in the future in epoch)

current_time = time.time()

For this example I add all the arguments but if you don’t change fromt he defaults that’s not needed

inventory.desktop_pool_push_image(desktop_pool_id=poolid,parent_vm_id=basevmid,snapshot_id=snapid, start_time=current_time, add_virtual_tpm=False, stop_on_first_error=False, logoff_policy="FORCE_LOGOFF")

And closing the connection

end=hvconnectionobj.hv_disconnect()
print(end)

and when I now look at my desktop pool it’s pushing the new image

I have created a new folder on Github for examples and the script to deploy new images is the first example. I did move a couple of the names to variables so make ie better usable. You can find it here. Or see the code below this.

import requests, getpass, urllib, time
import vmware_horizon

requests.packages.urllib3.disable_warnings()

url                     = "https://pod2cbr1.loft.lab"
desktop_pool_name       = "Pod02-Pool01"
base_vm_name            = "W10-L-2021-03-19-17-27"
snapshot_name           = "Snap_2"

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")
monitor = obj=vmware_horizon.Monitor(url=hvconnectionobj.url, access_token=hvconnectionobj.access_token)
external=vmware_horizon.External(url=hvconnectionobj.url, access_token=hvconnectionobj.access_token)
inventory=vmware_horizon.Inventory(url=hvconnectionobj.url, access_token=hvconnectionobj.access_token)

desktop_pools=inventory.get_desktop_pools()
desktop_pool = next(item for item in desktop_pools if item["name"] == desktop_pool_name)
poolid=desktop_pool["id"]

vcenters = monitor.virtual_centers()
vcid = vcenters[0]["id"]
dcs = external.get_datacenters(vcenter_id=vcid)
dcid = dcs[0]["id"]

base_vms = external.get_base_vms(vcenter_id=vcid,datacenter_id=dcid,filter_incompatible_vms=True)
base_vm = next(item for item in base_vms if item["name"] == base_vm_name)
basevmid=base_vm["id"]

base_snapshots = external.get_base_snapshots(vcenter_id=vcid, base_vm_id=base_vm["id"])
base_snapshot = next(item for item in base_snapshots if item["name"] == snapshot_name)
snapid=base_snapshot["id"]

current_time = time.time()
inventory.desktop_pool_push_image(desktop_pool_id=poolid,parent_vm_id=basevmid,snapshot_id=snapid, start_time=current_time)

end=hvconnectionobj.hv_disconnect()
print(end)











 

 

My Golden Image build using HashiCorp Packer

After a Tweet last week by former colleague and fellow vExpert Jeroen Buren, my reaction on that and another question that we got  I decided to finally make some time and document how my Packer Golden Image build works. To be honest I don’t think that it’s anything spectacular and most of it has been borrowed from either Mark Brookfield or someone else but I forgot who, sorry for that! (if you recognize your work send me a note and I’ll update this piece) While my templates aren’t really complicated I am happy with them and they are exactly what I need in my lab. Things can definitely be done better but it’s enough for me.

I use 2 main files, 1 with the generic settings for the type of image and one that has the variables for the vCenter where it will be created. The last one looks like this:

{
    "vm_name":"W10-p2-{{isotime \"2006-01-02-15-04\"}}",
    "vcenter_server":"pod1vcr1.loft.lab",
    "username":"administrator@vsphere.local",
    "password":"hahahahanope!",
    "datastore":"NVME1TB (1)",
    "datastore_iso":"ISO",
    "cluster": "Cluster_Pod2",
    "network": "dpg_loft_102",
    "winrm_username": "Administrator",
    "winrm_password": "VMware1!"
}

The VM name is W10-p2-dateandtime the isotime combined with that default time makes sure that I get the current date and time of running the script. For more information see this page: https://www.packer.io/guides/workflow-tips-and-tricks/isotime-template-function. I have separate datastores for ISO’s and where the VM will be created while that port group is on a dVswitch.

The 2nd file is slightly more complicated:

{
    "builders": [
    {
        "type": "vsphere-iso",
        "vcenter_server":      "{{user `vcenter_server`}}",
        "username":            "{{user `username`}}",
        "password":            "{{user `password`}}",
        "insecure_connection": "true",
 
        "vm_name": "{{user `vm_name`}}",
        "datastore": "{{user `datastore`}}",
    "Notes": "Windows 10 1909 Instant Clone Image build using Packer {{isotime \"2006-01-02-15-04\"}}",
        "create_snapshot": true,
        "cluster": "{{user `cluster`}}",
        "network": "{{user `network`}}",
        "boot_order": "disk,cdrom",
 
        "vm_version":       15,  
        "guest_os_type": "windows9_64Guest",
    "firmware":	"bios",
 
        "communicator": "winrm",
        "winrm_username": "{{user `winrm_username`}}",
        "winrm_password": "{{user `winrm_password`}}",
    "winrm_timeout": "5h",
 
        "CPUs":             2,
        "RAM":              6064,
        "RAM_reserve_all":  false,
    "video_ram": 128000,
    
    "remove_cdrom": true,
 
        "disk_controller_type":  "pvscsi",
        "disk_size":        51200,
        "disk_thin_provisioned": true,
    
    "configuration_parameters": {
      "svga.autodetect" : "FALSE",
      "svga.numDisplays" : "2"
    },
 
        "network_card": "vmxnet3",
 
        "iso_paths": [
        "[{{user `datastore_iso`}}] Windows_10_1909_enterprise.iso",
        "[{{user `datastore_iso`}}] VMware-Tools-windows-11.0.5-15389592.iso"
        ],
 
        "floppy_files": [
            "{{template_dir}}/setup/"
        ],
        "floppy_img_path": "[{{user `datastore_iso`}}] floppy/pvscsi-Windows8.flp"
    }
    ],
 
    "provisioners": [
    {
            "type": "windows-shell",
      "script": "{{template_dir}}/setup/onedrive.cmd"
        },
    {
      "type": "windows-update",
      "search_criteria": "IsInstalled=0",
      "filters": [
        "exclude:$_.Title -like '*Preview*'",
        "include:$true"
      ],
      "update_limit": 25
    },
    {
      "type": "windows-restart",
      "restart_timeout": "15m",
      "restart_check_command": "powershell -command \"& {Write-Output 'restarted.'}\""
    },
        {
            "type": "powershell",
            "inline": [
        "Set-TimeZone -Id 'W. Europe Standard Time'",
                "Get-AppXPackage -AllUsers | Where {($_.name -notlike \"Photos\") -and ($_.Name -notlike \"Calculator\") -and ($_.Name -notlike \"Store\")} | Remove-AppXPackage -ErrorAction SilentlyContinue",
                "Get-AppXProvisionedPackage -Online | Where {($_.DisplayName -notlike \"Photos\") -and ($_.DisplayName -notlike \"Calculator\") -and ($_.DisplayName -notlike \"Store\")} | Remove-AppXProvisionedPackage -Online -ErrorAction SilentlyContinue"     
            ]
        },
    {
      "type": "windows-restart",
      "restart_timeout": "15m",
      "restart_check_command": "powershell -command \"& {Write-Output 'restarted.'}\""
    },
        {
            "type": "powershell",
            "scripts": [
                "{{template_dir}}/setup/Horizon_Agent_IC.ps1"
                "{{template_dir}}/setup/appvolumes.ps1",
                "{{template_dir}}/setup/dem.ps1",
        "{{template_dir}}/setup/fslogix.ps1",
                ,
        "{{template_dir}}/setup/CU.ps1"
            ]
        },
    {
      "type": "windows-restart",
      "restart_timeout": "15m",
      "restart_check_command": "powershell -command \"& {Write-Output 'restarted.'}\""
    },
    {
            "type": "powershell",
            "scripts": [
        "{{template_dir}}/setup/osot.ps1"
            ]
        }
    ]
}

Some specifics: to mark my GI’s I always create a note with the type and build date again using the isotime.

"Notes": "Windows 10 1909 Instant Clone Image build using Packer {{isotime \"2006-01-02-15-04\"}}",

And as I am very lazy I also have it creating a snapshot for me

"create_snapshot": true,

These make sure I have more than the default ram for the build in graphics adapter

"RAM":              6064,
"RAM_reserve_all":  false,
"video_ram": 128000,


"configuration_parameters": {
  "svga.autodetect" : "FALSE",
  "svga.numDisplays" : "2"
},

Some versions of Packer had an issue with ejecting the cd-rom’s but that has been fixed now.

"remove_cdrom": true,

There are several optimizations that take place like the app volumes script at the beginning (onedrive.cmd) and the VMware OS Optimization Tool in the end (osot.ps1).

All the agents are shared from a webserver and this is one of the ps1 scripts that starts the installation, the horizon agent in this case.

$ErrorActionPreference = "Stop"
 
$webserver = "loftfls01.loft.lab"
$url = "http://" + $webserver
$installer = "VMware-Horizon-Agent-x86_64-8.0.0-16530789.exe"
$listConfig = "/s /v ""/qn VDM_VC_MANAGED_AGENT=1 ADDLOCAL=Core,ClientDriveRedirection,RTAV,TSMMR,VmwVaudio,USB,NGVC,PerfTracker,HelpDesk"""
 
# Verify connectivity
Test-Connection $webserver -Count 1
 
# Get Horizon Agent
Invoke-WebRequest -Uri ($url + "/" + $installer) -OutFile C:\$installer
 
# Unblock installer
Unblock-File C:\$installer -Confirm:$false -ErrorAction Stop
 
# Install Horizon Agent
Try 
{
   Start-Process C:\$installer -ArgumentList $listConfig -PassThru -Wait -ErrorAction Stop
}
Catch
{
   Write-Error "Failed to install the Horizon Agent"
   Write-Error $_.Exception
   Exit -1 
}
 
# Cleanup on aisle 4...
Remove-Item C:\$installer -Confirm:$false

and the osot.ps1 looks like this

$ErrorActionPreference = "Stop"
 
$webserver = "loftfls01.loft.lab"
$url = "http://" + $webserver
$osot = "VMwareOSOptimizationTool.exe"
$osotConfig = "VMwareOSOptimizationTool.exe.config"
 
# Verify connectivity
Test-Connection $webserver -Count 1
 
# Get Files
ForEach ($file in $osot,$osotConfig) {
   Invoke-WebRequest -Uri ($url + "/" + $file) -OutFile C:\$file
}
 
# Run OSOT
C:\VMwareOSOptimizationTool.exe -o -t "VMware Templates\Windows 10 and Server 2016 or later" -f all
 
# Sleep before cleanup
Start-Sleep -Seconds 180
 
# Cleanup on aisle 4...
ForEach ($file in $osot,$osotConfig) {
   Remove-Item C:\$file -Confirm:$false
}

I have even created a simple powershell script that starts the build with a couple extra options. -Timestamp-ui to show the timestamp while the -force isn’t needed anymore as each build has it’s own name but I keep it in there.

[CmdletBinding()]
param(
Parameter(Mandatory)]
[string]$environmentfile,
[Parameter(Mandatory)]
[string]$buildfile
)

c:\software\packer\packer.exe build -force -timestamp-ui -var-file $environmentfile $buildfile

So how does this look?

I understand that this is far from a full explanation of all the options in the json files but I think most things are rather generic with a few things that I have highlighted.

Total running time in my lab highly depends on what host I use (core speed) and what iso is used as I also install Windows Updates. The server 2019 ISO updated in sept 2020 takes 40 minutes while Windows 10 1909 without extra patches takes just over an hour.

Jon Howe also did a nice write-up with some more explanation: https://www.virtjunkie.com/vmware-template-packer/#Packer_Template_File_User_Variables