Omnissa Horizon – Windows 11 24H2 Instant Clone Template

Welcome to my Omnissa Horizon session about a Packer + Ansible pipeline that builds the Windows 11 24H2 golden image used for a Horizon Instant Clone desktop pool. It’s built and run entirely from the same ansible worker VM (Ubuntu 24.04), driven by scripts/build.sh, and produces a VM that gets cloned and snapshotted for Horizon to fork Instant Clones from. For more info of setting up the automation platform see: Omnissa Horizon Golden Images – Setting Up the Automation Platform

Rather than inlining every file’s contents here, each phase below explains what actually happens and links straight to the real files on GitHub — so you’re always looking at the current, working version, not a copy that can drift out of date.

AI-assisted development

This OS-Build was done with assistance from Claude by Anthropic, particularly for some of the Packer/Ansible-related tasks etc. While I am not above using AI to reduce the amount of manual work involved, every part of the solution has been implemented, tested, and proven to work in my lab environment before being included in this write-up.

Why

Traditionally, Horizon templates are maintained manually and can be very time-consuming and prone with human errors, therefore automating this task makes a lot of sense.

The Tasks:

  • Deploy template
  • Patch template
  • Upgrade agents
  • Run OSOT
  • Take snapshot
  • Push image

The tech. used:

  • Packer
  • Ansible
  • vSphere API
  • Horizon API

Build Outcomes

The final result and goal of this exercise is a pipeline that automatically:

  • Builds a Windows 11 24H2 VM
  • Applies Windows updates
  • Installs Horizon Agent
  • Installs DEM Agent
  • Installs App Volumes Agent
  • Optimizes the OS
  • Generalizes the image
  • Creates a snapshot
  • Publishes to Horizon Desktop Pool

Prerequisites

  • vCenter access (API credentials with rights to create/clone/snapshot VMs in the target cluster/folder)
  • The Windows 11 24H2 business-editions ISO on the control node’s NFS mount — Packer uploads it to a datastore cache (packer_cache) on the first build and reuses it after that — plus the ESXi-provided VMware Tools ISO
  • An Ansible control node with pywinrm and requests_ntlm installed (NTLM transport is required — see the Provisioning phase below)
  • ansible-vault set up for secrets (vCenter credentials, the build Administrator password, the Horizon API password)
  • Network reachability from the build VM’s network segment to Windows Update or to your WSUS server — which one is chosen when you start the build
  • A DHCP reservation and DNS A record for the build VM’s name (w11-24h2-tpl) — scripts/build.sh’s pre-flight check refuses to start until the name resolves to the reserved IP
  • An existing Horizon instant-clone desktop pool to publish to
  • Omnissa Horizon Agent, Dynamic Environment Manager (DEM) Agent, App Volumes Agent, the OS Optimization Tool (OSOT), and sdelete64.exe, staged on an NFS share mounted on the Ansible control node (/mnt/wdnfs/Ansible/<component>/, one subfolder per component — see the folder structure below)
  • Optional, for a vGPU build: an ESXi host with an NVIDIA GPU, the NVIDIA vGPU host driver installed and the host graphics type set to Shared Direct
/mnt/wdnfs/Ansible/
├── windows/
│   └── en-us_windows_11_business_editions_version_24h2_..._x64_dvd_....iso
├── horizon/
│   └── Omnissa-Horizon-Agent-x86_64-2603-8.18.0-24273927036.exe
├── dem/
│   └── Omnissa Dynamic Environment Manager Enterprise 2603 10.19 x64.msi
├── appvolumes/
│   └── App Volumes Agent.msi
├── osot/
│   ├── OmnissaHorizonOSOptimizationTool-x86_64-1.2.2603.23605577104.exe
│   └── sdelete64.exe
└── apps/
    └── (empty by default — optional extra installers via apps_to_install)

Note: One thing this pipeline deliberately does not do: join the golden image to the domain. Horizon Instant Clone pools join each spawned desktop to the domain individually, at spawn time — the golden image itself is never domain-joined.

Repository

Everything below lives under operations/template-automation/w11_24h2_tpl/ in the bjosoren/omnissa repository, plus a handful of shared files under operations/template-automation/automation-platform/ (inventory, group_vars, and the orchestration scripts) that every image template shares. Site-specific values — placement, network, installer filenames, the target pool — go in inventory/group_vars/w11_24h2_tpl.local.yml on the control node (start from the committed w11_24h2_tpl.local.yml.example); that file is gitignored and loaded last, so it overrides every committed default.

How the pieces fit together

scripts/build.sh is the entry point — run with no arguments it lists every image it discovers under images/ and prompts you to pick one, or you can pass an image key directly (scripts/build.sh w11_24h2_tpl). An interactive placement picker then lists your real clusters, hosts, datastores, port groups and folders from vCenter, asks whether to add an NVIDIA vGPU (from the profiles your hosts actually offer), whether to run Windows Update and from where (Microsoft or your WSUS server), whether to collect build logs, which Horizon pool to publish to and whether to delete the build VM afterwards — and can save those answers so a later run replays them unattended with scripts/build.sh w11_24h2_tpl –config NAME. It then checks that the build VM doesn’t already exist in vCenter (preflight_check_vm.yml), resolves this image’s settings from inventory/group_vars/w11_24h2_tpl.yml, _agents.yml, _osot.yml, _adv-vm-settings.yml and w11_24h2_tpl.local.yml plus the shared group_vars/all.yml and the vault into Packer variables, runs Packer (which hands off to Ansible for everything in-guest), clones and snapshots the result, and pushes it live via scripts/publish_to_pool.sh. A log for each build is created for debugging.

The build, phase by phase

Phase 1 — Provisioning

Create the virtual machine

Packer’s vsphere-iso builder creates the build VM directly against vCenter: 2 vCPU / 2 cores-per-socket / 8GB RAM / 80GB thin-provisioned disk by default (Windows 11 needs at least 64GB), hardware version 20, a pvscsi disk controller and a vmxnet3 network adapter. Secure Boot is on, as on the RDSH image: the firmware is EFI with Secure Boot (efi-secure). There’s no vTPM — that would need a key provider in vCenter — so Windows Setup’s TPM check is bypassed in the answer file instead. If you picked a vGPU profile when starting the build, the VM also gets that NVIDIA vGPU and all of its memory reserved, which vSphere requires for a vGPU VM; the NVIDIA guest driver isn’t installed by the pipeline yet. Any VM advanced parameters you define (vSphere extraConfig) are applied to the build VM at creation, so the published clone and every instant clone inherit them. The Windows ISO is read from the control node’s NFS mount and uploaded to a datastore cache once; the VMware Tools ISO is mounted as a second CD-ROM.

Files used:

  • w11_24h2_tpl.pkr.hcl — the Packer source/build definition
  • variables.pkr.hcl — every variable declaration (VM sizing, Secure Boot, vCenter placement, install media, agent installer paths)
  • image.conf — the fixed build-VM name (w11-24h2-tpl) and the published-clone naming prefix (gi-w1124h2)

Settings: in inventory/group_vars/w11_24h2_tpl.yml and w11_24h2_tpl.local.yml

  • VM sizing (vm_cpu_count, vm_cores_per_socket, vm_mem_size_mb, vm_disk_size_mb, vm_version)
  • vm_secure_boot — true by default
  • vm_vgpu_profile — empty (no vGPU) by default; normally chosen in the placement picker
  • Placement (vcenter_cluster, vcenter_datastore, vcenter_folder, vcenter_network) — defaults the placement picker can override per build
  • Install media (win_iso_url, win_iso_checksum, remote_cache_datastore, remote_cache_path, iso_paths for the VMware Tools ISO)

VM advanced parameters live in their own optional file, inventory/group_vars/w11_24h2_tpl_adv-vm-settings.yml (vm_advanced_settings). scripts/vm_advanced_settings.py can dump the parameters currently set on a VM into that file as a starting point, and diff the file against a finished build to confirm they were applied.

vCenter connection itself (vcenter_server, credentials) comes from the shared group_vars/all.yml and vault.

Omnissa documentation: https://techzone.omnissa.com/resource/manually-creating-optimized-windows-images-horizon-vms#create-a-virtual-machine

Install Windows 11 & enter Audit Mode

Windows Setup reads an autounattend.xml answer file delivered on a virtual floppy (no boot_command needed to point it there). Its windowsPE pass partitions the disk, picks the edition and sets the KMS client key, and writes the LabConfig registry values that bypass Setup’s TPM, Secure Boot and RAM checks. The Secure Boot bypass is written on every build, even though the VM has Secure Boot on: a cleaned-up answer file that only wrote it when Secure Boot was off made Windows Setup stop with error 0x8007000D – 0x40030, and going back to the version that always writes it fixed the build. The oobeSystem pass sets Microsoft-Windows-Deployment/Reseal/Mode=Audit — the guest boots straight into Audit Mode on its very first boot, before OOBE or Ansible’s own “Gathering Facts” ever runs. The same floppy also carries a PowerShell script that the answer file’s auditUser pass invokes to silently install VMware Tools from the second CD-ROM. WinRM is enabled (and opened in the firewall on port 5985) during the auditSystem/auditUser passes so Ansible can reach the guest once Packer hands off.

Files used:

windows-vmtools.ps1‘s silent-install arguments (which components get included/excluded via ADDLOCAL/REMOVE) follow Broadcom’s own documentation: Specify VMware Tools Components for Silent Installations.

WinRM runs over plain HTTP with NTLM authentication during the build — NTLM because Windows Update’s API needs the logon token Basic authentication doesn’t provide, and plain HTTP because NTLM message encryption on top of it caused intermittent “Bad HTTP response” errors. That’s why the build VM belongs on an isolated build network.

Settings: in inventory/group_vars/w11_24h2_tpl.yml

  • win_image_name (must match an edition name inside the install.wim exactly — “Windows 11 Enterprise” by default)
  • win_kms_key (the public Windows 11 Enterprise KMS client key by default)
  • win_language
  • win_keyboard (Norwegian by default — “0409:00000409” for US)
  • timezone
  • guest_hostname (max 15 characters)
  • win_full_name
  • win_org_name
  • winrm_port

The build Administrator password (vault_win_admin_password) comes from the vault.

Omnissa documentation: https://techzone.omnissa.com/resource/manually-creating-optimized-windows-images-horizon-vms#install-windows


Phase 2 — Inside Audit Mode

Everything below runs as a single Ansible playbook pass over WinRM once the guest is up. Each role checks for a pending reboot before it starts and reboots and reconnects as needed; Ansible’s Windows modules handle a mid-playbook WinRM reboot without any extra orchestration.

Audit mode allows you to bypass the Windows Out-of-Box Experience (OOBE), the initial setup screen that asks for user information, region, and language, so you can run tasks such as adding drivers, installing applications, applying updates, running scripts, and applying optimizations.

Source: https://techzone.omnissa.com/resource/manually-creating-optimized-windows-images-horizon-vms?check_logged_in=1#enter-audit-mode

Prepare the guest & verify Secure Boot

A short base role creates C:\Temp for staging installers and the build log directory, then fails the build immediately if the guest’s UEFI Secure Boot state doesn’t match vm_secure_boot — so a template that was meant to be Secure Boot never silently ships without it.

Files used:

Settings: vm_secure_boot, build_log_dir and build_log_enabled in inventory/group_vars/w11_24h2_tpl.yml.

Verify VMware Tools

VMware Tools itself was already installed by the autounattend script above — this role just confirms the VMTools service is running, then loops (up to 4 rounds) checking for and clearing any pending-reboot state left over from that install, rebooting as needed.

Files used:

Settings: none — the check logic and round limit are fixed in the role.

Verify Audit Mode

A fail-fast checkpoint: reads the guest’s ImageState registry value and stops the build immediately if Audit Mode wasn’t actually entered, rather than letting several roles that assume it silently run against the wrong state.

Files used:

Settings: none.

Install .NET 3.5 Framework

Enables the NetFx3 optional feature from the mounted install media’s sources\sxs folder (Windows 11 uses win_optional_feature — win_feature is Server-only), reboots if required, then verifies the inbox .NET Framework is 4.6.2 or newer, which Horizon Agent’s Performance Tracker needs.

Files used:

Settings: none — feature name and media-path detection are fixed in the role.

Omnissa documentation: https://techzone.omnissa.com/resource/manually-creating-optimized-windows-images-horizon-vms#install-hypervisor-tools-and-drivers

Windows Updates

Runs a bounded loop (up to 6 rounds) of search-install-reboot, stopping early once a round reports nothing left to install. Each round runs as SYSTEM (a plain user-context WinRM session gets E_ACCESSDENIED from the underlying Windows Update API otherwise), with one always-excluded update — KB2267602, the recurring Defender definition update, which would otherwise keep the loop from ever settling. Any round that actually installs something appends an entry — KB numbers, titles, install status — to windows_update_report.log in the build’s log directory, so a build’s installed-update history is auditable afterward without needing the live console output. That log, along with every other role’s own log, is zipped up and pulled back to the control node at the end of the build.

Files used:

Where the updates come from is chosen when you start the build: Windows Update (Microsoft, over the internet), your own WSUS server, or no updates at all. The win_updates task in round.yml uses server_selection: windows_update or managed_server accordingly. With WSUS the role first writes WUServer/WUStatusServer (your wsus_server_url) and UseWUServer = 1 under the WindowsUpdate policy registry key, and removes them again after the last round, so the image carries no build-time WSUS setting — the clones get theirs from GPO. WSUS must have approved updates for Windows 11 24H2 in the categories the role searches (security, critical and update rollups); otherwise the loop finds nothing and simply moves on. For a WSUS server on SSL, give an https:// URL (usually port 8531) — the guest must then also trust the server’s certificate, which the pipeline doesn’t import for you yet.

Settings: enable_windows_update, windows_update_source (windows_update or wsus) and wsus_server_url are asked in the placement picker; the defaults come from inventory/group_vars/w11_24h2_tpl.yml, and your real WSUS URL belongs in w11_24h2_tpl.local.yml so the picker offers it as the default. wsus_keep_policy (default false) decides whether the WSUS setting stays in the image.

Omnissa documentation: https://techzone.omnissa.com/resource/manually-creating-optimized-windows-images-horizon-vms#update-windows

Install applications (optional)

An optional, empty-by-default hook: loops over a list of extra installers, copying and silently installing each one. Nothing runs here unless you populate the list.

Files used:

Settings: apps_to_install (a list of name/installer/arguments entries) in inventory/group_vars/w11_24h2_tpl.yml — empty by default.

Omnissa documentation: https://techzone.omnissa.com/resource/manually-creating-optimized-windows-images-horizon-vms#install-applications-into-the-golden-image

Omnissa OSOT Optimize

Runs OS Optimization Tool’s Optimize pass via its documented command-line interface rather than the GUI (the GUI hangs under WinRM because it needs an interactive desktop a non-interactive session doesn’t have). The task launches OSOT as a fire-and-forget async job, then polls OSOT’s own log file for a completion marker rather than trusting the async job status, and fails the build if the pass reports an error or doesn’t finish within an hour.

Files used:

The full command-line syntax for driving OSOT — including the job numbers the two Finalize passes below reference — is documented by Omnissa: Run Windows OS Optimization Tool for Horizon from the Command Line. This role runs a second time in Phase 3, after Generalize; its meta/main.yml sets allow_duplicates: true so it can appear twice in the same play.

Settings: osot_installer in inventory/group_vars/w11_24h2_tpl_osot.yml.

Omnissa documentation: https://techzone.omnissa.com/resource/manually-creating-optimized-windows-images-horizon-vms#optimize-windows-using-the-windows-os-optimization-tool-for-horizon

Guard Sysprep against per-user Store apps

Specific to Windows 11: Sysprep /generalize fails with SYSPREP_APPX if a Store app is installed for the build user but not provisioned for all users — and the Store happily auto-updates apps in the background during a long build. This role turns off Store automatic app updates by policy and removes any package that’s installed for the current user but not provisioned, right before Generalize.

Files used:

Settings: none.

Omnissa OSOT Generalize

Runs OSOT’s Generalize pass (Sysprep) with a custom answer file, so the reseal lands back at a WinRM-reachable state (auto-logon plus first-logon commands) instead of hanging at interactive OOBE. The WinRM connection is expected to die mid-command as Sysprep tears the session down; the role waits it out, forces a fresh connection with ansible.builtin.meta: reset_connection (a stale one surfaces as an “invalid selectors” fault on the next role), and waits for AppX provisioning to finish after the first logon before moving on.

Two optional manual pauses bracket this step: one before Generalize — the last chance to look around, since Generalize consumes a Sysprep rearm count and exits Audit Mode for good — and one right after Sysprep. Each waits for a marker file on the control node (for example touch /tmp/w11_24h2_tpl_pre_generalize_pause.go from a second shell) and fails the build after 60 minutes.

Files used:

Settings: in inventory/group_vars/w11_24h2_tpl.yml

  • osot_generalize_manual_pause — default false
  • osot_generalize_post_sysprep_pause — default false
  • verify_phase_logs_pause — default false; pauses after each OSOT step so you can check its log

The answer-file template also pulls timezone, win_keyboard, win_language, and the build Administrator password from the vault.

Omnissa documentation: https://techzone.omnissa.com/resource/manually-creating-optimized-windows-images-horizon-vms#generalize-windows-using-the-windows-os-optimization-tool-for-horizon


Phase 3 — Cleanup & agent installs

Audit Mode has now been exited by the Generalize/Sysprep step above. Everything from here runs against the resealed image.

Clean up Microsoft Store apps

Removes the listed Store apps for all users and from the provisioned set, so new profiles — including every instant clone’s users — don’t get them back. Needed after Generalize because its profile copy just carried the build profile’s Store apps into the Default User profile.

Files used:

Settings: store_apps_to_remove in inventory/group_vars/w11_24h2_tpl.yml — Microsoft.Copilot and Microsoft.BingSearch by default.

Omnissa OSOT Optimize — second pass

OSOT Optimize runs again here — same role, same command-line interface and log polling as the Phase 2 pass — because Sysprep reverts some of the first pass’s optimizations.

Files used:

Settings: osot_installer in inventory/group_vars/w11_24h2_tpl_osot.yml.

Omnissa documentation: https://techzone.omnissa.com/resource/manually-creating-optimized-windows-images-horizon-vms#clean-up-microsoft-store-apps

Install Horizon Agent

Installs Horizon Agent for Windows in VDI mode (there’s no RDS role on this image, so the agent installs as a desktop agent) with a per-feature ADDLOCAL list built from your own toggles, then verifies the install landed by checking the registry. Instant Clone support (NGVC) must stay enabled for an instant-clone pool.

Files used:

The full list of silent-install properties is documented by Omnissa: Silent Installation Properties for Horizon Agent for Windows.

Settings: in inventory/group_vars/w11_24h2_tpl_agents.yml

  • horizon_agent_installer — installer path
  • horizon_vc_managed
  • horizon_ngvc — Instant Clone support, required for instant clones
  • horizon_perf_tracker — needs .NET 4.6.2+ (checked in Phase 2)
  • one boolean per optional feature (Blast, USB, RTAV, CDR, smart card, scanner, print redirection, URL filtering and so on)

Install DEM Agent

Installs Omnissa Dynamic Environment Manager with an ADDLOCAL list built the same way as the Horizon Agent step, then verifies via the registry’s uninstall key.

Files used:

The ADDLOCAL feature list and other unattended-install options are documented by Omnissa: Unattended Installation of Dynamic Environment Manager.

Settings: in inventory/group_vars/w11_24h2_tpl_agents.yml

  • dem_agent_installer
  • dem_flex_engine
  • dem_flex_migrate
  • dem_self_support
  • dem_mgmt_console
  • dem_integration_enabled
  • dem_noad_config
  • dem_comp_env_config

Install App Volumes Agent

Installs the App Volumes Agent MSI, pointed at your App Volumes Manager, then verifies via the registry’s uninstall key.

Files used:

The silent-install command-line options are documented by Omnissa: Install App Volumes Agent Silently.

Settings: in inventory/group_vars/w11_24h2_tpl_agents.yml (appvolumes_manager belongs in w11_24h2_tpl.local.yml)

  • appvolumes_agent_installer
  • appvolumes_manager
  • appvolumes_port
  • appvolumes_ssl_validation

Omnissa documentation: https://techzone.omnissa.com/resource/manually-creating-optimized-windows-images-horizon-vms#install-the-omnissa-agents

Omnissa OSOT Finalize — part 1

The first of two Finalize passes — OSOT Finalize jobs 0–7: disk, profile and log housekeeping, ending with zeroing empty disk space via sdelete64.exe. Because OSOT’s own exit code can’t be fully trusted for SDelete’s real completion, this role separately polls for the sdelete64 process itself to disappear — up to 4 hours — before letting the build continue.

Files used:

Settings: in inventory/group_vars/w11_24h2_tpl_osot.yml

  • osot_installer
  • osot_finalize_sdelete64_installer
  • osot_finalize_cleanup_job_numbers — “0 1 2 3 4 5 6 7”

Omnissa OSOT Finalize — part 2

The second Finalize pass — jobs 9 and 10, clearing KMS licensing settings and flushing the DNS cache — kept separate and positioned as the very last OSOT step.

Files used:

Settings: in inventory/group_vars/w11_24h2_tpl_osot.yml

  • osot_installer
  • osot_finalize_job_numbers — “9 10”

Omnissa documentation: https://techzone.omnissa.com/resource/manually-creating-optimized-windows-images-horizon-vms#finalize-windows-using-the-windows-os-optimization-tool-for-horizon


Phase 4 — Build logs, cleanup & shutdown

Collect build logs

Zips the guest’s build log directory — every role’s log, OSOT’s logs and windows_update_report.log — and fetches the archive to images/w11_24h2_tpl/build_logs/ on the control node, so the evidence survives even after the build VM is deleted.

Files used:

Settings: collect_build_logs, answered at the placement prompt (or saved in a –config).

Cleanup

Clears the staged installers out of C:\Temp. Packer’s own shutdown command then powers the VM off — there’s no explicit shutdown task in the playbook itself.

Files used:

Omnissa documentation: https://techzone.omnissa.com/resource/manually-creating-optimized-windows-images-horizon-vms#prepare-the-image-for-deployment


Phase 5 — Publish

These steps run against the vCenter and Horizon APIs only, once Packer’s own build finishes — they never touch the guest OS.

Clone, fix NIC, and snapshot

The build VM itself is never snapshotted directly — it’s cloned to a separate, uniquely-named published VM first (gi-w1124h2-<timestamp>), avoiding a MAC/IP collision between a Horizon-locked published VM and the next build. The build VM’s own NIC is flipped to an auto-generated MAC before the clone, so the clone gets its own distinct MAC with no fixup needed afterward. The clone itself is a plain vCenter clone with no network override and no customization spec — an earlier version that cloned with a fuller reconfigure/customization spec made every published image fail Horizon’s instant-clone push with AGENT_CUSTOMIZATION_FAULT. A snapshot is then taken on the published clone; this is what Horizon forks Instant Clones from. If you chose to at the placement prompt, the build VM is deleted afterwards.

Files used:

Settings: the published VM’s name is generated from image.conf’s PUBLISHED_VM_PREFIX plus a timestamp; placement reuses the same vcenter_* settings from the provisioning phase.

Deploy to the Horizon desktop pool

Pushes the new snapshot out via the Horizon REST API to an instant-clone desktop pool (the RDSH image uses the farm workflow instead — separate object types with separate API endpoints). Publishing a new image starts recomposing the desktops in the target pool, so when the pool wasn’t already chosen at the placement prompt, this step stops for a confirmation showing the target pool name, with the option to type a different pool name instead.

Files used:

Settings: in inventory/group_vars/w11_24h2_tpl_agents.yml (real values in w11_24h2_tpl.local.yml):

  • horizon_connection_server
  • horizon_pool_name
  • horizon_api_username
  • horizon_api_domain

The API password (vault_horizon_api_password) comes from the vault.

Production checklist

Before you rely on this image in production, tick off the points below. Each row points back to the chapter that explains it.

DoneAreaCheckCovered in
☐PlatformThe shared automation platform has passed its own production checklistPlatform checklist
☐vCenterBuild account can create, clone and snapshot VMs in the target cluster and folderPrerequisites
☐Install mediaWindows 11 24H2 business-editions ISO on the NFS mount (Packer caches it in packer_cache on the first build) and the ESXi VMware Tools ISO availablePrerequisites
☐InstallersHorizon Agent, DEM Agent, App Volumes Agent, OSOT and sdelete64.exe on the NFS share, one subfolder per component. Real filenames in w11_24h2_tpl.local.ymlPrerequisites
☐Control nodepywinrm and requests_ntlm installed. NTLM is required for WinRMPrerequisites
☐VaultvCenter credentials, the build Administrator password and the Horizon API password are in the vaultPrerequisites
☐DNS / DHCPDHCP reservation and DNS A record for w11-24h2-tpl. build.sh refuses to start until the name resolves to the reserved IPPrerequisites
☐Secure Bootvm_secure_boot matches what the pool expects. The build stops if the guest’s Secure Boot state differsInside Audit Mode
☐Windows UpdateBuild network reaches Windows Update or WSUS. WSUS has approved Windows 11 24H2 updates (security, critical, update rollups). An SSL WSUS certificate is trusted by the guestInside Audit Mode
☐Domain joinThe desktop pool’s own domain and OU settings are in place. The image is never domain-joined; Horizon joins each desktop at spawn timePrerequisites
☐AgentsApp Volumes Manager address, port and SSL validation, and the Horizon Agent and DEM feature toggles, point at production valuesCleanup & agents
☐vGPU (optional)NVIDIA host driver installed and host graphics set to Shared DirectPrerequisites
☐Build logscollect_build_logs on, and the archive in images/w11_24h2_tpl/build_logs/ reviewed after the buildLogs & shutdown
☐HorizonAn existing instant-clone desktop pool. horizon_connection_server, horizon_pool_name and a REST API account with only the rights needed to publish. A push recomposes the desktops in the poolPublish
☐Test buildOne full build through build.sh, published to a test pool before any production pool is touchedPublish

Troubleshooting

When a build fails, open the build transcript first: it shows which role or step stopped and why. The other logs below give the detail for that step.

Where the logs are

LogLocationWhat it shows
Build transcriptControl node: images/w11_24h2_tpl/logs/<published VM name>.logEverything build.sh, Packer and Ansible printed for that run, one file per build. Start here
Guest build logsBuild VM: C:\ProgramData\Omnissa\LogsEach installer’s own log, the OSOT logs and windows_update_report.log
Collected log archiveControl node: images/w11_24h2_tpl/build_logs/build_logs_<timestamp>.zipThe guest log folder, zipped and fetched at the end of the build when you chose to collect build logs. Survives the build VM being deleted
SysprepBuild VM: C:\Windows\System32\Sysprep\Panther\setupact.log and setuperr.logWhy a Generalize pass failed

Common problems

SymptomLikely cause and fixSee
build.sh refuses to startPre-flight found a leftover w11-24h2-tpl VM, or the name doesn’t resolve to the reserved IP. Delete the old VM or fix DHCP and DNSPrerequisites
Build stops right after Packer hands overThe guest’s Secure Boot state doesn’t match vm_secure_bootInside Audit Mode
Update rounds install nothing (WSUS)No approved Windows 11 24H2 updates in security, critical or update rollups, or the guest doesn’t trust an SSL WSUS certificateInside Audit Mode
OSOT step fails after an hourOSOT never wrote its completion marker. Read the OSOT log in the guest log folderInside Audit Mode
Generalize fails with SYSPREP_APPXA Store app is installed for the build user but not provisioned for all users. Check setupact.log for the package nameInside Audit Mode
Finalize part 1 looks stucksdelete64 is zeroing free disk space. The role waits up to 4 hours before failingCleanup & agents
Pool push fails with AGENT_CUSTOMIZATION_FAULTThe published VM was cloned with a customization spec. Use the plain clone in clone_for_publish.pyPublish

Omnissa Documentation:

Other Sources:

Disclaimer: Every tips/tricks/posting I have published here, is tried and tested in different IT-solutions. It is not guaranteed to work everywhere, but is meant as a tip for other users out there. Remember, Google is your friend and don’t be afraid to steal with pride! Feel free to comment below as needed.