XenApp 6.5 Application Reporting (Part 4)

Posted: December 19, 2013 in Citrix
Tags: , , , , , , , , , , , ,

Now, in Part 3, I showed my current XML layout, and the initial part of my reporting script.

The main ‘working’ part of the script is pretty straightforward — straightfoward ForEach loop.

#Get the folders from the XML file
$xml.Folders.ChildNodes | Where-Object {$_.AppFolder -ilike "*external*"} | foreach { 
   $Folder = $_

Since we’re building an HTML report, we build a header row for each of the folders.  This spans my 3 columns.  (Column 1 is the application name, Column2 is for the individual assigned users, and Column3 is for the assigned groups, and we also show the users that are assigned by the group.

#Write a header row for the Folder name
 Add-Content -Path $HTMLtemp -Value ("<th colspan='3'>" + $Folder.AppFolder + "</th>`r`n")
#Now write a header row
 Add-Content -Path $HTMLtemp -Value "<tr>`r`n"
 Add-content -path $HTMLTemp -Value "<th width=25%;border=1px><strong>Application Name</strong></td>`r`n"
 Add-content -path $HTMLTemp -Value "<th width=20%;border=1px><strong>Assigned Users</strong></td>`r`n"
 Add-content -path $HTMLTemp -Value "<th width=55%;border=1px><strong>Assigned Group Users</strong></td>`r`n"
 Add-Content -Path $HTMLtemp -Value "</tr>"

(As an important side note.. I could make it shorter using aliases, but a key thing I learned from my Don Jones class — don’t use those aliases unless you are doing interactive work.  When you put it in a script, you want it to be easily legible and understandable by someone else who may have no idea what your script is supposed to do.)

The next section goes through the apps in that folder, and pulls out the app name, the assigned users, and the group name.

foreach ($app in $Apps) {
    $TRString = "<tr>`r`n"
    $TRString = $TRString + "<td>" + ($App.DisplayName) + "<br>" + ($app.FolderPath) + "</td>`r`n"
    $TRString = $TRString + "<td>" + ($App.User -join ",") + "</td>`r`n"
    $TRString = $TRString + "<td>`r`n"

The interesting part of this is the -join statement.  The $app.User shows all the users since they are individual elements (<user>user1</user> etc.). The elements are actually strings, so using the -join turns them into a neatly delimited list, which is ideal for the presentation in the report.

The next thing to do is to process the group users.. Because they are in a subtree, we need a 2nd foreach loop to process them.  Remember, the XML is layed out like this for groups:

<Group GroupName='Name of group'>

So, the loop to process these groups looks like this:

foreach ($group in $app.Group) {
    $TRString = $TRString + "<strong>[" + ($group.GroupName) + "]</strong><br>`r`n"
    $TRString = $TRString + ($group.GroupUser -join ",") + "`r`n"
    $TRString = $TRString + "<br>`r`n"

We simply use the GroupName, since it is an attribute.  With the actual users being a subtree, we enumerate them directly by using the $group.groupuser as opposed to the individual users above ($app.user) and then we enumerate all the group users and again use the -join to create a neatly delimited list.

Now, we just close up the table row.

   $TRString = $TRString + "</td>`r`n"
   $TRString = $TRString + "</tr>`r`n"
   Add-Content -Path $HTMLtemp -Value $TRString
   } #Close foreach ($app in $apps)
} #close foreach( folders.childnodes)

Now, we close up the table and the body of the document.

Add-Content -Path $HTMLtemp -Value "</table>`r`n"
Add-Content -Path $HTMLtemp -Value "</body>`r`n"

Since the entire report was built using the Add-Content, the size of the farm won’t really matter.  If we kept everything in memory, we could potentially run out of ram on a large farm report.  And by using frequent Add-Content commands, it’s easier to tinker with the HTML to get it looking the way we want.

That’s the script.  But, as an appendix, I learned a few other details that are worth noting.  Because I intended this script to be used by my junior admins, they like to do things graphically.  There really isn’t much graphically to show, but I was able to use the Windows dialogs for opening the XML and saving the HTML file.  (Since I wrote out the HTML file to a temp file, then I just copied the temp file to the final location.

File Open:

[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null
$FileOpen = New-Object System.Windows.Forms.OpenFileDialog
$FileOpen.Filter = 'XML Files (*.xml)|*.xml'
$FileOpen.FilterIndex = 2
$FileOpen.Multiselect = $False
$FileOpen.SupportMultiDottedExtensions = $true
$FileOpen.Title = "Select the XML report from Get-DRFXAInformation.ps1"
if ($FileOpen.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
   [xml]$xml = New-Object -TypeName XML
   } else {

Items of note:
Since we are taking advantage of .Net directly, we have use the LoadWithPartialName structure.  Another advantage to using .Net objects was the ability to load other related objects.  (I was able to use the “real” messagebox on another script using a VB object).  The Filter (like most Visual Studio IDE’s) defines the part you actually see, with a pipe symbol ‘|’ and then the actual filter. I *believe* this is what the FilterIndex portion says (using #2), but I have yet to test that.  I was surprised to see the SupportMultiDottedExtensions as a setting.  I use multi-level extensions frequently, so this was a nice addition in .Net.  And of course, I turned off the multi-select.  This script is definitely not designed to handle multiple XML reports.  Finally, by using the $FileOpen.ShowDialog() actually causes the dialog to appear.  As part of my discovery of non-working methods, I had this portion in the script twice, which caused the dialog to appear twice in a row.


$FileSave = New-Object System.Windows.Forms.SaveFileDialog
$FileSave.AddExtension = $true
$FileSave.AutoUpgradeEnabled = $true
$FileSave.CheckPathExists = $true
$FileSave.CreatePrompt = $true
$FileSave.DefaultExt = 'htm'
$FileSave.Filter = 'HTML Files (*.htm)|*.htm'
$FileSave.FilterIndex = 2
#$FileSave.InitialDirectory = [Environment]::GetFolderPath('MyDocuments')
$FileSave.OverwritePrompt = $true
$FileSave.ShowHelp = $false
$FileSave.SupportMultiDottedExtensions = $true
$FileSave.Title = 'Save the External Application Access report...'
$FileSave.ValidateNames = $true
$FileSave.FileName = 'ServerFarm_ExternalApplicationAccess_Report'
$FileSave.RestoreDirectory = $true
if ($FileSave.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
 Copy-Item -Path $HTMLtemp -Destination $FileSave.FileName
 Remove-Item -Path $HTMLtemp
 } else {

Items of note:
SInce we had already loaded the Windows.Forms, we don’t have to load it again.  There are several interesting pieces to this dialog.
.AddExtension – if you save the file and don’t specify the extension, it will add the designated extension (.DefaultExtension) automatically.
.CreatePrompt – prompts you to create the file if it does not exist.
.OverWritePrompt – this is your noclobber type option.  It prevents you from overwriting an existing file without confirmation
.ValidateNames – This was a nice function.. it prevents you from naming the file with invalid characters.  I.e. it makes it difficult to name a file with an embedded & character, since that is very tough to deal with in NTFS.
.RestoreDirectory – this causes it to remember the last file location opened. The commented out .InitalDirectory is mutually exclusive with this option.  (this option lets you specify the directory to open the dialog at).
And the last part of the script deletes the temporary directory if the file save works properly.  (I like to be tidy 🙂

Future Improvement Ideas
I have several ideas for future improvements.  If/When I do these, I’ll post about them.

  1. Parameterize the file open and file save options
  2. Parameterize the output file name for the report
  3. Add a parameter for an optional folder filter.. right now, it’s hard coded to *external*, but this can be made more flexible.
  4. Work on learning & using the ConvertTo-HTML command for the output



Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s