Automating the Citrix App Layer

Introduction:
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:
STATIC
Values specific for your environment (probably only edited during setup)
APP LAYER
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:
VCServer
Hostname/IP/FQDN of your VMware vCenter server.
ALAppliance
Hostname/IP/FQDN of your Citrix App Layering appliance.
ALUsername
Username of an App Layering administrator.
ALPassword
Password of the same App Layering administrator.
ALSDKLocation
Location of the (downloaded) Citrix App Layering PowerShell SDK.
BuildVMUsername
Username of an administrator in the OS layer/sequencer.
BuildVMPassword
Password of the OS layer administrator.
LocalTempFolderName
Name of the folder that will be used as temporary location for the application installer.
PSDriveLetter
Drive letter that will be used to connect to the local disk of the sequencer.
ALConnectorName
Name of the hypervisor connector in Citrix App Layering.
ALOSLayerName
Name of the OS layer that you would like to use.

App layer variables:
ALAppLayerName
Name of the app layer that you would like to create.
ALAppLayerDescription
Description of the application that you are installing.
AppInstallerLocation
Location of application installer file.
AppInstallerFile
Name of application installer file. (MSI or EXE, if MSI then misexec.exe /I command will automatically be added)
AppInstallParameters
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 ---------------------
# STATIC
# 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
# /STATIC

# APP LAYER
# 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
# /APP LAYER

# 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 {$_.name -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 `
    -Confirm:$false

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

# Get App Layering build VM
    While (!(Get-VM | Where-Object {$_.name -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 {$_.name -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 = {
        Param($BuildVMUsername,$BuildVMPassword)
        $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.

Loading

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.

    Reply
    1. Travis Adams

      as i saw this i found a handy solution 🙂

      [System.Security.SecurityElement]::Escape($ALPassword)
      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.

      Reply

Leave a Reply

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