Automating the Citrix App Layer

If your daily job has anything to do with virtual workspaces, you definitely have heard something about app layering. App layering is seen as the replacement for the ‘old’ application virtualization solutions like Microsoft App-V and VMware ThinApp.
While app layering is a good solution for most of the usual issues that come with running many applications on the same machines, it still takes a lot of time to create all those layers.
So together with automation-aficionado Chris Twiest we asked ourselves the question:
Is it possible to automate app layer creation?

This blog will focus on the automation of creating app layers with Citrix App Layering, while Chris Twiest focused on doing the same with VMware App Volumes, the result of which you can find here. This blog is also a precursor to our presentation at the Expert 2 Expert Virtualization Conference in Athens (link).

EDIT Dec 19th 2018: Click here for a YouTube video of the presentation.

App Layering players:
There are currently three main players in the app layering space:
VMware App Volumes (formerly CloudVolumes)
Citrix App Layering (formerly UniDesk)
LiquidWare FlexApp

I am not going to make a detailed comparison of the three. Bas van Kaam already made an excellent blog about it, which definitely is worth a read.

Why are we doing this and how did we get there:
The main goal of this automation-challenge was to see if it actually can be done and not to create a production-ready, fast and, most important of all, vendor-supported solution. This last thing currently is not even possible since neither App Layering, App Volumes nor FlexApp currently offer a SDK of some sort to ‘talk’ to the app layering solution. So we needed to resort to other methods.

Instead of Chris Twiest, who reverse-engineered the App Volumes management page in a very interesting way, I actually had it pretty easy. Ryan Butler already did most of the work for me by creating a PowerShell SDK for Citrix App Layering (link).

The automation process:
So after the usual automation hassle (scripting, testing, bug fixing, rinse and repeat) I ended up with the script below. Although the actual app layer creation was pretty easy, I spent a lot of time installing the application and finalizing the image.
Where the competing products have a dedicated sequencer, Citrix App Layering creates one during app layer creation from the specified OS layer. Since you don’t know the hostname or the IP address of the machine, I used PowerCLI to grab that information from VMware vSphere. If you are using another hypervisor, you would need to edit that part of the script. Luckily, the name of the virtual machine in the hypervisor will have the app layer name in its own display name so you can filter based on that.

When I have the hostname and IP, I can use invoke-command to install the application and run the ‘Shutdown for finalize’ batch file. After that, the script will actually finalize the layer.

The variables and the installation:
Before I will get to the script, I will run through all the variables that you would need to edit to make it work in your environment. I have divided this into two kinds of parameters:
Values specific for your environment (probably only edited during setup)
Values specific for the application layer that you want to create (edited for each app layer)

To get the script running you would need the following things:
– Citrix App Layering appliance
– Citrix App Layering connector to your hypervisor
– OS layer of Windows 7, 8.x or 10 with the firewall disabled and PS remoting enabled (Enable-PSRemoting –Force)
– Management server with PowerCLI installed (assuming you are using vSphere)
– Ryan Butler’s Citrix App Layering PowerShell SDK somewhere on a file-share (optional, it will try to download it first)
– Application installation file somewhere on a file-share (single file installations only currently!)
– Application silent installation parameters

Copy the script to your management server and run through the following variables:

Static variables:
Hostname/IP/FQDN of your VMware vCenter server.
Hostname/IP/FQDN of your Citrix App Layering appliance.
Username of an App Layering administrator.
Password of the same App Layering administrator.
Location of the (downloaded) Citrix App Layering PowerShell SDK.
Username of an administrator in the OS layer/sequencer.
Password of the OS layer administrator.
Name of the folder that will be used as temporary location for the application installer.
Drive letter that will be used to connect to the local disk of the sequencer.
Name of the hypervisor connector in Citrix App Layering.
Name of the OS layer that you would like to use.

App layer variables:
Name of the app layer that you would like to create.
Description of the application that you are installing.
Location of application installer file.
Name of application installer file. (MSI or EXE, if MSI then misexec.exe /I command will automatically be added)
Silent installation parameters for the application.

After filling out all these variables, you are ready to run the script. Now keep in mind that Citrix App Layering is not the fastest solution in the world, so the script will run for a while. I am also using a lot of ‘Start-Sleep’-commands that I don’t really like and might break the script when your environment is too slow.
When I find the time, I will look into refining this by waiting for specific results. But for now: feel free to improve it where needed. 😉

The actual script:

# SCRIPT INFO -------------------
# --- Create App Layer on Citrix App Layering and VMware vSphere ---
# By Chris Jeucken
# v0.9
# -------------------------------
# Run on management server with PowerCLI installed

# VARIABLES ---------------------
# Environment specific
    $VCServer = "vcenter01.local.lan" # <<-- Insert hostname/IP/FQDN of your VMware vCenter server
    $ALAppliance = "ctxal01.local.lan" # <<-- Insert hostname/IP/FQDN of your Citrix App Layering appliance
    $ALUsername = "administrator" # <<-- Insert username of an App Layering administrator
    $ALPassword = "SuperSecurePassword" # <<-- Insert password of the same App Layering administrator
    $ALSDKLocation = "\\fileserver01.local.lan\Share$\Citrix\AppLayering BETA-SDK\ctxal-sdk\" # <<-- Insert location of the downloaded SDK files
    $BuildVMUsername = "UberAdministrator" # <<-- Insert username of an administrator in the OS layer/sequencer
    $BuildVMPassword = "MegaSecurePassword" # <<-- Insert password of the same administrator
    $LocalTempFolderName = "Temp" # <<-- Name of the folder that will be used as temporary location for the application installer
    $PSDriveLetter = "Y" # <<-- Drive letter that will be used to connect to the local disk of the sequencer

# App layering information
    $ALConnectorName = "ALvCenterConnector" # <<-- Name of the hypervisor connector in Citrix App Layering
    $ALOSLayerName = "Win10-Base" # <<-- Name of the OS layer that you would like to use

# App layering information    
    $ALAppLayerName = "NotepadPlusPlus" # <<-- Name of the app layer that you would like to create
    $ALAppLayerDescription = "Text editor" # <<-- Description of the application that you are installing

# Application information
    $AppInstallerLocation = "\\fileserver01.local.lan\Share$\Notepad++" # <<-- Location of application installer file
    $AppInstallerFile = "npp.7.5.8.Installer.exe" # <<-- Name of application installer file
    $AppInstallParameters = "/S" # <<-- Silent installation parameters for the application

# Default variables
    $ALSDKModuleName = "ctxal-sdk" # <<-- Don't need to change this
    $ALSDKModule = "ctxal-sdk.psm1" # <<-- Don't need to change this
# Combine variables
    $AppInstaller = $AppInstallerLocation + "\" + $AppInstallerFile
    $ALSDK = $ALSDKLocation + $ALSDKModule

# Create Virtual Center credentials
   # $VCCredentials = New-Object System.Management.Automation.PSCredential ($VCUsername, $VCPassword)

# Create App Layering credentials
    $ALPasswordSecure = ConvertTo-SecureString $ALPassword -AsPlainText -Force
    $ALCredentials = New-Object System.Management.Automation.PSCredential ($ALUsername, $ALPasswordSecure)

# Define error action preference
    $ErrorActionPreference = "Stop"

# Execution policy
    Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
# -------------------------------

# FUNCTIONS ---------------------
    Function Get-PendingReboot {
        # Reset variables
        $PendingCBSReboot = $False
        $PendingWUAUReboot = $False
        $PendingDomainJoin = $False
        $PendingMachineRename = $False
        $PendingFileRename = $False

        # Making registry connection to the local computer
	    $HKLM = [UInt32] "0x80000002"
	    $WMI_Reg = [WMIClass] "\\localhost\root\default:StdRegProv"

        # Get build information
        $WMI_OS = Get-WmiObject -Class Win32_OperatingSystem -Property BuildNumber, CSName -ComputerName "localhost" -ErrorAction Stop

        # Query Component Based Services registry key
	    If ([Int32]$WMI_OS.BuildNumber -ge 6001) {
		    $RegSubKeysCBS = $WMI_Reg.EnumKey($HKLM,"SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\")
		    $PendingCBSReboot = $RegSubKeysCBS.sNames -contains "RebootPending"

        # Query Windows Update for pending reboot
	    $PendingWUAURebootReg = $WMI_Reg.EnumKey($HKLM,"SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\")
	    $PendingWUAUReboot = $PendingWUAURebootReg.sNames -contains "RebootRequired"

        # Query PendingFileRenameOperations registry key
	    $RegSubKeySM = $WMI_Reg.GetMultiStringValue($HKLM,"SYSTEM\CurrentControlSet\Control\Session Manager\","PendingFileRenameOperations")
	    $RegValuePFRO = $RegSubKeySM.sValue
	    If ($RegValuePFRO) {
		    $PendingFileRename = $True

        # Query JoinDomain registry key
	    $Netlogon = $WMI_Reg.EnumKey($HKLM,"SYSTEM\CurrentControlSet\Services\Netlogon").sNames
	    $PendingDomainJoin = ($Netlogon -contains 'JoinDomain') -or ($Netlogon -contains 'AvoidSpnSet')

        # Query ComputerName and ActiveComputerName registry keys and compare
	    $ActCompNm = $WMI_Reg.GetStringValue($HKLM,"SYSTEM\CurrentControlSet\Control\ComputerName\ActiveComputerName\","ComputerName")
	    $CompNm = $WMI_Reg.GetStringValue($HKLM,"SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName\","ComputerName")

	    If (($ActCompNm -ne $CompNm) -or $PendingDomainJoin) {
	        $PendingMachineRename = $true

        # Convert to a single true or false and return results
        $RebootPending = ($PendingCBSReboot -or $PendingWUAUReboot -or $PendingDomainJoin -or $PendingMachineRename -or $PendingFileRename)

        $Results = "Reboot needed: $RebootPending"
        Return $Results
# -------------------------------

# MODULES -----------------------
    Write-Host "Importing PowerShell modules" -ForegroundColor Yellow
# Importing VMware PowerCLI Snap-ins
    $SnapinList = @( "VMware.VimAutomation.Core", "VMware.VimAutomation.License", "VMware.DeployAutomation", "VMware.ImageBuilder", "VMware.VimAutomation.Cloud")
    $VMwareLoaded = Get-PSSnapin -Name $SnapinList -ErrorAction SilentlyContinue | ForEach-Object {$_.Name}
    $Registered = Get-PSSnapin -Name $SnapinList -Registered -ErrorAction SilentlyContinue  | ForEach-Object {$_.Name}

    Foreach ($Snapin in $Registered) {
        If ($VMwareLoaded -notcontains $Snapin) {
            Add-PSSnapin $Snapin

# Import Citrix App Layering SDK (BETA)
    If ($PSRepository = Find-Module -Name $ALSDKModuleName -ErrorAction "SilentlyContinue") {
        Set-PSRepository -Name $PSRepository.Repository -InstallationPolicy Trusted
        Install-Module -Name $ALSDKModuleName -Scope CurrentUser
    } Else {
        Write-Host PowerShell module $ALSDKModuleName not found. Installing manually.
        Get-ChildItem -Path $ALSDKLocation -Recurse | Unblock-File
        Import-Module $ALSDK -Force
# -------------------------------

# CONNECTIONS -------------------
    Write-Host "Setup connections with Citrix App Layering appliance and hypervisor" -ForegroundColor Yellow

# Initial connect to app layering appliance
    $ALWebSession = Connect-ALsession -aplip $ALAppliance -Credential $ALCredentials -Verbose

# Connect to Virtual Center Server
    Set-PowerCLIConfiguration -InvalidCertificateAction "Ignore" -DisplayDeprecationWarnings:$False -Confirm:$False | Out-Null
    If (!(Connect-VIServer -Server $VCServer -Force)) {
        Disconnect-VIServer -Force -Confirm:$False
        Connect-VIServer -Server $VCServer -Force
# -------------------------------

# SCRIPT ------------------------
    Write-Host "Retrieve app layering information and create app layer" -ForegroundColor Yellow
# Get Connector
    $ALConnector = Get-ALconnector -websession $ALWebSession -type Create | Where-Object {$ -eq $ALConnectorName}

# Get Fileshare
    $ALFileshare = Get-ALRemoteshare -websession $ALWebSession

# Get OS layer
    $ALOSLayer = Get-ALOsLayer -websession $ALWebSession | Where-Object {$_.Name -eq $ALOSLayerName}

# Get ID of latest OS layer revision
    $ALOSLayerRevisions = Get-ALOsLayerDetail -websession $ALWebSession -id $ALOSLayer.Id
    $ALOSLayerLatestRevision = $ALOSLayerRevisions.Revisions.OsLayerRevisionDetail | Where-Object {$_.State -eq "Deployable"} | Sort-Object Revision -Descending | Select-Object -First 1

# Create new app layer
    New-ALAppLayer -websession $ALWebSession `
    -version "1.0" `
    -name $ALAppLayerName `
    -description $ALAppLayerDescription `
    -connectorid $ALConnector.Id `
    -osrevid $ALOSLayerLatestRevision.Id `
    -diskformat $ALConnector.ValidDiskFormats.DiskFormat `
    -OsLayerSwitching BoundToOsLayer `
    -fileshareid $ALFileshare.Id `

# Wait for app layer creation
    Start-Sleep -Seconds 180

# Get App Layering build VM
    While (!(Get-VM | Where-Object {$ -like "$ALAppLayerName*"})) {
        Write-Host "Build VM not created yet" -ForegroundColor Cyan
        Start-Sleep -Seconds 60
    Write-Host "Build VM created, wait for boot" -ForegroundColor Yellow
    Start-Sleep -Seconds 180
    Write-Host "Get build VM information and setup install process" -ForegroundColor Yellow
    $ALBuildVM = Get-VM | Where-Object {$ -like "$ALAppLayerName*"} | Select-Object Name, @{N="IP";E={@($_.Guest.IPAddress[0])}}, @{N="HostName";E={@($_.guest.HostName)}}

# Create App Layering build VM credentials
    $BuildVMUsernameComplete = $ALBuildVM.HostName + "\" + $BuildVMUsername
    $BuildVMPasswordSecure = ConvertTo-SecureString $BuildVMPassword -AsPlainText -Force
    $BuildVMCredentials = New-Object System.Management.Automation.PSCredential ($BuildVMUsernameComplete, $BuildVMPasswordSecure)

# Set all installation variables
    Set-Item WSMan:\localhost\Client\TrustedHosts * -Force
    $ALBuildVMIPUNC = "\\" + $ALBuildVM.IP + "\" + "c$"
    $ALBuildVMPSDriveTemp = $PSDriveLetter + ":\" + $LocalTempFolderName
    $ALBuildVMLocalTemp = "C:\" + $LocalTempFolderName
    $ALBuildVMLocalInstaller = $ALBuildVMLocalTemp + "\" + $AppInstallerFile

    If ($AppInstallerFile -like "*.msi") {
        $InstallArgumentList = "/q /i $ALBuildVMLocalInstaller /l*v $ALBuildVMLocalTemp\msi.log"
        $InstallScriptBlock = {
            Param ($InstallArgumentList)
            Start-Process C:\Windows\System32\msiexec.exe -ArgumentList $InstallArgumentList -Wait -NoNewWindow
    } Else {
        $InstallArgumentList = $AppInstallParameters
        $InstallScriptBlock = {
            Param ($ALBuildVMLocalInstaller,$InstallArgumentList)
            Start-Process $ALBuildVMLocalInstaller -ArgumentList $InstallArgumentList -Wait -NoNewWindow

# Copy software to build VM
    Write-Host "Copy software to build VM" -ForegroundColor Yellow    
    New-PSDrive -Name $PSDriveLetter -PSProvider FileSystem -Root $ALBuildVMIPUNC -Persist -Credential $BuildVMCredentials
    If (!(Test-Path $ALBuildVMPSDriveTemp)) {
        New-Item -Path $ALBuildVMPSDriveTemp -ItemType "Directory" -Force
    Copy-Item -Path $AppInstaller -Destination $ALBuildVMPSDriveTemp
    Remove-PSDrive -Name $PSDriveLetter -PSProvider FileSystem -Force
    Start-Sleep -Seconds 30

# Install software and wait for install
    Write-Host "Perform software installation" -ForegroundColor Yellow
    Invoke-Command -ComputerName $ALBuildVM.IP -Credential $BuildVMCredentials -ScriptBlock $InstallScriptBlock -ArgumentList $ALBuildVMLocalInstaller,$InstallArgumentList -Verbose
    Start-Sleep -Seconds 120

# Check for pending reboot, perform if needed and wait
    Write-Host "Check for pending reboot" -ForegroundColor Yellow
    $PendingRebootNeeded = Invoke-Command -ComputerName $ALBuildVM.IP -Credential $BuildVMCredentials -ScriptBlock ${Function:Get-PendingReboot}
    If ($PendingRebootNeeded -like "*True") {
        Write-Host "Pending reboot detected, rebooting machine"
        Restart-Computer -ComputerName $ALBuildVM.IP -Credential $BuildVMCredentials -Force
        Start-Sleep -Seconds 120
    } Else {
        Write-Host "No pending reboot detected, moving on."
        Start-Sleep -Seconds 10

# Configure auto login
    $AddAutoAdminLogon = {
        $RegistryPath = "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\winlogon"
        $DefaultUsername = $BuildVMUsername
        $DefaultPassword = $BuildVMPassword
        New-ItemProperty -Path Registry::$RegistryPath -Name AutoAdminLogon -Value 1 -PropertyType String -Force | Out-Null
        New-ItemProperty -Path Registry::$RegistryPath -Name DefaultUserName -Value $DefaultUsername -PropertyType String -Force | Out-Null
        New-ItemProperty -Path Registry::$RegistryPath -Name DefaultPassword -Value $DefaultPassword -PropertyType String -Force | Out-Null
    Invoke-Command -ComputerName $ALBuildVM.IP -Credential $BuildVMCredentials -ScriptBlock $AddAutoAdminLogon -ArgumentList $BuildVMUsername,$BuildVMPassword -Verbose -Debug
    Write-Host "Rebooting computer" -ForegroundColor Green
    Restart-Computer -ComputerName $ALBuildVM.IP -Credential $BuildVMCredentials -Force
    Start-Sleep -Seconds 180

# Finalize layer in build VM
    Write-Host "Finalize layer in build VM" -ForegroundColor Yellow
    $ALFinalizeCommand = {
        $Content = Get-Content -Path "$env:ProgramFiles\Unidesk\Uniservice\ShutdownForFinalize.cmd" 
        $Content[0..($Content.Length-2)] | Set-Content -Path "$env:ProgramFiles\Unidesk\Uniservice\ShutdownForFinalizeEdit.cmd" -Force
        Start-Process "$env:ProgramFiles\Unidesk\Uniservice\ShutdownForFinalizeEdit.cmd" -Wait
    Invoke-Command -ComputerName $ALBuildVM.IP -Credential $BuildVMCredentials -ScriptBlock $ALFinalizeCommand
    Start-Sleep -Seconds 10
    Write-Host "Wait for build VM to turn off" -ForegroundColor Yellow
    While (Test-Connection -ComputerName $ALBuildVM.IP -Count 1 -Quiet) {
        Write-Host "Build VM still running" -ForegroundColor Cyan
        Start-Sleep -Seconds 15
    Write-Host "Build VM turned off" -ForegroundColor Yellow
    Start-Sleep -Seconds 15

# Finalize layer in App Layering
    Write-Host "Get information to finalize app layer" -ForegroundColor Yellow
    $AlAppLayer = Get-ALapplayer -websession $ALWebSession | Where-Object {$_.Name -eq $ALAppLayerName}
    $ALAppLayerRevisions = Get-ALapplayerDetail -websession $ALWebSession -id $ALAppLayer.Id
    $ALAppLayerLatestRevision = $ALAppLayerRevisions.Revisions.AppLayerRevisionDetail | Where-Object {$_.State -eq "Finalizable"} | Sort-Object Revision -Descending | Select-Object -First 1
    $ALAppLayerDiskLocation = Get-ALLayerInstallDisk -websession $ALWebSession -id $ALAppLayerLatestRevision.LayerId
    Start-Sleep -Seconds 10
    Write-Host "Finalize app layer" -ForegroundColor Yellow
    Invoke-ALLayerFinalize -websession $ALWebSession -fileshareid $ALFileshare.Id -LayerRevisionId $ALAppLayerLatestRevision.Id -uncpath $ALAppLayerDiskLocation.DiskUncPath -filename $ALAppLayerDiskLocation.DiskName -Confirm:$False

    Write-Host "Wait for App Layer to finalize" -ForegroundColor Yellow
    While ((Get-ALapplayer -websession $ALWebSession | Where-Object {$_.Name -eq $ALAppLayerName}).StatusFlags -like "ContainerBeingModified*") {
        Write-Host "App Layer not finalized" -ForegroundColor Cyan
        Start-Sleep -Seconds 15
    Write-Host "App Layer finalized" -ForegroundColor Yellow
# -------------------------------

DISCLAIMER: Once again: I’m in no way an expert PowerShell scripter, so it might not be the most efficient code, but it gets the job done. And, of course, feel free to use it/alter it/publish it as your own.


4 thoughts on “Automating the Citrix App Layer

  1. Travis Adams

    Just to point out something. On the Citrix Applayering connection script, if you have any reserved characters that is in your console password, make sure to use proper XML escape chars to get around it. otherwise the connect-alsession will fail.

    1. Travis Adams

      as i saw this i found a handy solution 🙂

      put that on the line before the convert $ALPassword to secure string (line 49) and that should fix the issue.
      i had a password with an & in it… this converts that to a &
      in theory this should work with any other XML escape chars.


Leave a Reply

Your email address will not be published. Required fields are marked *