Omnissa Horizon – Windows Server 2025 RDSH Instant Clone Template

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:

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.

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

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:

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:

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.

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 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 exactlyProvisioning
☐Licensingwin_kms_key holds the Windows Server 2025 Datacenter volume-license (GVLK) keyPrerequisites
☐InstallersHorizon 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.ymlPrerequisites
☐Control nodepywinrm and requests_ntlm installed. NTLM is required for WinRMPrerequisites
☐VaultvCenter credentials, the build account password and the Horizon API password are in the vaultPrerequisites, Publish
☐DNSThe build VM’s name resolves in DNS before the build startsHow the pieces fit together
☐Secure Bootvm_secure_boot matches what the farm expects. The build stops if the guest’s Secure Boot state differsProvisioning
☐Windows UpdateBuild 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 guestInside Audit Mode
☐Windows UpdateUpdates are never skipped for a production build. The RDS role install needs a complete component storeInside Audit Mode
☐Group PolicyRD licensing mode and Remote Desktop Users membership come from a domain GPO. The image itself is never domain-joinedHousekeeping & shutdown
☐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 Direct. The NVIDIA guest driver is not installed by the pipeline yetPrerequisites
☐Build logsBuild log archive and windows_update_report.log reviewed after the buildInside Audit Mode
☐Horizonhorizon_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 farmPublish
☐Test buildOne full build through build.sh, published to a test farm before any production farm 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/rdsh_2025_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/rdsh_2025_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 stops before Packer startsPre-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 againHow the pieces fit together
Build stops right after Packer hands overThe guest’s Secure Boot state doesn’t match vm_secure_bootInside Audit Mode
Build stops at Verify Audit ModeThe answer file never reached Audit Mode. Check win_image_name against the editions in install.wimProvisioning
RDS role install fails in DISMWindows Update was skipped, so the component store is incomplete. Rebuild with updates onInside 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 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
Finalize part 1 looks stucksdelete64 is zeroing free disk space. The role waits up to 4 hours before failingCleanup & agents
Farm 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.