Tag Archives: XenApp

PowerShell: Cleanup XenApp Users

cleanup1

Intro

One of the signs of a successful IT team is to have clearly defined environments for development, testing, and production. To actually put this into practice however, requires enough head count and enough resources (real and virtual) to maintain it. Many Citrix shops don’t have these luxuries. In this post, we will look at a script I wrote to help cleanup the users/groups assigned to Published Applications in a XenApp 6.5 farm that was used for development, testing, and production.

Hey, we need to test an app…

This phrase is met with trepidation by many Citrix Admins. Your co-worker has just sent an email with scant details and many assurances that this application test is just a POC and won’t have many users or require many resources. So you (being awesome) get the application installed and working and the POC starts.

First, you add one or two users, then more and more, then a group that holds most of the department that wanted to test this application to being with. Within a month, you’re told the POC was wildly successful and they already purchased the software. Now you have an application with a “Configured Users” section that is out of control

cleanup1
Yes, this is a real production application

So, if you have a number of applications like this how can you clean things up or at least get some feedback on what should change?

Putting It Together

Loops and loops

We save all the active applications to a variable and just keep the application name and assigned accounts. Then we start looping through the accounts for each application. If the account matches the regular expression (see the $accountpattern variable in the full script below) for user accounts, then it is added to the $isUser variable. Otherwise, we add the AD Group to the $isGroup variable (skipping the “Citrix Admins” group as it is already added to every application).

Act on the information

Keep in mind that we are still in the second account foreach loop. Review the $isUser and $isGroup variables. If a user is a member of one of the groups already assigned to the application, make a note in a text file to remove the user from the application. If a user is not a member of any groups, then recommend that a new AD group be created for this application and add those users to it. Otherwise, if there are no groups associated with the application, recommend that one get created. You can step through the if..else logic with some test applications and add actual active directory actions to further automate the script.

At the end you will end up with a text file (two if you use the $addADGroupList switch variable). The xaappfixes_datetimestamp.txt file has a list of users to remove from groups and suggested AD (Active Directory) groups that should be created. The xaappgroups_datetimestamp.txt just lists suggested AD groups that you could run through a AD script or hand over to your AD team.

Here is an example of the xaappfixes file

cleanup2

Here is an example of the xaappgroups file

cleanup3

The Script

You can get the script from GitHub

Thanks for reading,
Alain Assaf

PowerShell: E-mail Server Load and Load Rules XenApp 7.x

drevilps
NOTE: I WROTE THIS SCRIPT FOR A XENAPP 7.6 ENVIRONMENT WITH POWERSHELL 4.0 AND XenApp 7.6 XENAPP CMDLETS.

Intro

Citrix has utilized load balancing in XenApp for years. This feature allows you to set parameters in a load evaluator, apply it to a server, and have Citrix manage how users are assigned to servers. Despite the numerous tools available on the market that report on your XenApp farm utilization, Citrix user load balancing remains the easiest to implement (and cheapest).

Find Server Load in Director
In Director, to find the Server Load Evaluator values, click on the “Filters” button.

director1

Then select Machines -> All Machines.

director2

Now click the Server OS Machines tab.

director3

Here, you will find the “Load Evaluator Index” column. This column displays a percentage from 0% to 100% to show how loaded (or unloaded) your server is.

director4

If you hover over a percentage, a floating window will display the details that make up the index percentage. While this is a more visual way to find this information than in the past (“qfarm /load” anyone), there is no straightforward way to get this detail in a single report for individual servers. There is an Load Evaluator Index trend report, but it summaries information for the whole Delivery Group and not single servers.

director5

Thankfully, with Citrix PowerShell commands, we can regularly report on individual server load (including the load evaluator rules) and e-mail that information.

The Script

<#
.SYNOPSIS
Gathers server load, assigned LE, and active and disconnected sessions and emails a HTLM formatted report.
.DESCRIPTION
Gathers server load, assigned LE, and active and disconnected sessions and emails a HTLM formatted report. It is recommended that this script be run as a Citrix admin. In addition, the Citrix Powershell modules should be installed .PARAMETER DeliveryController Required parameter. Which Citrix Delivery Controller (farm) to publish applicaiton with .EXAMPLE PS C:\PSScript &amp;gt; .\get-ctxLoadAndLE.ps1

 Will use all default values.
 Will query servers in the default Farm and create an HTA file and optionally email the report.
.EXAMPLE
 PS C:\PSScript &amp;gt; .\get-ctxLoadAndLE.ps1 -DeliveryController YOURDDC.DOMAIN.LOCAL 

 Will use YOURDDC.DOMAIN.LOCAL for the delivery controller address.
 Will query servers in the YOURDDC.DOMAIN.LOCAL Farm and create an HTA file and optionally email the report.
.OUTPUTS
 An HTA file is created and used for the report email. The HTA file is saved to the $TEMP environment variable
.NOTES
 NAME: get-ctxLoadAndLE.ps1
 VERSION: 2.00
 CHANGE LOG - Version - When - What - Who
 1.00 - 01/11/2012 -Initial script - Alain Assaf
 1.01 - 01/18/2012 - Changed way I get user sessions because it was timing out - Alain Assaf
 1.02 - 02/20/2012 - Added sendTo variable to add mulitple receipients - Alain Assaf
 1.03 - 03/05/2012 - Added lines to include LE Rules - Alain Assaf
 1.04 - 04/26/2012 - Added Test-Port function from Aaron Wurthmann (aaron (AT) wurthmann (DOT) com) - Alain Assaf
 2.00 - 09/15/2016 - Rewritten for XenApp 7.x. Removed test-port function - Alain Assaf
 AUTHOR: Alain Assaf
 LASTEDIT: September 19, 2016
.LINK
 http://www.linkedin.com/in/alainassaf/
 http://wagthereal.com
 http://powershell.com/cs/blogs/ebook/archive/2008/10/23/chapter-4-arrays-and-hashtables.aspx
 http://technet.microsoft.com/en-us/library/ff730946.aspx
 http://technet.microsoft.com/en-us/library/ff730936.aspx
 http://stackoverflow.com/questions/6588265/jquery-select-first-and-second-td
#>

Param(
 [parameter(Position = 0, Mandatory=$False )]
 [ValidateNotNullOrEmpty()]
 $DeliveryController="YOURDDC.DOMAIN.LOCAL" # Change to hardcode a default value for your Delivery Controller
 )

#Constants
$datetime = get-date -format "MM-dd-yyyy_HH-mm"
$Domain=".domain.local" # Change to match your companies Active Directory Domain
$ScriptRunner = (gci env:username).value
$PSSnapins = ("*citrix*")
$compname = (gci env:COMPUTERNAME).value
$ErrorActionPreference= 'silentlycontinue'

#Assign e-mail(s) to $sendto variable and SMTP server to $SMTPsrv
$sendto = "SOMEONE@YOURDOMAIN.com"
$SMTPsrv = 127.0.0.1

### START FUNCTION: get-mymodule #####################################################
Function Get-MyModule {
 Param([string]$name)
 if(-not(Get-Module -name $name)) {
 if(Get-Module -ListAvailable | Where-Object { $_.name -like $name }) {
 Import-Module -Name $name
 $true
 } #end if module available then import
 else { $false } #module not available
 } # end if not module
 else { $true } #module already loaded
}
### END FUNCTION: get-mymodule #####################################################

### START FUNCTION: get-mysnapin #####################################################
Function Get-MySnapin {
 Param([string]$name)
 if(-not(Get-PSSnapin -name $name)) {
 if(Get-PSSnapin -Registered | Where-Object { $_.name -like $name }) {
 add-PSSnapin -Name $name
 $true
 } #end if module available then import
 else { $false } #snapin not available
 } # end if not snapin
 else { $true } #snapin already loaded
}
### END FUNCTION: get-mysnapin #####################################################

#Initialize array
$finalout = @()

#Import Module(s) and Snapin(s)
foreach ($module in $PSModules) {
 if (!(get-mymodule $module)) {
 write-verbose "$module PowerShell Cmdlet not available."
 write-verbose "Please run this script from a system with the $module PowerShell Cmdlets installed."
 exit
 }
}
foreach ($snapin in $PSSnapins) {
 if (!(get-MySnapin $snapin)) {
 write-verbose "$snapin PowerShell Cmdlet not available."
 write-verbose "Please run this script from a system with the $snapin PowerShell Cmdlets installed."
 exit
 }
}

#Get server list
$ctxservers = Get-BrokerSharedDesktop -AdminAddress $DeliveryController | select -Property DNSName,RegistrationState

#Get user sessions
$ctxsrvSessions = Get-BrokerSession -AdminAddress $DeliveryController | select -Property UserFullName,UserName,HostedMachineName,Protocol,SessionState | where {($_.SessionState -eq 'Active' -or $_.SessionState -eq 'Disconnected')}

#Create a new object array with all the data we need. You will need to change the column names to reflect your evaluator rules.
foreach ($srv in $ctxservers) {
 if ($srv.RegistrationState -eq "Registered") {
 $ctxsrv = $srv.DNSName
 $srvload = Get-BrokerMachine -dnsname $ctxsrv | select -Property dnsname,RegistrationState,loadindex,loadindexes
 $srvactive = @($ctxsrvSessions | where {$_.HostedMachineName -eq $srv -and $_.SessionState -eq 'Active'}).count
 $srvdisconn = @($ctxsrvSessions | where {$_.HostedMachineName -eq $srv -and $_.SessionState -eq 'Disconnected'}).count
 $objctxsrv = new-object System.Object
 $objctxsrv | Add-Member -type NoteProperty -name ServerName -value $srv.DNSName.TrimEnd($Domain)
 $objctxsrv | Add-Member -type NoteProperty -name Status -value "Running"
 $objctxsrv | Add-Member -type NoteProperty -name Load -value $srvload.loadindex.ToString()
 $objctxsrv | Add-Member -type NoteProperty -name Active -value $srvactive.ToString()
 $objctxsrv | Add-Member -type NoteProperty -name Disconnected -value $srvdisconn.ToString()
 $objctxsrv | Add-Member -type NoteProperty -name 'CPU Usage' -value $srvload.LoadIndexes[0].Split(":")[1]
 $objctxsrv | Add-Member -type NoteProperty -name 'Memory Usage' -value $srvload.LoadIndexes[1].Split(":")[1]
 $objctxsrv | Add-Member -type NoteProperty -name 'Session Count' -value $srvload.LoadIndexes[2].Split(":")[1]
 $finalout += $objctxsrv
 } else {
 $objctxsrv = new-object System.Object
 $objctxsrv | Add-Member -type NoteProperty -name ServerName -value $srv.DNSName.TrimEnd($Domain)
 $objctxsrv | Add-Member -type NoteProperty -name Status -value "OFFLINE"
 $objctxsrv | Add-Member -type NoteProperty -name Load -value "0"
 $objctxsrv | Add-Member -type NoteProperty -name Active -value "0"
 $objctxsrv | Add-Member -type NoteProperty -name Disconnected -value "0"
 $objctxsrv | Add-Member -type NoteProperty -name 'CPU Usage' -value "0"
 $objctxsrv | Add-Member -type NoteProperty -name 'Memory Usage' -value "0"
 $objctxsrv | Add-Member -type NoteProperty -name 'Session Count' -value "0"
 $finalout += $objctxsrv
 }
 }

#Create HTML Header
$head = '
&amp;lt;img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cstyle%3E%0AMETA%7Bhttp-equiv%3Arefresh%20content%3A30%3B%7D%0ABODY%7Bfont-family%3AVerdana%3B%7D%0ATABLE%7Bborder-width%3A%201px%3Bborder-style%3A%20solid%3Bborder-color%3A%20black%3Bborder-collapse%3A%20collapse%3B%7D%0ATH%7Bfont-size%3A12px%3B%20border-width%3A%201px%3Bpadding%3A%202px%3Bborder-style%3A%20solid%3Bborder-color%3A%20black%3Bbackground-color%3APaleTurquoise%7D%0ATD%7Bfont-size%3A12px%3B%20border-width%3A%201px%3Bpadding%3A%202px%3Bborder-style%3A%20solid%3Bborder-color%3A%20black%3Bbackground-color%3AGhostWhite%7D%0A%3C%2Fstyle%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="&amp;amp;lt;style&amp;amp;gt;" title="&amp;amp;lt;style&amp;amp;gt;" /&amp;gt;

&amp;lt;img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20type%3D%22text%2Fjavascript%22%20src%3D%22http%3A%2F%2Fajax.googleapis.com%2Fajax%2Flibs%2Fjquery%2F1.3.2%2Fjquery.min.js%22%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="&amp;amp;lt;script&amp;amp;gt;" title="&amp;amp;lt;script&amp;amp;gt;" /&amp;gt;
&amp;lt;img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20type%3D%22text%2Fjavascript%22%3E%0A%24(function()%7B%0Avar%20linhas%20%3D%20%24(%22table%20tr%22)%3B%0A%24(linhas).each(function()%7B%0Avar%20Valor%20%3D%20%24(this).find(%22td%3Anth-child(2)%22).html()%3B%0Aif(Valor%20%3D%3D%20%22OFFLINE%22)%7B%0A%20%24(this).find(%22td%22).css(%22background-color%22%2C%22LightCoral%22)%3B%0A%7Delse%20if(Valor%20%3D%3D%20%22Running%22)%7B%0A%20%24(this).find(%22td%22).css(%22background-color%22%2C%22LightGreen%22)%3B%0A%7D%0A%7D)%3B%0A%7D)%3B%0A%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="&amp;amp;lt;script&amp;amp;gt;" title="&amp;amp;lt;script&amp;amp;gt;" /&amp;gt;
'
#$header = "
&amp;lt;H5&amp;gt;XenApp DASHBOARD&amp;lt;/H5&amp;gt;

"
$title = "XenApp DASHBOARD"
#$body = $finalout | ConvertTo-Html -head $head -body $header -title $title
#$body = $finalout | ConvertTo-Html -head $head -title $title

#Create mail parameters
$messageParameters = @{
 Subject = "Report: Server Load and Assigned LE for XenApp Farm"
 Body = $finalout | ConvertTo-Html -head $head -title $title | out-string
 From = "Admin-Citrixtask@DOMAIN.COM"
 To = $sendto
 SmtpServer = "$SMTPsrv"
}

#Send e-mail with Server load data
Send-MailMessage @messageParameters -BodyAsHtml

#Uncomment to output the results in an HTA file to view in a browser
#$finalout | ConvertTo-Html -head $head -title $title | out-file $env:temp\report.hta
#ii $env:temp\report.hta

Example report

Currently the script records the following:

  • Server Name
  • Status
  • Server Load
  • Active Sessions
  • Disconnected Sessions
  • Load Evaluator Rules (you will have to modify the above script depending on which LE rules are using)
    • CPU Usage
    • Memory Usage
    • Session Count (not a count of sessions, but the LE number that contributes to the load)

director6

Explore more (these articles refer to older versions of XenApp, but they are still relevant)

Load Manager Values Explained

Load Manager Rules Explained

Troubleshooting Load Balancing Issues

Conclusion

As always, I welcome all comments and questions.

Thanks,
Alain

PowerShell: Display Maintenance Message for a XenApp Published Application

wherearemycitrixapps

Intro

For years, management of Citrix Published Applications has been pretty heavy-handed. You had the following options:

  1. You had enough servers to ensure your users always had access to their apps.
  2. You established a regular maintenance period that everyone knew about (I’m trying not to burst out laughing) that allowed you to perform application and server maintenance.
  3. You implemented an application virtualization technology that allowed for incremental testing and roll-out of application changes without affecting all your users.
  4. You used the Citrix console to disable and hide an application (resulting in calls to the help desk like the above picture).

image

In this blog post I’ll present a PowerShell script that allows you to display a maintenance message instead of hiding the application. This allows you to do maintenance and give users some feedback about what’s going on.

Disabling the application

We’re assuming the runner of the script has XenApp farm admin permissions enough to change published application properties and that the XenApp 6.5 SDK is installed.  When the change-appmaintenance.ps1 script runs, it creates a text file with the same name as the published app under C:\_scripts\appmaintenance that stores 3 items of information (this assumes the –disabled flag is used):

  1. The original Command Line field of the published application (used when we need to put the application back in production.
  2. The person who ran the script.
  3. The current date and time (when the script was run).

Here’s a command line example (the –verbose flag is optional, but shows feedback messages):

PS C:\_scripts> .\change-appmaintenance.ps1 -PublishedApp "testdisable" -disable –Verbose

Here is the maintenance file created for the TestDisable published application.

image

Once the information is saved, the script changes the command line to the following:

image

The show-appmaintenancebox.ps1 script is simple, but you can add more information as needed. You could further customize the maintenance message by passing parameters, but you may run into the character limit (256 characters) in the Command Line field. In this case you will get the following message:

image

You could also solve the above issue by calling a batch file to run the script.

$wshell = New-Object -ComObject Wscript.Shell
$wshell.Popup("This application is unavailable because it is undergoing maintenance. Please try again later.",0,"APPLICAITON UNDER MAINTENANCE",0)

NOTE: More info about the Popup Method is here.

When a user launches the application, the script now displays this window.

image

Enabling the Application

To reverse the changes you run the same command without the –Disabled flag (the –verbose flag is optional, but shows feedback messages):

PS C:\_scripts> .\change-appmaintenance.ps1 -PublishedApp "testdisable" –Verbose

The script will check for the published application maintenance file under C:\_scripts\appmaintenance, read the file, and use the first line to change the Command Line back to the correct one for the application. Then it will delete the maintenance file.

The Script

<#
.SYNOPSIS
	Disables (or re-enables) a published application by replacing the original command line with one that
 calls a pop-up window.
.DESCRIPTION
	Disables (or re-enables) a published application by replacing the original command line with one that
 calls a pop-up window. 

	It is recommended that this script be run as an a XenApp Farm administrator.
 This script assumes that the XenApp 6.5 powershell commandets are installed.
.PARAMETER PublishedApp
    Required parameter. Application that will be enabled or disabled.
.PARAMETER Disable
    Conditional parameter.
    If present, the application is disabled and maintenance message will be displayed. If not, then the maintenance message
    is removed and applicaiton is republished.
.EXAMPLE
	PS C:\PSScript > .\change-appmaintenance.ps1 -PublishedApp $PublishedApplicationName

	Script will try and re-enable the application.
 Will not display feedback
.EXAMPLE
	PS C:\PSScript > .\change-appmaintenance.ps1 -PublishedApp $PublishedApplicationName -verbose

	Script will try and re-enable the application.
 Will display feedback
.EXAMPLE
	PS C:\PSScript > .\change-appmaintenance.ps1 -PublishedApp $PublishedApplicationName -Disabled -verbose

	Script will disable the application.
 Will display feedback
.INPUTS
	PublishedApp maintenance file. If the file exists it assumes that the application was already put in maintenance. The script will read in the
    information from the file and take it out of maintenance (assuming the disabled flag is not present). It will delete the existing file if
    taking the application is taken out of maintenance.
.OUTPUTS
	A maintenance file will be generated per application (assuming the disabled switch is present). The file will contain the original location of the application from the published application
    properties, the name of the script runner, and the date the change was made.
.NOTES
	NAME: change-appmaintenance.ps1
	VERSION: 1.00
    CHANGE LOG - Version - When - What - Who
                 1.00 - 08/15/2014 - Inititail script - Alain Assaf
	AUTHOR: Alain Assaf
	LASTEDIT: August 15, 2014
.LINK
 http://www.linkedin.com/in/alainassaf/
  http://wagthereal.com
  http://gallery.technet.microsoft.com/scriptcenter/PowerShell-Message-Box-6c6e4f75
#>

Param(
    [parameter(Position = 0, Mandatory=$true )]
    [ValidateNotNullOrEmpty()]
	[string]$PublishedApp,

    [parameter(Position = 1, Mandatory=$False )]
    [ValidateNotNullOrEmpty()]
    [switch]$Disable)

### FUNCTION: get-mysnapin ####################################################
Function Get-MySnapin {
    Param([string]$name)
    if(-not(Get-PSSnapin -name $name)) {
        if(Get-PSSnapin -Registered | Where-Object { $_.name -eq $name }) {
            add-PSSnapin -Name $name
            $true
        } #end if module available then import
        else { $false } #snapin not available
        } # end if not snapin
    else { $true } #snapin already loaded
}
### FUNCTION: get-mysnapin ####################################################

#Constants
$datetime = get-date -format "MM-dd-yyyy_HH-mm"
$Domain=(Get-WmiObject Win32_ComputerSystem).Domain
$ScriptRunner = (get-aduser $env:username | select name).name
$XAZDC = "XenApp65ZDC.local"
$appmaintfolder = "C:\_scripts\appmaintenance"
$PSSnapins = ("Citrix.XenApp.Commands")
$XApps = New-Object System.Collections.ArrayList

#Populate XApps
foreach ($a in $PublishedApp) {$XApps.Add($a)>$null}

#Import Snapin(s)
foreach ($snapin in $PSSnapins) {
    if (!(get-MySnapin $snapin)) {
        write-verbose "$snapin PowerShell Cmdlet not available."
        write-verbose "Please run this script from a system with the $snapin PowerShell Cmdlets installed."
        exit
    }
}

#Confirm application(s) exists
foreach ($apps in $XApps) {
    $appresult = Get-XAApplication -ComputerName $XAZDC -BrowserName $apps -ErrorAction SilentlyContinue
    if ([boolean]($appresult)) {
        write-verbose "$apps exists"
        if ($Disable) {   #Disable flag is present
            $appmaintfile = $apps + ".txt"
            if (test-path -Path $appmaintfolder\$appmaintfile) {
                write-verbose "$appmaintfile already exists. Please confirm application name. No changes made"
            } else {      #Create maintenance file and replace app's command line
                add-content -Value $appresult.CommandLineExecutable -Path $appmaintfolder\$appmaintfile
                Add-Content -Value $ScriptRunner -Path $appmaintfolder\$appmaintfile
                Add-Content -Value $datetime -Path $appmaintfolder\$appmaintfile
                Set-XAApplication -computername $XAZDC -BrowserName $apps -CommandLineExecutable "powershell.exe -noprofile -executionpolicy bypass -command c:\_scripts\Show-appmaintenancebox.ps1"
            }
        } else {          #Read maintenance file and restore app's command line
            $appmaintfile = $apps + ".txt"
            if (test-path -Path $appmaintfolder\$appmaintfile) {
                $fileinfo = get-content -path $appmaintfolder\$appmaintfile
                Set-XAApplication -computername $XAZDC -BrowserName $apps -CommandLineExecutable $fileinfo[0]
                remove-item -Path .\appmaintenance\$appmaintfile
                Write-Verbose "Application maintenance removed for $apps"
            } else {
                write-verbose "*** ERROR *** expected maintenance file:$appmaintfile is missing. Please confirm application name. Script will exit"
                exit
            }
        }
    } else {
        Write-Verbose "$apps does not exist"
    }
}

 

Script Logic Flow

change-appmaintenance_scriptflow

Future tasks

To use this with your users, I would recommend that you try calling the PowerShell script with VBS. This should allow you to hide the PowerShell window that calls the script. You can read more here: Hiding a Powershell window using VBscript

I hope you found this script useful and that it may help you. I welcome all comments and suggestions.

Thanks,
Alain