Posts Tagged ‘htm’

Here’s the script to build the report from the previous script that gathered the data.

#
# Name: Get-DRFExternalAppAccess.ps1
#
# Author: David Figueroa
#
# Date: 12/2/2013
#
# Purpose: An automated script to enumerate the access to the external applications and produce a usable report describing this data.
#
# Credits:
# I wrote the XML document using this article = http://www.powershellmagazine.com/2013/08/19/mastering-everyday-xml-tasks-in-powershell/
#
#Load the Assembly
[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
$xml.Load($FileOpen.FileName)
} else {
exit
}
#Create a temporary HTML file
$HTMLtemp = $env:TEMP + "\CTX" + (Get-Random -Maximum 99999).ToString() + ".htm"
# if the file exists, delete it, since it is a temp file :-)
if (Test-Path $HTMLTemp) {
Remove-Item -Path $HTMLtemp
}
#Build the HTML header for the report
Add-Content -Path $HTMLtemp -Value "<!DOCTYPE html>`r`n"
Add-Content -Path $HTMLtemp -Value "<html lang='en-US'>`r`n"
Add-content -Path $HTMLtemp -Value "<head>`r`n"
#CSS information
Add-Content -Path $HTMLtemp -Value "<style>s`r`n"
Add-Content -Path $HTMLTemp -Value "body { font-family:Verdana;font-size:10pt;color:blue}`r`n"
Add-Content -Path $HTMLTemp -Value "th {font-family:Verdana;font-size:11pt;color:blue}`r`n"
Add-Content -Path $HTMLTemp -Value "td {font-family:Verdana;font-size:10pt;color:blue}`r`n"
Add-Content -Path $HTMLtemp -Value "p {font-family:Verdana;font-size:14pt;color:blue}`r`n"
Add-Content -Path $HTMLtemp -Value "</style>`r`n"
Add-Content -Path $HTMLTemp -Value "<title>External Application Access Report</title>`r`n"
Add-Content -Path $HTMLtemp -Value "</head>`r`n"
Add-Content -Path $HTMLtemp -Value "<body>`r`n"
Add-Content -Path $HTMLTemp -Value "<p>External Application Access Report</p>`r`n"
Add-Content -Path $HTMLtemp -Value "<table border='1'>`r`n"
#Get the folders from the XML file
$xml.Folders.ChildNodes | Where-Object {$_.AppFolder -ilike "*external*"} | foreach {
$Folder = $_
#Write a header row for the Folder name
Add-Content -Path $HTMLtemp -Value ("<th colspan='3'>" + $Folder.AppFolder + "</th>`r`n")
#Start enumerating the applications
$Apps = $Folder.ChildNodes
#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>"
#Now process each of the apps
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"
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"
}
$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)
Add-Content -Path $HTMLtemp -Value "</table>`r`n"
Add-Content -Path $HTMLtemp -Value "</body>`r`n"
#Load the XML file
$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 {
exit
}

 

I’m still trying to figure out how to get the scripts loaded as attachments (sorry for the delay!)  But, I’m just going to post them as text.

# Name: New-DRFExternalAppAccess.ps1
#
# Author: David Figueroa
#
# Date: 11/21/2013
#
# Purpose: An automated script to enumerate the access to the external applications and produce a usable report describing this data.
#
# Credits:
# I wrote the XML document using this article = http://www.powershellmagazine.com/2013/08/19/mastering-everyday-xml-tasks-in-powershell/
#
# Import Citrix modules</div>
Add-PSSnapin -Name Citrix* -ErrorAction SilentlyContinue
Add-PSSnapin -Name ActiveDirectory -ErrorAction SilentlyContinue
#FUNCTIONS
# --------------------------------------------------------------------------------------------------------
# Create the XML file
# --------------------------------------------------------------------------------------------------------
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null
$FileSave = New-Object System.Windows.Forms.SaveFileDialog
$FileSave.AddExtension = $true
$FileSave.AutoUpgradeEnabled = $true
$FileSave.CheckPathExists = $true
$FileSave.CreatePrompt = $true
$FileSave.DefaultExt = 'xml'
$FileSave.Filter = 'XML Files (*.xml)|*.xml'
$FileSave.FilterIndex = 2
#$FileSave.InitialDirectory = [Environment]::GetFolderPath('MyDocuments')
$FileSave.OverwritePrompt = $true
$FileSave.ShowHelp = $false
$FileSave.SupportMultiDottedExtensions = $true
$FileSave.Title = 'Save the XML Application report...'
$FileSave.ValidateNames = $true
$FileSave.FileName = 'ServerFarm_Application_Report'
$FileSave.RestoreDirectory = $true
if ($FileSave.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
    $XMLPath = $FileSave.FileName
   } else {
     exit
   }
$XMLWriter = New-Object System.XML.XMLTextWriter($XMLPath, $Null)
#Set formatting
$XMLWriter.Formatting = 'Indented'
$XMLWriter.Indentation = 1
$XMLWriter.IndentChar = "`t"
#Write the header
$XMLWriter.WriteStartDocument()
#Set the XSL Statements
$XMLWriter.WriteProcessingInstruction("xml-stylesheet", "type='text/xsl' href='style.xsl'")
#--------------------------------------------------------------------------------------------------
# Enumerate Folders & Apps
#--------------------------------------------------------------------------------------------------
#Start with a Folders node
$XMLWriter.WriteStartElement('Folders') #Start Main 'Folders' Node
$BaseFolder = Get-XAFolder -FolderPath "Applications" -Recurse
$BaseFolder.FolderPath | foreach {
    $AppFolder = $_ 
    #Start a node for each folder
    #Create a "clean" name for the Node
    $AppFolderName = $AppFolder.Replace("/", "_")
    $XMLWriter.WriteStartElement('Folder') #Start individual Folder node
    $XMLWriter.WriteAttributeString("AppFolder",$AppFolder)
    #Enumerate the apps in that folder
    $Apps = Get-XAApplication -FolderPath $AppFolder
    $AppsCount = $Apps.Count
    $i = 1
    foreach ($App in $Apps) {
        Write-Progress -id 1 -Activity "Processing applications in $AppFolder" -Status "Progress: $i of $AppsCount" -PercentComplete ([int](($i/$Apps.Count)*100)) -CurrentOperation ($app.DisplayName)
    #Filter out spaces to write the XML node name
    $NodeName = $App.browsername.Replace(" ", "_")
    $XMLWriter.WriteStartElement('Applications') #Start the individual app node
    $XMLWriter.WriteAttributeString("AppName", $NodeName) 
    $XMLWriter.WriteAttributeString("BrowserName", $app.BrowserName) 
    $XMLWriter.WriteElementString('AccessSessionConditions', $app.AccessSessionConditions)
    $XMLWriter.WriteElementString('AccessSessionConditionsEnabled', $app.AccessSessionConditionsEnabled)
    $XMLWriter.WriteElementString('AddToClientDesktop', $app.AddToClientDesktop)
    $XMLWriter.WriteElementString('AddToClientStartMenu', $app.AddToClientStartMenu)
    $XMLWriter.WriteElementString('AlternateProfiles', $app.AlternateProfiles)
    $XMLWriter.WriteElementString('AnonymousConnectionsAllowed', $app.AnonymousConnectionsAllowed)
    $XMLWriter.WriteElementString('ApplicationId', $app.ApplicationId)
    $XMLWriter.WriteElementString('ApplicationType', $app.ApplicationType)
    $XMLWriter.WriteElementString('AudioRequired', $app.AudioRequired)
    $XMLWriter.WriteElementString('AudioType', $app.AudioType)
    $XMLWriter.WriteElementString('BrowserName', $app.BrowserName)
    $XMLWriter.WriteElementString('CachingOption', $app.CachingOption)
    $XMLWriter.WriteElementString('ClientFolder', $app.ClientFolder)
    $XMLWriter.WriteElementString('ColorDepth', $app.ColorDepth)
    $XMLWriter.WriteElementString('CommandLineExecutable', $app.CommandLineExecutable)
    $XMLWriter.WriteElementString('ConnectionsThroughAccessGatewayAllowed', $app.ConnectionsThroughAccessGatewayAllowed)
    $XMLWriter.WriteElementString('ContentAddress', $app.ContentAddress)
    $XMLWriter.WriteElementString('CpuPriorityLevel', $app.CpuPriorityLevel)
    $XMLWriter.WriteElementString('Description', $app.Description)
    $XMLWriter.WriteElementString('DisplayName', $app.DisplayName)
    $XMLWriter.WriteElementString('Enabled', $app.Enabled)
    $XMLWriter.WriteElementString('EncryptionLevel', $app.EncryptionLevel)
    $XMLWriter.WriteElementString('EncryptionRequired', $app.EncryptionRequired)
    $XMLWriter.WriteElementString('FolderPath', $app.FolderPath)
    $XMLWriter.WriteElementString('HideWhenDisabled', $app.HideWhenDisabled)
    $XMLWriter.WriteElementString('InstanceLimit', $app.InstanceLimit)
    $XMLWriter.WriteElementString('LoadBalancingApplicationCheckEnabled', $app.LoadBalancingApplicationCheckEnabled)
    $XMLWriter.WriteElementString('MachineName', $app.MachineName)
    $XMLWriter.WriteElementString('MaximizedOnStartup', $app.MaximizedOnStartup)
    $XMLWriter.WriteElementString('MultipleInstancesPerUserAllowed', $app.MultipleInstancesPerUserAllowed)
    $XMLWriter.WriteElementString('OfflineAccessAllowed', $app.OfflineAccessAllowed)
    $XMLWriter.WriteElementString('OtherConnectionsAllowed', $app.OtherConnectionsAllowed)
    $XMLWriter.WriteElementString('PreLaunch', $app.PreLaunch)
    $XMLWriter.WriteElementString('ProfileLocation', $app.ProfileLocation)
    $XMLWriter.WriteElementString('ProfileProgramArguments', $app.ProfileProgramArguments)
    $XMLWriter.WriteElementString('ProfileProgramName', $app.ProfileProgramName)
    $XMLWriter.WriteElementString('RunAsLeastPrivilegedUser', $app.RunAsLeastPrivilegedUser)
    $XMLWriter.WriteElementString('SequenceNumber', $app.SequenceNumber)
    $XMLWriter.WriteElementString('SslConnectionEnabled', $app.SslConnectionEnabled)
    $XMLWriter.WriteElementString('StartMenuFolder', $app.StartMenuFolder)
    $XMLWriter.WriteElementString('TitleBarHidden', $app.TitleBarHidden)
    $XMLWriter.WriteElementString('WaitOnPrinterCreation', $app.WaitOnPrinterCreation)
    $XMLWriter.WriteElementString('WindowType', $app.WindowType)
    $XMLWriter.WriteElementString('WorkingDirectory', $app.WorkingDirectory)
    #Now that we have the properties, get the application report properties, and store those
    $AppReportProperties = Get-XAApplicationReport -BrowserName $app.BrowserName
    #Now write the other elements that are relevant
    #ServerNames is a space delimited list, so break them out as individual elements
    foreach ($server in $AppReportProperties.ServerNames) {
        $XMLWriter.WriteElementString('Server', $server)
        }
    #Now get the accounts
    foreach ($account in $AppReportProperties.Accounts) {
        if ($account.AccountType -eq 'Group') {
            #Start a group node
            $XMLWriter.WriteStartElement('Group') #Start Group Element
            $XMLWriter.WriteAttributeString('GroupName', $account.AccountName)
            #Now get the group members and write those out, as a subnode
            $GroupMembers = Get-ADGroupMember -Identity ($account.AccountName)
            foreach ($member in $GroupMembers) {
                $XMLWriter.WriteElementString('GroupUser', $member.name)
            }
            $XMLWriter.WriteEndElement() #End the group node
            } else {
            $XMLWriter.WriteElementString('User', $account.AccountName)
            } #Close If ($account..)
         } #Close ForEach ($account...)
         $xmlWriter.WriteEndElement() #End the App Node 
         #Increment $i for the progress bar
         $i++
     } #Close the ForEach ($app...) 
     $XMLWriter.WriteEndElement() #Close Individual Folder Node?
 } #Close BaseFolder ForEach
 $XMLWriter.WriteEndElement()#Close the Main 'Folders' node
#Close up the XML
$XMLWriter.WriteEndDocument()
$XMLWriter.Flush()$XMLWriter.Close()

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'>
     <GroupUser>User1</GroupUser>
     <GroupUser>User2</GroupUser>
</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
   $xml.Load($FileOpen.FileName)
   } else {
     exit
   }

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.

FileClose:

$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 {
 exit
 }

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

 

The first 2 parts of this series were about getting the data out of XenApp 6.5, and into a usable XML format.  While it felt like a long and arduous process, I didn’t realize at the time, that it was only about 30% of the work.  The major part of the project was ahead – I had to turn this data into a usable report.  I had gone through quite a bit of effort to do all this work with the goal of being able to produce more & differentiated reports from the same set of data without having to redo all the gathering efforts.

The first step was to load the XML file.  Referring back to Dr. Weltner’s post, I created an XML object, and loaded the data.  I have seen many posts about powershell where the poster used a get-content statement to load the XML file.  Dr. Weltner states that this is an issue of performance – that using a get-content construct as opposed to an XML load takes as much as 7x as long on larger XML files.  The data from the XA 6.5 farm was pretty sizeable (~750kb), and this was a pretty small farm.

$FileOpen = "c:\temp\farminfo.xml"
[xml]$xml = New-Object -TypeName XML
$xml.Load($FileOpen.FileName)

(The [xml] statement strongly types the $xml variable.  This means that the variable will always be an XML document, and pre-configures the methods and properties which greatly speeds up the ability to load the document and perform other functions.)

This is a very fast load..I tried a get-content just to try it and it was noticeably slower.  I didn’t do any formal testing, but I could definitely feel the difference.  Now, I piped out my $xml to get-member to see what I was working with. The type is System.Xml.XmlDocument, with an interesting number of properties and methods.  To start tinkering the various properties and methods were pretty easily discovered and brought out.  (Using the ISE {Integrated Scripting Environment} with Intellisense makes this  part substantially easier).

I tried some of the easy things – $xml.applications.  This gave me a list of applications.  I dug a bit deeper down, and I could pull the users pretty easily.  However, I could not pull the group users.  This made little sense..  (During this testing phase, I noticed the spaces in my published app names, and when I tried to access that node, I could not – powershell threw an error.  This is where I discovered I needed the replace statement in part 2.  I reran the script to gather the XA data to start with clean data.  I reloaded the $xml variable, and began digging through it again.  I still had an issue with not being able to extract the group users.  I decided I would come back to this part.

I moved on to creating the HTML portion of what I was looking for.  During this process, I looked at http://www.w3schools.com and taught myself a small amount of inline-CSS.  I won’t go into excessive detail on this section, but the HTML portion looked like this:

#Create a temporary HTML file
$HTMLtemp = $env:TEMP + "\CTX" + (Get-Random -Maximum 99999).ToString() + ".htm"
# if the file exists, delete it, since it is a temp file
if (Test-Path $HTMLTemp) {
 Remove-Item -Path $HTMLtemp
 }
#Build the HTML header for the report
Add-Content -Path $HTMLtemp -Value "<!DOCTYPE html>`r`n" 
Add-Content -Path $HTMLtemp -Value "<html lang='en-US'>`r`n"
Add-content -Path $HTMLtemp -Value "<head>`r`n"
#CSS information
Add-Content -Path $HTMLtemp -Value "<style>s`r`n"
Add-Content -Path $HTMLTemp -Value "body { font-family:Verdana;font-size:10pt;color:blue}`r`n"Add-Content -Path $HTMLTemp -Value "th {font-family:Verdana;font-size:11pt;color:blue}`r`n"
Add-Content -Path $HTMLTemp -Value "td {font-family:Verdana;font-size:10pt;color:blue}`r`n"
Add-Content -Path $HTMLtemp -Value "p {font-family:Verdana;font-size:14pt;color:blue}`r`n"
Add-Content -Path $HTMLtemp -Value "</style>`r`n"
Add-Content -Path $HTMLTemp -Value "<title>External Application Access Report</title>`r`n"
Add-Content -Path $HTMLtemp -Value "</head>`r`n"
Add-Content -Path $HTMLtemp -Value "<body>`r`n"
Add-Content -Path $HTMLTemp -Value "<p>External Application Access Report</p>`r`n"
Add-Content -Path $HTMLtemp -Value "<table border='1'>`r`n"

I created a temporary file that should be a unique name.  It’s not truly guaranteed, which is why i used the test-path to check the existence of the file, and delete it if it does.  The deletion was purely an arbitrary position on my part.  To me, if the file is in the temp directory, it’s temporary, and does not have to be stored.

In powershell terms, the `r`n represents a carriage-return and newline.  This causes each line to be separated by Windows style CRLF’s in the resulting HTML document. WIthout these, the output looks like one long continuous string, and is difficult to read.  By using these characters, we’re able to create a legible document.

I realize many people would use the ConvertTo-HTML command.  However, I am very inexperienced with this command, and I knew enough to write my own base HTML files.  So, I used the add-content commands and the temp file.  Overall, this is a reasonably quick setup, although using ConvertTo-HTML would probably be substantially faster.  I’ll end up upgrading the script to use this eventually, but for now, this was sufficient.

On to creating the body.  I had multiple application nodes, so some sort of foreach loop would take care of this.  Given the fact that I was looking at a table structure for my intended structure, I set up a function to write the table rows.  (I’ll cover more detail about this later). But, parsing the applications proved to be much tougher than expected.  $xml.applications turned out something like this:

Applications
------
{Application, Application, Application, Application...}

So, it was more time for get-member.   Get-Member turned up another XML node.. so it was kind of a dead-end.  I’d already seen XML elements and their methods and properties.  I did some poking around, and I found out about XPath notation and syntax.   I started tinkering around with SelectNode, etc. trying to find the right syntax to get the actual node information I was looking for.  This turned out to be much tougher than expected.  XPath uses case-sensitive notation, which means you need to know exactly how it is capitalized, the correct spelling, etc.  I won’t cover all my successes at finding non-working methodologies 😉

I have my external apps gathered into a folder structure with the word external in the path name. My existing code had grabbed my external application folder, and processed it.  Furthermore, having the node names being the application names did not work out easily, and making the Applications the top level node, made it much more difficult to get the data I wanted.  Lastly, the subfolders of my external applications folder simply would not work, they were ignored.   I went back and revisited how I was generating the XML file.   As I gave it some thought, it occurred to me that it would be easier to pull all of the folders, and then later add a filtering mechanism for reporting.  This would also give me more flexibility in creating new reports with other information that might be requested.  Now, having tinkered with the XML commands, and the XML writer significantly more than when I started, I was able to revisit grabbing the folder information.  I went for a structure more like this:

<Folders>
    <Folder AppFolder="Applications/Folder1">
        <Applications AppName="App1_Application" BrowserName="App1 Application">
            <property>value</property>
            <Group GroupName="Groupname">
                <GroupUser>username</GroupUser>
            </Group>
            <User>username2</user>
         </Applications>
    </Folder>
</Folders>

Ultimately, this turned out to be the best structure.  I’d be able to use where-object clauses to filter by the attributes.  I went ahead and built in a filter to pick up my external applications.   So, I reran my gathering script to create the XML, and started a foreach loop.

#Get the folders from the XML file
$xml.Folders.ChildNodes | foreach {

This worked out extremely well. I was able to pull all of the folders, including all of the external application folder.  I added a quick filter in that same line.  This pulled out the folders I wanted it to.

Where-Object {$_.AppFolder -ilike "*external*"} |

However, the enumeration for the folders really did not work – I could get it to work in the console, but not in the script.  I turned to the powershell.com forums.  I got a partial answer, and that was the piece I needed.

$BaseFolder = Get-XAFolder -FolderPath "Applications" -Recurse
    $BaseFolder.FolderPath | foreach {
        $AppFolder = $_

Using the $AppFolder = $_ worked exactly has hoped.  So, even though it should have worked without it, adding this piece made it work smoothly.

Now, finally… building usable data and a usable report!  

Now that I knew I could get the data I needed, I needed to find a way to automate it and make it more efficient.  Powershell has excellent built-in support for XML, and comes with commands to export and import XML files: Export-CLIXML and Import-CLIXML.

These 2 commands create powershell specific XML files.  They can take any powershell object, and create an XML file that can be reimported with perfect fidelity. So, I did a test export to see what I would come up with.

get-xaapplication -browsername Notepad | export-clixml -path c:\temp\notepad.xml

(Export-CLIXML does not produce an output object.. it creates the final endpoint xml file.  I quickly found it that it put most of my information into the XML file, it also included far more than I wanted or could use.  But, it didn’t include the assigned user information, and while I could combine that with the information from get-xaapplicationreport, it was going to be a lot of work to combine it all, along with a lot off information that was just going to get in the way.  Additionally the user account information was not in a format  I was happy with.  So, I figured it was worth the effort to get the information and store it in my own XML format.

Now, as most admins know, XML on the surface is simply a markup language file similar to HTML, but with stricter rules.  I didn’t know XML, but there were more than enough online references to make it doable.  In passing, I had seen that there was native support for XML files directly (by specifying the file as xml), so I did some research.  I found an article by Dr. Tobias Weltner on powershell.com, and this turned out to be a critical find in this process. http://www.powershellmagazine.com/2013/08/19/mastering-everyday-xml-tasks-in-powershell.  The most important item was the .Net XMLTextWriter.  This object allows you to create clean and well formatted XML files.

Here is the base code for creating an XML file:

#$XMLPath is the path to the intended file path
$XMLWriter = New-Object System.XML.XMLTextWriter($XMLPath, $Null) 
#These commands set the formatting options.
#They are pretty self explanatory. 
#The XMLWriter.IndentChar sets a tab as the indenting character
$XMLWriter.Formatting = 'Indented'
$XMLWriter.Indentation = 1
$XMLWriter.IndentChar = "`t"
#This creates the document in memory
$XMLWriter.WriteStartDocument() 
#Set the XSL Statements
$XMLWriter.WriteProcessingInstruction("xml-stylesheet", "type='text/xsl' href='style.xsl'")

Now a critical piece of information about creating XML with this object, is you must nest your elements properly.  To start an element, the command is $XMLWriter.WriteStartElement(‘MyNode’> This creates an element that looks like this: <MyNode>.  A very important piece of information for this — the name of a node cannot have a space in the name.  To close the node, you use $XMLWriter.WriteEndElement().  This produces </MyNode>. You do not have to specify which node you are closing – it follows the nesting order.

You can assign attributes using $XMLWriter.WriteAttributeString(‘valuename’, valuedata). To write an actual nested element, you use $XMLWriter.WriteElementString(‘ElementName’, ElementData).  Finally, to close our your XML, you use $XMLWriter.WriteEndDocument() to end the document, $XMLWriter.Flush() to purge it from memory, and $XMLWriter.Close() to close the physical document.  One important note about powershell — if you use double-quote marks (” “) to surround a text item, powershell examines everything between the delimiters (“”) looking for variables and expressions to interpret.  If you use single-quote mark (‘ ‘) then powershell does not look for things to do in the string.  The overhead is very tiny using ” ” marks, but with more and more repeated operations, you will start to notice the overhead. 

For a quick test to see how this works, you can try this:

$XMLpath = "c:\temp\xml\testfile.xml"
$xmlwriter = New-Object -TypeName System.XML.XMLTextWriter($xmlpath, $null)
$xmlwriter.Formatting = 'Indented'
$xmlwriter.Indentation = 1
$xmlwriter.IndentChar = "`t"
$xmlwriter.WriteStartDocument()
$xmlwriter.WriteProcessingInstruction("xml-stylesheet", "type='text/xsl' href='style.xsl'")
$xmlwriter.WriteStartElement('Folders')
$xmlwriter.WriteAttributeString('Name', 'TopLevel')
$xmlwriter.WriteStartElement('Applications')
$xmlwriter.WriteAttributeString('Description', 'These are top level folders')
$xmlwriter.WriteElementString('Notepad', 'Notepad.exe')
$xmlwriter.WriteElementString('Calculator', 'Calc.exe')
$xmlwriter.WriteElementString('Wordpad', 'Write.exe')
$xmlwriter.WriteEndElement()
$xmlwriter.WriteEndElement()
$xmlwriter.WriteEndDocument()
$xmlwriter.Flush()
$xmlwriter.Close()

And this produces this XML:

<?xml version="1.0"?>
<?xml-stylesheet type='text/xsl' href='style.xsl'?>
<Folders Name="TopLevel">
 <Applications Description="These are top level folders">
 <Notepad>Notepad.exe</Notepad>
 <Calculator>Calc.exe</Calculator>
 <Wordpad>Write.exe</Wordpad>
 </Applications>
</Folders>

Now on to the task at hand!   I ran $test = get-xaapplication -browsername Notepad.  And then I ran it into get-member to see how it looked.  Virtually everything came across as a property, and had a value.  This meant it was perfect for WriteElementString(Property,Value). Now, I ran a test script with the XMLTextWriter to see what I could get out of it.  I had my starting piece to create the document, and then used this code segment:

$Apps = get-xaapplication -folderpath 'Applications'
 foreach ($App in $Apps) {
    $XMLWriter.WriteStartElement('Applications') 
    $XMLWriter.WriteStartElement("$App.BrowserName")
    $XMLWriter.WriteElementString('AccessSessionConditions', $app.AccessSessionConditions)
    $XMLWriter.WriteElementString('AccessSessionConditionsEnabled', $app.AccessSessionConditionsEnabled)
    $XMLWriter.WriteElementString('AddToClientDesktop', $app.AddToClientDesktop)
    $XMLWriter.WriteElementString('AddToClientStartMenu', $app.AddToClientStartMenu)
    $XMLWriter.WriteElementString('AlternateProfiles', $app.AlternateProfiles)
    $XMLWriter.WriteElementString('AnonymousConnectionsAllowed', $app.AnonymousConnectionsAllowed)
    $XMLWriter.WriteElementString('ApplicationId', $app.ApplicationId)
    $XMLWriter.WriteElementString('ApplicationType', $app.ApplicationType)
    $XMLWriter.WriteElementString('AudioRequired', $app.AudioRequired)
    $XMLWriter.WriteElementString('AudioType', $app.AudioType)
    $XMLWriter.WriteElementString('BrowserName', $app.BrowserName)
    $XMLWriter.WriteElementString('CachingOption', $app.CachingOption)
    $XMLWriter.WriteElementString('ClientFolder', $app.ClientFolder)
    $XMLWriter.WriteElementString('ColorDepth', $app.ColorDepth)
    $XMLWriter.WriteElementString('CommandLineExecutable', $app.CommandLineExecutable)
    $XMLWriter.WriteElementString('ConnectionsThroughAccessGatewayAllowed', $app.ConnectionsThroughAccessGatewayAllowed)
    $XMLWriter.WriteElementString('ContentAddress', $app.ContentAddress)
    $XMLWriter.WriteElementString('CpuPriorityLevel', $app.CpuPriorityLevel)
    $XMLWriter.WriteElementString('Description', $app.Description)
    $XMLWriter.WriteElementString('DisplayName', $app.DisplayName)
    $XMLWriter.WriteElementString('Enabled', $app.Enabled)
    $XMLWriter.WriteElementString('EncryptionLevel', $app.EncryptionLevel)
    $XMLWriter.WriteElementString('EncryptionRequired', $app.EncryptionRequired)
    $XMLWriter.WriteElementString('FolderPath', $app.FolderPath)
    $XMLWriter.WriteElementString('HideWhenDisabled', $app.HideWhenDisabled)
    $XMLWriter.WriteElementString('InstanceLimit', $app.InstanceLimit)
    $XMLWriter.WriteElementString('LoadBalancingApplicationCheckEnabled', $app.LoadBalancingApplicationCheckEnabled)
    $XMLWriter.WriteElementString('MachineName', $app.MachineName)
    $XMLWriter.WriteElementString('MaximizedOnStartup', $app.MaximizedOnStartup)
    $XMLWriter.WriteElementString('MultipleInstancesPerUserAllowed', $app.MultipleInstancesPerUserAllowed)
    $XMLWriter.WriteElementString('OfflineAccessAllowed', $app.OfflineAccessAllowed)
    $XMLWriter.WriteElementString('OtherConnectionsAllowed', $app.OtherConnectionsAllowed)
    $XMLWriter.WriteElementString('PreLaunch', $app.PreLaunch)
    $XMLWriter.WriteElementString('ProfileLocation', $app.ProfileLocation)
    $XMLWriter.WriteElementString('ProfileProgramArguments', $app.ProfileProgramArguments)
    $XMLWriter.WriteElementString('ProfileProgramName', $app.ProfileProgramName)
    $XMLWriter.WriteElementString('RunAsLeastPrivilegedUser', $app.RunAsLeastPrivilegedUser)
    $XMLWriter.WriteElementString('SequenceNumber', $app.SequenceNumber)
    $XMLWriter.WriteElementString('SslConnectionEnabled', $app.SslConnectionEnabled)
    $XMLWriter.WriteElementString('StartMenuFolder', $app.StartMenuFolder)
    $XMLWriter.WriteElementString('TitleBarHidden', $app.TitleBarHidden)
    $XMLWriter.WriteElementString('WaitOnPrinterCreation', $app.WaitOnPrinterCreation)
    $XMLWriter.WriteElementString('WindowType', $app.WindowType)
    $XMLWriter.WriteElementString('WorkingDirectory', $app.WorkingDirectory)
}

This worked great.  (Although, later on for reasons I will explain, it proved to have some serious issues).  Now I knew I was on to something that would be seriously useful.   Next was a considerable amount of testing, retesting and tweaking. I ended up with code that produced XML similar to this sample:

<Applications/External>
     <Notepad>
          <...>
          <FolderPath>Applications/External</FolderPath>
          <...>
          <TitleBarHidden>False</TitleBarHidden>
          <...>
     </Notepad>
     <...>
</Applications/External>

(The <…> represent extra properties & applications). Now, I had a working base model to get the rest of the information I needed. Now, since I was already grabbing the applications, it was easy enough to grab the applicationreport with the properties  I needed.  Inside the foreach loop, I added

$AppReport = get-xaapplicationreport -browsername $app.browsername

I picked up some interesting factoids about the way the Servers were presented.  They came out as a space delimited list.  This really wasn’t suitable for what I wanted, so I added this fragment

foreach ($server in $AppReportProperties.ServerNames) {
     $XMLWriter.WriteElementString('Server', $server)
}

Back to my $test object.  I ran $test.accounts, and it produced something like this:

AccountDisplayName : Domain\Domain Users
AccountName : Domain Users
AccountType : Group 
AccountId : 0X2/NT/SWRCU/S-1-5-21-xxxxxxxxx-xxxxxxxx-xxxxxxxxxx-513
AccountAuthority : NTDomain/Domain
OtherName : Domain Users
SearchPath : CN=Domain Users,OU=Groups,DC=domain,DC=tld
MachineName : ServerName
AccountDisplayName : Domain\David Figueroa
AccountName : David
AccountType : User 
AccountId : 0X2/NT/SWRCU/S-1-5-21-xxxxxxxxx-xxxxxxxx-xxxxxxxxxx-yyyyy
AccountAuthority : NTDomain/Domain
SearchPath : CN=David,OU=Users,DC=domain,DC=tld
MachineName : ServerName

The users turned out to be pretty easy to handle with this fragment

$XMLWriter.WriteElementString('User', $account.AccountName)

But, the groups were just single entities in and of themselves.  I needed the users.  More research, and it turns out that the Citrix object type is just an account identity.. it doesn’t seem to offer a good way to extract the users.  But, it does offer the Account Name, which I would be able to use with the ActiveDirectory module cmdlets.  Fortunately, I had tinkered with these AD cmdlets previously, and I was able to come up with this fragment to handle all the users and groups.

foreach ($account in $AppReportProperties.Accounts) {
 if ($account.AccountType -eq 'Group') {
 #Start a group node
 $XMLWriter.WriteStartElement('Group') #Start Group Element
 $XMLWriter.WriteAttributeString('GroupName', $account.AccountName)

 #Now get the group members and write those out, as a subnode
 $GroupMembers = Get-ADGroupMember -Identity ($account.AccountName)

 foreach ($member in $GroupMembers) {
 $XMLWriter.WriteElementString('GroupUser', $member.name)
 }
 $XMLWriter.WriteEndElement() #End the group node
 } else {
     $XMLWriter.WriteElementString('User', $account.AccountName)
}

Now, I put the entire thing together and ran it.  It produced a good looking XML file.  But, it had a problem.. some of the application names had spaces in them, and since there were being set as Nodes, that was an issue.  But, that was fairly easily resolved with these 2 lines

$NodeName = $App.browsername.Replace(" ", "_")
$XMLWriter.WriteStartElement($NodeName, $App.BrowserName)

I reran it, and we had good looking XML with valid node names.

The next part of this series I’ll cover getting the data back out of the XML file for the report, and some of the corrections I had to make.  I’ll also put up the final version of both the data gathering script, and the report script.

David Figueroa

At one point, my boss had asked me for a list of who had remote access through our Citrix servers.  We have XenDesktop 5.6 & XenApp 6.5, but we don’t allow access to the XD machines, so, I had 1/2 my battle done.

Now the first time, I did it by hand, since I didn’t have a lot of time to figure out how to do it.  That was fine and dandy for a while, but my boss came back and asked me to refresh it.  Ok, so now I have a pattern, and it’s highly likely I’ll have to do it again at some point, so time to automate it.

In the past, I had written a pretty extensive MFCOM script to document XenApp 4.5/5.0 farms.  But, since we have XA 6.5, MFCOM wasn’t an option.  I started digging into the XA 6.5 SDK, and going through the powershell cmdlets.  I installed the SDK, and found out that it was built on powershell remoting.  I’m not strong on powershell remoting yet, so, the SDK was not much use without being on one of the servers (at least until I spend some time learning the remoting).

Most of the naming of the cmdlets looks very straightforward, and Citrix does follow Microsoft’s recommendation about putting initials in their commands to distinguish them.  All of the nouns for the commands start with xa.   The most obvious promising cmdlet was get-xaapplication.  One of the primary things about powershell is the discoverability.  So, the first thing to do was load the snapin, and run the get-help command.  I ran a quick help get-xaapplication -full. (As a side note, a while back I took Don Jones’ advanced powershell class, and it was fantastic.  In this class I learned about using the help command – it not only shows you help like get-help, it also paginates the help output – similar to more.exe or piping to out-host -paging.)

NAME
 Get-XAApplication

SYNOPSIS
 Retrieves the published applications in the farm.

SYNTAX
 Get-XAApplication [[-BrowserName] <String[]>] [-LocalhostOnly] [-ComputerName <String>] [<CommonParameters>]

 Get-XAApplication -FolderPath <String[]> [-LocalhostOnly] [-ComputerName <String>] [<CommonParameters>]

 Get-XAApplication -ServerName <String[]> [-SessionId <Int32[]>] [-LocalhostOnly] [-ComputerName <String>] 
 [<CommonParameters>]

 Get-XAApplication -WorkerGroupName <String[]> [-LocalhostOnly] [-ComputerName <String>] [<CommonParameters>]

 Get-XAApplication -FileTypeName <String[]> [-LocalhostOnly] [-ComputerName <String>] [<CommonParameters>]

 Get-XAApplication -AccountDisplayName <String[]> [-LocalhostOnly] [-ComputerName <String>] [<CommonParameters>]

 Get-XAApplication -LoadEvaluatorName <String[]> [-LocalhostOnly] [-ComputerName <String>] [<CommonParameters>]

 Get-XAApplication -InputObject <XAApplication[]> [-LocalhostOnly] [-ComputerName <String>] [<CommonParameters>]

Looks promising.. Fortunately for us, our external applications are collected together in a folder tree called external.  So, I tried get-xaapplication -folderpath external. That threw an error.  So, now I tried get-xafolder.  That turned up multiple folders:

FolderPath
----------
Applications
Servers
WorkerGroups

So, it was definitely interesting that it picked up the Applications node from the tree in AppCenter.  So, I tried get-xaapplication -folderpath applications/external.  I got the list of applications I was expecting.  The next thing was to see what properties were available. So, rather than typing it all again (with the expectation I’d be typing it a lot more, I set a quick $test = get-xaapplication -folderpath applications/external.  Another key option for discoverability – the get-member command.  The command shows the properties of the object.  $test | get-member.  I started looking at the properties. But, a critical item was missing – no trace of the user accounts?

A little internet research turned up that I needed the get-xaapplicationreport command.  The internet research turned up the idea of piping the get-xaapplication into the get-xaapplicationreport command.  I combined it all to see what I got from it.

PS C:\> Get-XAApplication | Get-XAApplicationReport | get-member

I found a property I was looking for – Accounts.. So the next step was to do the same command but instead of ending with get-member, I used format-list *.  This puts all the properties in a list, and shows you what the data actually is.  Accounts definitely contained the data I was looking for. Now it was time to get more detailed.  So, this time, I took a specific application.

$test = Get-XAApplicationReport -BrowserName notepad

Now, I just accessed the properties for $test.  $test.accounts provided a list of the user accounts and group accounts assigned to the application. But, it turned up several key properties: AccountName, AccountID, AccountType.  But, I still did not want to do this manually, or with a lot of in between steps.  I could find what I needed, and it was time to find a better way to handle this.  And I’ll cover that in the next post.