Welcome to my Omnissa Horizon session about a Packer + Ansible pipeline that builds the Windows Server 2025 RDSH golden image used for a Horizon Instant Clone farm. 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 Server 2025 VM
- Applies Windows updates
- Installs Horizon Agent
- Installs DEM Agent
- Installs App Volumes Agent
- Installs Horizon Client
- Optimizes the OS
- Generalizes the image
- Creates a snapshot
- Publishes to Horizon Farm
Prerequisites
- vCenter access (API credentials with rights to create/clone/snapshot VMs in the target cluster/folder)
- A Windows Server 2025 install ISO and the ESXi-provided VMware Tools ISO, already staged on a datastore Packer can mount directly
- 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 account 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 real build needs the update pass (see Windows Updates below)
- A volume-license (GVLK) key for Windows Server 2025 Datacenter
- Omnissa Horizon Agent, Horizon Client, 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/
├── horizon/
│ ├── Omnissa-Horizon-Agent-x86_64-2603-8.18.0-24273927036.exe
│ └── Omnissa-Horizon-Client-2606-8.19.0-32215845441.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 host to the domain individually, at spawn time — the golden image itself is never domain-joined.
Repository
Everything below lives under operations/template-automation/rdsh_2025_tpl/ in the bjosoren/omnissa repository, plus a handful of shared files under operations/template-automation/automation-platform/ (inventory, group_vars, and the two orchestration scripts) that every image template shares.
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 rdsh_2025_tpl). An interactive placement picker then lists your real clusters, ESXi hosts, datastores, port groups and folders from vCenter and asks the per-build questions: 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 farm to publish to and whether to delete the build VM afterwards. The answers can be saved so a later run replays them unattended with scripts/build.sh rdsh_2025_tpl –config NAME. It then checks whether this image’s build VM already exists in vCenter (preflight_check_vm.yml), checks the DNS record and that every installer is on the NFS share, resolves this image’s settings from inventory/group_vars/rdsh_2025_tpl.yml, _agents.yml, _osot.yml and rdsh_2025_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

Provisioning
Create the virtual machine
Packer’s vsphere-iso builder creates the build VM directly against vCenter: 4 vCPU / 2 cores-per-socket / 16GB RAM / 80GB thin-provisioned disk by default, EFI firmware with Secure Boot (efi-secure — set vm_secure_boot: false for plain EFI), no vTPM (Horizon adds its own to the clones from the farm settings), and a vmxnet3 network adapter. 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. It boots straight from the staged Windows Server ISO with the VMware Tools ISO mounted as a second CD-ROM.
Files used:
- rdsh_2025_tpl.pkr.hcl — the Packer source/build definition
- variables.pkr.hcl — every variable declaration (VM sizing, vCenter placement, install media, agent installer paths)
- image.conf — the fixed build-VM name and the published-clone naming prefix
Settings: in inventory/group_vars/rdsh_2025_tpl.yml and rdsh_2025_tpl.local.yml
- VM sizing (vm_cpu_count, vm_cores_per_socket, vm_mem_size_mb, vm_disk_size_mb)
- Placement (vcenter_cluster, vcenter_datastore, vcenter_folder, vcenter_network)
- Install media (iso_paths)
- vm_secure_boot — true by default
- vm_vgpu_profile — empty (no vGPU) by default; normally chosen in the placement picker
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 Server 2025 & enter Audit Mode
Windows Setup reads an autounattend.xml answer file delivered on a virtual floppy (no boot_command needed to point it there). That same answer file’s 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 during the auditSystem/auditUser passes so Ansible can reach the guest once Packer hands off.
Files used:
- floppy/autounattend.pkrtpl.hcl — the unattend answer-file template
- floppy/windows-vmtools.ps1 — silent VMware Tools installer, run from the answer file
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.
Settings: in inventory/group_vars/rdsh_2025_tpl.yml
- win_image_name (must match an edition name inside the mounted install.wim exactly)
- win_kms_key
- win_language
- win_keyboard
- timezone
- guest_hostname
- win_full_name
- win_org_name
The build account 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 (starting with a short base role that preps a temp/log directory and stops the build if the guest’s UEFI Secure Boot state doesn’t match vm_secure_boot). Each role 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.
Install 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 Windows feature from the mounted install media’s sources\sxs folder (auto-detected via WMI), then reboots if required.
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. This step matters more than it might look: skipping it was the actual root cause of a real DISM failure later in the pipeline (the RDS role install needs a complete component store, and without one it falls back to Windows Update — which the isolated build network can’t reach). Each round runs under a SYSTEM-escalated task (a plain user-context WinRM session gets E_ACCESSDENIED from the underlying Windows Update API otherwise), with one always-excluded update (a recurring Defender-definition KB that was looping indefinitely). 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; rounds that find nothing to install are skipped as noise. That log, along with every other role’s own log, is zipped up and pulled back to the control node automatically at the end of the build.
Files used:
- roles/windows_update/tasks/main.yml
- roles/windows_update/tasks/round.yml (the per-round logic)
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 answer is passed to the playbook, and the win_updates task in round.yml uses server_selection: windows_update or managed_server accordingly.
With WSUS, the role:
- writes WUServer and WUStatusServer (your wsus_server_url) and UseWUServer = 1 under the WindowsUpdate policy registry key, and restarts the Windows Update service
- runs the update rounds against WSUS
- removes those values again after the last round, so the image carries no build-time WSUS setting — the clones get theirs from GPO
- finds nothing to install if WSUS hasn’t approved updates for Windows Server 2025 in the categories the role searches (security, critical and update rollups) — the loop then simply moves on, so check your approvals
If your WSUS server uses SSL, give an https:// URL (WSUS’s SSL binding is typically port 8531 rather than the default 8530). Since the VM isn’t domain-joined and won’t auto-enroll a certificate, the guest must also trust the WSUS server’s certificate — the pipeline doesn’t import it 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/rdsh_2025_tpl.yml, and your real WSUS URL belongs in rdsh_2025_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. Only skip updates for a quick verification run of everything else, since a real build may hit the DISM failure above without them.
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/rdsh_2025_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 (so the process survives outside WinRM’s own job object), then polls OSOT’s own log file directly 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 also runs a second time later in the pipeline, in Phase 3 right after Store apps cleanup — see that section below. Its meta/main.yml sets allow_duplicates: true specifically so it can appear twice in the same play.
Settings: in inventory/group_vars/rdsh_2025_tpl_osot.yml
- osot_installer — path to the OSOT executable
- osot_template — exists but deliberately unused — passing it as OSOT’s -t argument failed to load in testing” reads cleaner.
Omnissa documentation: https://techzone.omnissa.com/resource/manually-creating-optimized-windows-images-horizon-vms#optimize-windows-using-the-windows-os-optimization-tool-for-horizon
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. Optionally pauses for a manual review first — Generalize consumes a Sysprep rearm count and permanently exits Audit Mode, so this is the last point to check anything by hand. The WinRM connection is expected to die mid-command as Sysprep tears the session down; the role waits it out and reconnects once the guest comes back.
A stale WinRM connection surviving that reboot used to surface later as an “invalid selectors” fault on the next role, since Ansible’s WinRM connection plugin doesn’t reset itself after a manual reboot the way it would after ansible.windows.win_reboot. The role now forces a fresh connection immediately after Generalize completes with ansible.builtin.meta: reset_connection, so nothing downstream inherits the stale session.
Files used:
Settings: in inventory/group_vars/rdsh_2025_tpl_osot.yml
- osot_generalize_manual_pause — default false
- osot_installer
The answer-file template also pulls timezone, win_keyboard, win_language, and the build admin 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 two specific Store-delivered apps (Copilot and Bing Search) for all users, including the provisioned/default-user copy — needed specifically because the Generalize step’s profile-copy behavior just copied the build profile, Store apps included, into the Default User profile.
Files used:
Settings: none — the app list is fixed in the role.
Omnissa OSOT Optimize — second pass
OSOT Optimize runs again here, right after the Store apps cleanup above and before the RDS role install below — same role, same command-line interface and log-polling as the Phase 2 pass, just invoked a second time now that Audit Mode has been exited and a few more changes have landed on the guest.
Files used:
- roles/osot_optimize/tasks/main.yml — same role as the Phase 2 pass; its meta/main.yml sets allow_duplicates: true so it can run twice in one play
Settings: osot_installer in inventory/group_vars/rdsh_2025_tpl_osot.yml (same setting as the Phase 2 pass).
Omnissa documentation: https://techzone.omnissa.com/resource/manually-creating-optimized-windows-images-horizon-vms#clean-up-microsoft-store-apps
Install the RDS role
Installs the RDS-RD-Server Windows feature plus the .NET 4.x features Horizon Agent’s PerfTracker needs, pointed explicitly at the install media’s own feature source (without that, DISM silently falls back to Windows Update). Horizon Agent auto-detects RDSH mode from this role’s presence rather than needing a separate flag, so it has to run before the Horizon Agent install below.
Files used:
Settings: none — feature names and the media-source path are fixed in the role.
Install Horizon Agent
Installs Horizon Agent for Windows with a per-feature ADDLOCAL list built from your own toggles (Blast, USB redirection, RTAV, smart card, and so on), then verifies the install landed by checking the registry.
Files used:
The full list of silent-install properties (ADDLOCAL feature names, install options) is documented by Omnissa: Silent Installation Properties for Horizon Agent for Windows.
Settings: in inventory/group_vars/rdsh_2025_tpl_agents.yml
- horizon_agent_installer — installer path
- horizon_vc_managed
- one boolean per optional feature
Install Horizon Client
Installs Omnissa Horizon Client on the RDSH host, for users who open nested sessions from their RDSH session, with Log in as Current User installed and selected by default. It runs Omnissa’s documented silent install — Omnissa-Horizon-Client-<version>.exe /silent /norestart LOGINASCURRENTUSER_DISPLAY=1 LOGINASCURRENTUSER_DEFAULT=1 ADDLOCAL=TSSO — reboots if the installer asks for it, checks that the client is registered as installed, and copies the installer’s own logs into the build log directory. It runs right after the Horizon Agent install.
Files used:
The command-line options (ADDLOCAL=TSSO, LOGINASCURRENTUSER_DISPLAY, LOGINASCURRENTUSER_DEFAULT and the rest) are documented by Omnissa: Install Horizon Client from the Command Line.
Settings: in inventory/group_vars/rdsh_2025_tpl_agents.yml (the real installer filename belongs in rdsh_2025_tpl.local.yml)
- install_horizon_client — true by default; set it to false to leave the client out
- horizon_client_installer — installer path on the NFS share; scripts/build.sh checks it exists before the build starts
- horizon_client_addlocal — TSSO (Log in as Current User)
- horizon_client_login_as_current_user_display and horizon_client_login_as_current_user_default — both true
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/rdsh_2025_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 (manager address, port, SSL validation, and so on) are documented by Omnissa: Install App Volumes Agent Silently.
Settings: in inventory/group_vars/rdsh_2025_tpl_agents.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 — disk, profile, and log housekeeping (NGEN, DISM cleanup, Compact OS, Disk Cleanup, clearing event logs, disabling Superfetch, clearing the default user profile, and zeroing empty disk space via sdelete64.exe). Because OSOT’s own exit code can’t be fully trusted for the SDelete job’s real completion, this role separately polls for the sdelete64 process itself to disappear — up to 4 hours — before letting the build continue, so the next step never overlaps with a still-running SDelete pass.
Files used:
Settings: in inventory/group_vars/rdsh_2025_tpl_osot.yml
- osot_installer
- osot_finalize_sdelete64_installer
- osot_finalize_cleanup_job_numbers
Omnissa OSOT Finalize — part 2
The second Finalize pass — just two jobs, clearing KMS licensing settings and flushing the DNS cache — kept separate from part 1 and positioned as the very last OSOT step, right before hardening and cleanup.
Files used:
Settings: in inventory/group_vars/rdsh_2025_tpl_osot.yml
- osot_installer
- osot_finalize_job_numbers
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 — Final housekeeping & shutdown
Hardening & cleanup
The hardening step is documentation-only in this pipeline on purpose — RD licensing mode and Remote Desktop Users group membership both need to be handled by a domain GPO instead, since this golden image is never domain-joined (a domain group’s SID can’t be resolved without one). The cleanup step just 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 two steps run against the vCenter API 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 (avoiding a MAC/IP collision between a Horizon-locked published VM and the next build’s own guest). The build VM’s own NIC is flipped to an auto-generated MAC before the clone happens, not after — this environment’s clone carries the source NIC’s MAC allocation type over to the destination as-is, so once the build VM’s NIC is already “Automatic”, the clone gets its own distinct MAC for free with no fixup needed afterward. The clone itself is a plain vCenter clone with no network override and no customization spec — the pyVmomi equivalent of a bare vSphere Client clone or PowerCLI’s New-VM. That’s load-bearing, not stylistic: an earlier version of this step cloned via Ansible’s community.vmware.vmware_guest module with an explicit networks: parameter, which builds a fuller reconfigure/customization spec on top of the clone — and every image published that way failed Horizon’s instant-clone push with AGENT_CUSTOMIZATION_FAULT. A snapshot is then taken on the published clone; this is what Horizon actually forks Instant Clones from.
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 farm
Pushes the new snapshot out via the Horizon REST API. RDSH images publish to a farm, not a desktop pool (those are genuinely separate object types with separate API endpoints in Horizon — a VDI desktop template would use the pool workflow instead). Publishing a new image starts recomposing every machine currently provisioned from the target farm, so the target is always confirmed first. When you picked the farm in the placement picker (or saved it in a –config), that choice is the confirmation and the push starts right away; run by hand without a target, the script stops for a plain y/N showing the target farm name, with the option to type a different farm name instead.
Files used:
Settings: in inventory/group_vars/rdsh_2025_tpl_agents.yml:
- horizon_connection_server
- horizon_farm_name
- horizon_api_username
- horizon_api_domain
The 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.
| Done | Area | Check | Covered in |
|---|---|---|---|
| ☐ | Platform | The shared automation platform has passed its own production checklist | Platform checklist |
| ☐ | vCenter | Build account can create, clone and snapshot VMs in the target cluster and folder | Prerequisites |
| ☐ | Install media | Windows Server 2025 ISO and the ESXi VMware Tools ISO staged on a datastore Packer can mount. win_image_name matches an edition inside install.wim exactly | Provisioning |
| ☐ | Licensing | win_kms_key holds the Windows Server 2025 Datacenter volume-license (GVLK) key | Prerequisites |
| ☐ | Installers | Horizon Agent, Horizon Client, DEM Agent, App Volumes Agent, OSOT and sdelete64.exe on the NFS share, one subfolder per component. Real filenames in rdsh_2025_tpl.local.yml | Prerequisites |
| ☐ | Control node | pywinrm and requests_ntlm installed. NTLM is required for WinRM | Prerequisites |
| ☐ | Vault | vCenter credentials, the build account password and the Horizon API password are in the vault | Prerequisites, Publish |
| ☐ | DNS | The build VM’s name resolves in DNS before the build starts | How the pieces fit together |
| ☐ | Secure Boot | vm_secure_boot matches what the farm expects. The build stops if the guest’s Secure Boot state differs | Provisioning |
| ☐ | Windows Update | Build network reaches Windows Update or WSUS. WSUS has approved Windows Server 2025 updates (security, critical, update rollups). An SSL WSUS certificate is trusted by the guest | Inside Audit Mode |
| ☐ | Windows Update | Updates are never skipped for a production build. The RDS role install needs a complete component store | Inside Audit Mode |
| ☐ | Group Policy | RD licensing mode and Remote Desktop Users membership come from a domain GPO. The image itself is never domain-joined | Housekeeping & shutdown |
| ☐ | Agents | App Volumes Manager address, port and SSL validation, and the Horizon Agent and DEM feature toggles, point at production values | Cleanup & agents |
| ☐ | vGPU (optional) | NVIDIA host driver installed and host graphics set to Shared Direct. The NVIDIA guest driver is not installed by the pipeline yet | Prerequisites |
| ☐ | Build logs | Build log archive and windows_update_report.log reviewed after the build | Inside Audit Mode |
| ☐ | Horizon | horizon_connection_server, horizon_farm_name and a REST API account with only the rights needed to publish. Remember a push recomposes every machine in the farm | Publish |
| ☐ | Test build | One full build through build.sh, published to a test farm before any production farm is touched | Publish |
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
| Log | Location | What it shows |
|---|---|---|
| Build transcript | Control node: images/rdsh_2025_tpl/logs/<published VM name>.log | Everything build.sh, Packer and Ansible printed for that run, one file per build. Start here |
| Guest build logs | Build VM: C:\ProgramData\Omnissa\Logs | Each installer’s own log, the OSOT logs and windows_update_report.log |
| Collected log archive | Control node: images/rdsh_2025_tpl/build_logs/build_logs_<timestamp>.zip | The 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 |
| Sysprep | Build VM: C:\Windows\System32\Sysprep\Panther\setupact.log and setuperr.log | Why a Generalize pass failed |
Common problems
| Symptom | Likely cause and fix | See |
|---|---|---|
| build.sh stops before Packer starts | Pre-flight found a leftover build VM, a DNS name that doesn’t resolve, or an installer missing on the NFS share. Delete the old VM or fix the path, then run again | How the pieces fit together |
| Build stops right after Packer hands over | The guest’s Secure Boot state doesn’t match vm_secure_boot | Inside Audit Mode |
| Build stops at Verify Audit Mode | The answer file never reached Audit Mode. Check win_image_name against the editions in install.wim | Provisioning |
| RDS role install fails in DISM | Windows Update was skipped, so the component store is incomplete. Rebuild with updates on | Inside Audit Mode |
| Update rounds install nothing (WSUS) | No approved Windows Server 2025 updates in security, critical or update rollups, or the guest doesn’t trust an SSL WSUS certificate | Inside Audit Mode |
| OSOT step fails after an hour | OSOT never wrote its completion marker. Read the OSOT log in the guest log folder | Inside Audit Mode |
| Finalize part 1 looks stuck | sdelete64 is zeroing free disk space. The role waits up to 4 hours before failing | Cleanup & agents |
| Farm push fails with AGENT_CUSTOMIZATION_FAULT | The published VM was cloned with a customization spec. Use the plain clone in clone_for_publish.py | Publish |
Omnissa Documentation:
- Manually creating optimized Windows images for Horizon VMs
- Using Automation to Create Optimized Windows Images for Horizon VMs
- Silent Installation Properties for Horizon Agent for Windows
- Unattended Installation of Dynamic Environment Manager
- Install App Volumes Agent Silently
- Run Windows OS Optimization Tool for Horizon from the Command Line
- Install Horizon Client from the Command Line
Other Sources:
- My GitHub repository: bjosoren/omnissa
- BroadCom: Specify VMware Tools Components for Silent Installations.
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.