Export And Import Citrix XenDesktop Published Apps

Dominik Britz's picture

In one of my last projects I needed an automated way to migrate published apps from a Citrix XenDesktop test site to a production site. There is no way to do so in Citrix Studio. Additionally the customer has many published apps configured with FTAs, categories and so on I was sure I would have missed something if I had gone the manual way.

I headed over to my colleague Clemens Geiler and we talked and scripted. The outcome were two scripts, one to export all published apps to json files and one to import apps with the information of the exported json files. I will explain the scripts in this article but you also can scroll to the end and download them right away. (Or here)

Before you start

·         Citrix is adding features in each new release. The scripts were developed for version 7.8. If you use them with a later version note that new options maybe not get considered. If we have new releases for newer versions, we will update this blog post accordingly.

  • PowerShell version 3 is required. Be sure to update to that version if you are using Windows Server 2008R2.
  • The scripts have to be executed on a Citrix Controller.
  • Run the scripts in an elevated powershell console.
  • If you want to see some logs, run the scripts with –Verbose parameter. 

Export published apps

To export published apps, use the script Export-PublishedApps.ps1. As apps are published to Citrix delivery groups (or desktop group), you have to run the script for each delivery group you want to export apps from. For that reason the script has two mandatory parameters:

  • ExportFolder -> Folder where to save the exported json files. If it does not exist, the script creates it. In this folder the script creates a subfolder for each DesktopGroup.
  • DesktopGroup -> The Citrix Studio Desktop Delivery Group which will be the source for the export

Example: 

# Exports all published apps which are published to the desktop delivery group "Test" to the folder "C:\Export\Test"
.\Export-XenDesktopApplications.ps1 -ExportFolder C:\Export -DesktopGroup Test

Let’s have a look at the script. First you need to get the unique id of the desktop group. You can then get all apps published to that desktop group with Get-BrokerApplication –DesktopGroupUid. You loop through all the apps and build a hashtable with all properties for each app.

$DesktopGroupUid = (Get-BrokerDesktopGroup -Name $DesktopGroup).Uid
Get-BrokerApplication -DesktopGroupUid $DesktopGroupUid | ForEach-Object{
    $CurrentAppHashTable = @{}
    $CurrentApp = $_
    $CurrentApp|Get-Member -MemberType Property|foreach-object{
    $_.name
    }|foreach-object{
        $CurrentAppHashTable.Add($_, $(($CurrentApp).$($_)))
    }

We also want to get the icon for each app. They are saved in a base64 encoded string.

$BrokerIconUid = ($CurrentApp).IconUid
$BrokerEnCodedIconData = (Get-BrokerIcon -Uid $BrokerIconUid).EncodedIconData
$CurrentAppHashTable.Add('EncodedIconData', $BrokerEnCodedIconData) 

The last thing we want to save in our hashtable are the file type associations. FTAs are defined per site. To get the associations with our apps we loop through all configured FTAs and search for the uid of the apps. Because an app can have multiple FTAs, we build a hashtable from the information and add it to our existing hashtable as a value.

Get-BrokerConfiguredFTA | Where-Object {$_.ApplicationUid -eq $CurrentApp.Uid} | ForEach-Object -Process {
        $FTAUid = "FTA-" + "$($_.Uid)"
        $FTA = @{}
        $FTA.Add('ContentType',$_.ContentType)
        $FTA.Add('ExtensionName',$_.ExtensionName)
        $FTA.Add('HandlerDescription',$_.HandlerDescription)
        $FTA.Add('HandlerName',$_.HandlerName)
        $FTA.Add('HandlerOpenArguments',$_.HandlerOpenArguments)
        $CurrentAppHashTable.Add("$FTAUid", $FTA)

That’s it! Last but not least we save the hashtable in a json file.

$AppName = $_.ApplicationName -replace ' ','_'
$Extension = 'json'
$Filename = $Appname + '.' + $Extension
$Content = $CurrentAppHashTable|ConvertTo-Json -Compress #-Compress is needed because of a bug in PowerShell -> http://stackoverflow.com/questions/23552000/convertto-json-throws-error-when-using-a-string-terminating-in-backslash
$Content|Set-Content -Path $(join-path $ExportFolder\$DesktopGroup $Filename) -Force

 

Import published apps

In the next step we are able to publish apps with the information of the exported json files in a new empty site. Importing is way more complex than exporting. Here we go!

As apps are published do desktop groups and we exported them per desktop group, we have to do the same we did when importing. For that reason the script Import-XenDesktopApplications.ps1 has two mandatory parameters as well:

  • ImportFolder -> Folder where the input json files are located
  • DesktopGroup -> The Citrix Studio Desktop Delivery Group which will be the target for the import. If the app already exists at a Site, the script will skip that app.

In the first step we built a list of all attributes we can find in the json files. We need them later on.

$AttributNames = (Convertfrom-Json "$(Get-Content (Get-ChildItem $ImportFolder -Filter *.json).fullname)"|Get-Member -MemberType NoteProperty|Select-Object Name).name

The icon data has to be removed because you can’t configure the icon on app creation. You have to do it later.

(Get-ChildItem $ImportFolder -filter *.json).FullName|ForEach-Object{
    $BrokerApplicationJSONFile = ConvertFrom-Json "$(Get-Content $_)"
    $BrokerApplicationHashTable = @{}
    $BrokerApplicationJSONFile|get-member -MemberType NoteProperty|Where-Object{-not [string]::IsNullOrEmpty($BrokerApplicationJSONFile."$($_.name)")}|ForEach-Object {$BrokerApplicationHashTable.add($_.name,$BrokerApplicationJSONFile."$($_.name)")}
    $AttributNames|ForEach-Object{        
        if($_ -match 'AdminFolderUid' -or $_ -match 'AssociatedDesktopGroupUUIDs' -or $_ -match 'EncodedIcon')
        {
            $BrokerApplicationHashTable.Remove("$_")
        }        
    }     

The name of a published app contains the full path to the app including Citrix Studio folder names. If you have two published apps called Notepad, one in the root and the other one in the folder “Germany”, then name of the first one is “Notepad” , the second one’s is “Germany\Notepad”. The problem now is, that the Citrix cmdlets can’t handle strings with a backslash in them. That is why we safe the full name in the variable $OldAppName for later use. We then check if the app already exists, if so we skip this app.

$OldAppName = $BrokerApplicationHashTable.Name #save the full name with possible admin folders for later use as we have to trim all backspaces because studio does not allow them on app creation
    
    If (Get-BrokerApplication $OldAppName -ErrorAction SilentlyContinue)
    {
        Write-Verbose "App $OldAppName already exists. Skipping..."
    }

The next step is to handle admin folders. Admin folders are the folders you as an admin can see in Citrix Studio. On migrating apps from one site to another, we cannot assume that admin folders present at the source site as well exist at the destination site. Admin folders have to be created before we can publish an app which resides in that admin folder. On top the Citrix cmdlets do not have a –Force switch so that parent admin folders get created automatically. We have to handle that by ourselves. To all abundance you have to call the cmdlet for the admin folder creation with the parameter –ParentFolder in case there is a root folder and without it if there’s none. Well designed… So, these are the conditions we have to take care of:

App doesn’t have an admin folder -> Nothing to do :)

App has one or more admin folder

   -> There is only one admin folder level (e.g. “Germany”)

   -> There are multiple admin folder levels (e.g. “Germany\Cologne”)

      -> Root folder does not exist

      - >Root folder already exists

If ($BrokerApplicationHashTable.AdminFolderName)
        {
            $Folder = $BrokerApplicationHashTable.AdminFolderName
            Write-Verbose "Admin folder $Folder configured"
            If ($Folder.Split('\').Count -eq 1) #only one folder level
            {
                $FolderName = $Folder.Split('\')[0]
                If (Get-BrokerAdminFolder -Name $Folder -ErrorAction SilentlyContinue)
                {
                    Write-Verbose "Admin folder $Folder already exists. Skipping..."
                }
                Else
                {
                    Write-Verbose "Create admin folder $FolderName" 
                    $null = New-BrokerAdminFolder -FolderName $FolderName
                }
            }
            Else #multiple folder levels
            {
                $max = $Folder.Split('\').Count - 1
                for ($i = 0; $i -le $max; $i++)
                {
                    If ($i -eq 0) #root folder does not exist
                    {
                        $FolderName = $Folder.Split('\')[0]
                        If (Get-BrokerAdminFolder -Name $Folder -ErrorAction SilentlyContinue)
                        {
                            Write-Verbose "Admin folder $Folder already exists. Skipping..."
                        }
                        Else
                        {
                            Write-Verbose "Create admin folder $FolderName" 
                            $null = New-BrokerAdminFolder -FolderName $FolderName
                        }
                    }
                    Else #root folder already exists
                    {
                        $FolderName = $Folder.Split('\')[$i]
                        $ParentFolder = ($Folder.SubString(0, $Folder.LastIndexOf("$FolderName"))).Trim('\')
                        If (Get-BrokerAdminFolder -Name $Folder -ErrorAction SilentlyContinue)
                        {
                            Write-Verbose "Admin folder $Folder already exists. Skipping..."
                        }
                        Else
                        {
                            Write-Verbose "Create admin folder $FolderName in parent folder $ParentFolder"
                            $null = New-BrokerAdminFolder -FolderName $FolderName -ParentFolder $ParentFolder
                        }
                    }
                }
            }
        } 

Admin folders, check! Off to the published app itself. One could assume that you can use something easy like splatting to create a published app:

$NewBrokerApplication = @{
    'ApplicationTyp' = 'HostedOnDesktop';
    'Name' = $($BrokerApplicationHashTable.Name);
    'BrowserName' = $($BrokerApplicationHashTable.BroswerName);
    'CommandLineExecutable' = $($BrokerApplicationHashTable.CommandLineExecutable)
    # and so on
}

New-BrokerApplication @NewBrokerApplication

You can’t. The Citrix cmdlets can’t handle empty values. Silly Citrix cmdlets! Instead we have to build a string without the parameters that equal $null and pass that over to Invoke-Expression.

$BrokerApplicationHashTable.Name = $BrokerApplicationHashTable.Name.Split('\')[-1]
        $BrokerApplicationHashTable.CommandLineArguments = $BrokerApplicationHashTable.CommandLineArguments -replace '"%\*"','' -replace "`"","'"
    
        $MakeApp = 'New-BrokerApplication -ApplicationType HostedOnDesktop'
        if($BrokerApplicationHashTable.Name -ne $null){$MakeApp += " -Name `"$($BrokerApplicationHashTable.Name)`""}
        if($BrokerApplicationHashTable.BrowserName -ne $null){$MakeApp += " -BrowserName `"$($BrokerApplicationHashTable.BrowserName)`""}
        if($BrokerApplicationHashTable.CommandLineExecutable -ne $null){$MakeApp += " -CommandLineExecutable `"$($BrokerApplicationHashTable.CommandLineExecutable)`""}
        if($BrokerApplicationHashTable.Description -ne $null){$MakeApp += " -Description `"$($BrokerApplicationHashTable.Description)`""}
        if($BrokerApplicationHashTable.ClientFolder -ne $null){$MakeApp += " -ClientFolder `"$($BrokerApplicationHashTable.ClientFolder)`""}
        if($BrokerApplicationHashTable.CommandLineArguments -ne ""){$MakeApp += " -CommandLineArguments `"$($BrokerApplicationHashTable.CommandLineArguments)`""}        
        if($BrokerApplicationHashTable.Enabled -ne $null){$MakeApp += " -Enabled `$$($BrokerApplicationHashTable.Enabled)"}    
        if($BrokerApplicationHashTable.WorkingDirectory -ne $null){$MakeApp += " -WorkingDirectory `"$($BrokerApplicationHashTable.WorkingDirectory)`""}
        if($BrokerApplicationHashTable.PublishedName -ne $null){$MakeApp += " -PublishedName `"$($BrokerApplicationHashTable.PublishedName)`""}
        if($BrokerApplicationHashTable.AdminFolderName -ne $null){$MakeApp += " -AdminFolder `"$($BrokerApplicationHashTable.AdminFolderName)`""}
        if($BrokerApplicationHashTable.WaitForPrinterCreation -ne $null){$MakeApp += " -WaitForPrinterCreation `$$($BrokerApplicationHashTable.WaitForPrinterCreation)"}
        if($BrokerApplicationHashTable.StartMenuFolder -ne $null){$MakeApp += " -StartMenuFolder `"$($BrokerApplicationHashTable.StartMenuFolder)`""}        
        if($BrokerApplicationHashTable.ShortcutAddedToStartMenu -ne $null){$MakeApp += " -ShortcutAddedToStartMenu `$$($BrokerApplicationHashTable.ShortcutAddedToStartMenu)"}
        if($BrokerApplicationHashTable.ShortcutAddedToDesktop -ne $null){$MakeApp += " -ShortcutAddedToDesktop `$$($BrokerApplicationHashTable.ShortcutAddedToDesktop)"}
        if($BrokerApplicationHashTable.Visible -ne $null){$MakeApp += " -Visible `$$($BrokerApplicationHashTable.Visible)"}
        if($BrokerApplicationHashTable.SecureCmdLineArgumentsEnabled -ne $null){$MakeApp += " -SecureCmdLineArgumentsEnabled `$$($BrokerApplicationHashTable.SecureCmdLineArgumentsEnabled)"}        
        if($BrokerApplicationHashTable.MaxTotalInstances -ne $null){$MakeApp += " -MaxTotalInstances $($BrokerApplicationHashTable.MaxTotalInstances)"}        
        if($BrokerApplicationHashTable.CpuPriorityLevel -ne $null){$MakeApp += " -CpuPriorityLevel $($BrokerApplicationHashTable.CpuPriorityLevel)"}
        if($BrokerApplicationHashTable.Tags -ne $null){$MakeApp += " -Tags `"$($BrokerApplicationHashTable.Tags)`""}
        if($BrokerApplicationHashTable.MaxPerUserInstances -ne $null){$MakeApp += " -MaxPerUserInstances $($BrokerApplicationHashTable.MaxPerUserInstances)"}
        
        $MakeApp += " -DesktopGroup `"$DesktopGroup`""
            
        Write-Verbose "Create app $($BrokerApplicationHashTable.Name) with command $MakeApp"
        Invoke-Expression $MakeApp | Out-Null #we cant use splatting as the cmdlet New-BrokerApplication can not deal with empty values

As stated earlier the icon can’t be configured on app publishing. Therefore, we do it in the next step:

$EncodedIcon = New-BrokerIcon -EncodedIconData "$($BrokerApplicationJSONFile.EncodedIconData)"
Set-BrokerApplication -Name "$OldAppName"  -IconUid $EncodedIcon.Uid

Next: Add users to the published app.

If ($BrokerApplicationHashTable.AssociatedUserNames -ne $null)
        {
            Set-BrokerApplication -Name "$OldAppName" -UserFilterEnabled $true          
            $users = $BrokerApplicationHashTable.AssociatedUserNames 
            foreach($user in $users)
            {
                Write-Verbose "Add user $user to app $OldAppName"
                Add-BrokerUser -Name $user -Application "$OldAppName"
            }            
        }

We’re nearly there. The last step is to configure FTAs. Like with app publishing PowerShell splatting is not supported and we have to go with our string and Invoke-Expression method.

Write-Verbose "Check if app $OldAppName has FTAs configured"
        $AttributNames | where-object {$_ -match 'FTA-'} | ForEach-Object {
            $OldFTA = $BrokerApplicationHashTable.$_
            
            $MakeFTA = "New-BrokerConfiguredFTA -ApplicationUid $((Get-brokerapplication -Name $OldAppName).Uid)"
            if($OldFTA.ExtensionName -ne $null){$MakeFTA += " -ExtensionName `"$($OldFTA.ExtensionName)`""}
            if($OldFTA.ContentType -ne $null){$MakeFTA += " -ContentType `"$($OldFTA.ContentType)`""}
            if($OldFTA.HandlerOpenArguments -ne $null){$MakeFTA += " -HandlerOpenArguments `"$($OldFTA.HandlerOpenArguments)`""}
            if($OldFTA.HandlerDescription -ne $null){$MakeFTA += " -HandlerDescription `"$($OldFTA.HandlerDescription)`""}
            if($OldFTA.HandlerName -ne $null){$MakeFTA += " -HandlerName `"$($OldFTA.HandlerName)`""}
            
            Write-Verbose "Create FTA $($OldFTA.ExtensionName) for app $OldAppName with command $MakeFTA"
            Invoke-Expression $MakeFTA | Out-Null #we cant use splatting as the cmdlet New-BrokerConfiguredFTA can not deal with empty values    
        }

Done!

Thanks to Clemens Geiler for his help. Thanks to one of my favorite customers for letting me test and implement this in their environment.

Feel free to use the scripts or take the parts you need. Return here every now and then in case  Citrix released new versions of XenDesktop.

Regards,

Dominik

Comments
excellent article . Thanks
excellent article . Thanks for sharing
Would be great....
If i can find the script download!
Dominik Britz's picture
Thanks
Hi Terry. Thanks for your comment. There seems to be something wrong with our CMS. Please find the download here until we fixed the article: https://www.sepago.de/files/drupal/export_import_xendesktopapplications.zip I also added a direct link at the beginning.
max 250 records
Hi, great script, I'm new to powershell, how do I increase the maxrecordcount?
Modify the Export script and
Modify the Export script and add -MaxRecordCount 9999 to the Get-BrokerApplication cmdlet. Joe
try this: after each Get
try this: after each Get-BrokerApplication add a -MaxRecordCount 1000
Thanks for these scripts
Just to says thanks. It worked like a charm for me.
Add new comment
By submitting this form, you accept the Mollom privacy policy.