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