Posts RSS Comments RSS 253 Posts and 411 Comments till now

PowerShell cmdlets for Splunk!


Splunk PowerShell Resource Kit

The Splunk PowerShell Resource Kit enables IT administrators to manage their Splunk topology, configure Splunk internals, and engage the Splunk search engine from their PowerShell session.

Example Uses
PS > Get-Splunkd

ComputerName MgmtPort HTTPPort SessionTimeout MinFreeSpace EnableWeb EnableWebSSL Splunk_Home
———— ——– ——– ————– ———— ——— ———— ———–
YETICLIENT 8089 8000 1h 2000 True True C:\Program Files\Splunk

PS > Get-SplunkdUser

ComputerName UserName FullName Email Type DefaultApp Roles
———— ——– ——– —– —- ———- —– admin Administrator Splunk launcher admin

Here are a few of the tasks enabled by the Resource Kit:

  • Determine or change the status of Splunk services across a set of Splunk servers in parallel.
  • Force one or more Splunk servers to reload their configuration, in parallel.
  • Deploy multiple Splunk forwarders to all active hosts in a Windows domain.
  • Retrieve a list of Splunk server classes, optionally filtered by last deployment client connection time, associated applications, or matching patterns.
  • Issue a Splunk search and format the retrieved events as a table, a list, or in a windowed grid view.

Most of the documentation leaves in the “Splunk PowerShell Resource Kit Cookbook”, which we highly recommend you read. You can find it here: Splunk PowerShell Resource Kit


You can find anything having to do with developing on Splunk at the Splunk developer portal:
You can also find full reference documentation of the REST API: API Reference


Email: Stay connected with other developers building on Splunk:
Answers: Check out this tag on Splunk answers for:
Twitter: @splunkdev


Resource Kits in Preview will not be Splunk supported. Once the PowerShell Resource Kit an Open Beta we will provide more detail on support.

Issues should be filed here:

Contact Us

You can reach the Dev Platform team at


The Splunk PowerShell Resource Kit is licensed under the Apache License 2.0. Details can be found in the file LICENSE.


blog: Avoid hardcoding in scripts. Here are some Simple discovery options in Powershell

When writing scripts I have always been a fan of making them as generic as possible. This may make the script a tad more complicated, but it allows it to be dynamic and also allows you to share these scripts between environments (i.e. Lab, QC, Production.) Basically we want to avoid hardcoding Domains, Domain Controllers, OUs, Containers, and site info.

Below I provide some simple examples of getting this information dynamically. This will allow you to discover the information instead of hardcoding it in the script.

To get forest information like Domains, Sites, ForestMode, RootDomain, and Forest masters you can use this:
  1. $Forest = [DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()

To get Domain information like Domain Controllers, DomainMode, Domain Masters, and Forest Root.
  1. $Domain = [DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()

To get the current Site information for the local machine like Subnets, Sitelinks, Location, Bridgehead Servers, and Domain Controllers.
  1. $MySite = [DirectoryServices.ActiveDirectory.ActiveDirectorySite]::GetComputerSite()

With these variables you can find all the Active Directory infrastructure information you could possibly want.

Here are some more specific examples on how to use these variables:

To find all your Global Catalogs in the forest
  1. $Forest.GlobalCatalogs

To find all the Domain Controllers in the current domain
  1. $Domain.DomainControllers

To see what application partitions your forest has
  1. $forest.ApplicationPartitions

To see the forest roles
  1. $forest | select SchemaRoleOwner,NamingRoleOwner

To see the domain roles
  1. $domain | select PDCRoleOwner,RidRoleOwner,InfrastructureRoleOwner

To see the subnets in the current site
  1. $MySite.subnets

To see the bridgehead Servers
  1. $MySite.BridgeheadServers

blog: Sick of WMI timeouts stopping your script? Try Test-Host!

I manage a large number of servers and this management involves a ton of WMI queries. A long time back I got sick of waiting and waiting for my WMI queries so I developed a little script called Test-Port that would do a WMI ping or would test a specific port (for firewalls) and pass the object on if connectivity passed.

You can see more details about that script here: Test-Port (kinda like portqry without verbose output)

With v2 just on the horizon I wanted to see what I could improve using Powershell v2 and decided to re-write the code (found below.)

Some of the new features are:
- Rename to Test-Host
- Built in Help and examples (biggy)
- Parameter Binding
- Added more logging with -verbose switch

This can be very useful if you ever have the need to do a task on a number of Computers (specifically WMI.) For example, If I needed to get all the machines running x64 in my domain I could something like this:

  1. $computers = Get-QADComputer -search $OU | %{$_.dnshostname} | Test-Host -tcp 135
  2. $query = "SELECT * FROM win32_processor WHERE addresswidth='64'"
  3. Get-WMIObject -query $query -comp $computers -asJob

Here is the code for Test-Host
Can download here: Test-Host.ps1 (from
  1. function Test-Host
  2. {
  4. [CmdletBinding()]
  6. Param(
  7. [Parameter(ValueFromPipeline=$true,Mandatory=$True)]
  8. [string]$Server,
  9. [Parameter()]
  10. [int]$TCPPort,
  11. [Parameter()]
  12. [int]$timeout=1000,
  13. [Parameter()]
  14. [string]$property
  15. )
  16. Begin
  17. {
  18. function TestPort {
  19. Param($srv,$tport,$tmOut)
  20. Write-Verbose " [TestPort] :: Start"
  21. Write-Verbose " [TestPort] :: Setting Error state = 0"
  22. $Error\3ActionPreference = "SilentlyContinue"
  24. Write-Verbose " [TestPort] :: Creating [system.Net.Sockets.TcpClient] instance"
  25. $tcpclient = New-Object system.Net.Sockets.TcpClient
  27. Write-Verbose " [TestPort] :: Calling BeginConnect($srv,$tport,$null,$null)"
  28. $iar = $tcpclient.BeginConnect($srv,$tport,$null,$null)
  30. Write-Verbose " [TestPort] :: Waiting for timeout [$timeout]"
  31. $wait = $iar.AsyncWaitHandle.WaitOne($tmOut,$false)
  32. # Traps
  33. trap
  34. {
  35. Write-Verbose " [TestPort] :: General Exception"
  36. Write-Verbose " [TestPort] :: End"
  37. return $false
  38. }
  39. trap [System.Net.Sockets.SocketException]
  40. {
  41. Write-Verbose " [TestPort] :: Exception: $($_.exception.message)"
  42. Write-Verbose " [TestPort] :: End"
  43. return $false
  44. }
  45. if(!$wait)
  46. {
  47. $tcpclient.Close()
  48. Write-Verbose " [TestPort] :: Connection Timeout"
  49. Write-Verbose " [TestPort] :: End"
  50. return $false
  51. }
  52. else
  53. {
  54. Write-Verbose " [TestPort] :: Closing TCP Sockett"
  55. $tcpclient.EndConnect($iar) | out-Null
  56. $tcpclient.Close()
  57. }
  58. if($?){Write-Verbose " [TestPort] :: End";return $true}
  59. }
  60. function PingServer {
  61. Param($MyHost)
  62. Write-Verbose " [PingServer] :: Pinging $MyHost"
  63. $pingresult = Get-WmiObject win32_pingstatus -f "address='$MyHost'"
  64. Write-Verbose " [PingServer] :: Ping returned $($pingresult.statuscode)"
  65. if($pingresult.statuscode -eq 0) {$true} else {$false}
  66. }
  67. }
  68. Process
  69. {
  70. Write-Verbose ""
  71. Write-Verbose " Server : $Server"
  72. if($TCPPort)
  73. {
  74. Write-Verbose " Timeout : $timeout"
  75. Write-Verbose " Port : $TCPPort"
  76. if($property)
  77. {
  78. Write-Verbose " Property : $Property"
  79. if(TestPort $Server.$property -tport $TCPPort -tmOut $timeout){$Server}
  80. }
  81. else
  82. {
  83. if(TestPort $Server -tport $TCPPort -tmOut $timeout){$Server}
  84. }
  85. }
  86. else
  87. {
  88. if($property)
  89. {
  90. Write-Verbose " Property : $Property"
  91. if(PingServer $Server.$property){$Server}
  92. }
  93. else
  94. {
  95. Write-Verbose " Simple Ping"
  96. if(PingServer $Server){$Server}
  97. }
  98. }
  99. Write-Verbose ""
  100. }
  101. }

blog: More thoughts on cmdlet design

If you were told you needed to move your GUI based management application to Powershell what would you do? How would you approach the design spec so that you maintain what your user base expects while providing the extra benefit of a CLI interface.

This is an interesting problem and one that I expect many will have in the future. I have thought about this at great length and decided that if you take the "task base" approach it is much simpler than you may first expect.

Let me illustrate what I mean using the dnsmgmt.msc interface. How would we go about converting that to a Powershell base cli that we could build a GUI on?

First I would break down the existing GUI into components. Like Servers, Zones, and Records. I would then think about what I could do to those components. Finally I would think of edge case scenarios that do not fit this generic model.

Using this method we would end up with something like this (not complete... just for illustration)

  • Add-DNSServer
  • Get-DNSServer
  • Set-DNSServer
  • Remove-DNSServer
  • New-DNSZone
  • Get-DNSZone
  • Set-DNSZone
  • Remove-DNSZone
  • New-DNSRecord
  • Get-DNSRecord
  • Set-DNSRecord
  • Remove-DNSRecord
  • ...
Edge Case:
  • Get-DNSInterface
  • Set-DNSInterface
  • Get-DNSRootHint
  • Set-DNSRootHint
  • Get-DNSForwarder
  • Set-DNSForwarder
  • Remove-DNSForwarder
  • ...

Using this method allows you design a scalable GUI bases on a solid CLI while maintaining a consistent.

What do you think?

Multiple Paths to the same End (Citrix)

Today I was asked this question

“Is there a more efficient way to list each published app and list out servers that are associated with it? In hope to give me an output that will display the ‘AppName’ objects once and display all the servers.”

He was referring to script I have posted somewhere (/shrug.)

I knew exactly what he wanted and thought “HEY! This would be a good time to blog about MFCom object Nesting.”

What do I mean by “MFCom object Nesting?” I simply mean that MFCom object model is nested in such a way that you can attain the same information from various code paths. For example, Farm objects contain Application objects which contain Server objects, but Farm objects also have Server objects which contain Application objects.

Farm -> Applications -> Servers
Farm -> Servers -> Applications

Why is this important or why would Citrix do this? IMO, it is to provide a shorter code path for the information you want.

Here are some examples

In the case of the question I received, what he had required getting all the applications from each server and returning the information like this:

$mfarm.Servers | Select ServerName -expandproperty Applications | Select ServerName,
                                                                          @{n="Groups";e={$_.Groups | %{$_.GroupName}}}

What he wanted to get was all the applications and output the information directly like:

$mfarm.Applications | %{$_.LoadData(1);$_} | Select-Object AppName,
                                                           @{n=‘Groups’;e={$_.Groups | %{$_.GroupName}}},
                                                           @{n=‘Servers’;e={$_.Servers | %{$_.ServerName}}}

Perhaps a more direct example would be collecting Sessions. If I want all the Sessions for a Server I could do one of the following:

$farm.Sessions | ?{$_.ServerNamematch "Server1"}

this works, but has to touch all my sessions… a better way would be like:

Get-CitrixServer  "Server1" | %{$_.Sessions}

More Info on LoadData()
If you are not familiar with the LoadData() method on the Application object it is critical that you get familiar, and get familiar quick.

LoadData() was introduced in MPS 4.0 (I believe) to allow the farm (or directly instantiated Application objects) to be returned with just a small bit of information. This allows you to quickly get basic application information without having to collect the entire data set, saving both time and network traffic. The problem is that most people do not know this and it can get quiet messy.

Why is it messy? If you get an application and set a property (like adding a user) and then commit the data back to the server using the SaveData() method, what do you think will happen? One could expect it would only update that which has change, but one would be wrong. When you call SaveData() it actually commits whatever is in local memory to the farm (yup, you got it… HOSED!) You just committed an effectively empty application and set it back to the Farm wiping out any existing information.

Moral of the story? USE LoadData().

My First Venture into S.DS.P and Powershell

There has be much debate and agony on the the slowness of System.DirectoryServices.DirectorySearcher Class. This has lead me down the path of playing with System.DirectoryServices.Protocols (aka s.ds.p.)

By far the best tool out there right now is ADFind by joe Richards. So I used this for my target (although I didnt expect to get close.)

Here is the Guide I used in my Journey:
Introduction to System.DirectoryServices.Protocols (S.DS.P)

I started by writing the script you will find below and tested in a domain with well over 700k users and one with 200K. I did the test three different times in each Domain changing the order so cache hits wouldn’t be an issue. I also use objectclass on all three so index wouldn’t be a factor. I should NOTE that my script ONLY returns the count ATM. I am going to add properties next.

As you will see by the test results below… I got pretty darn close to adfind.exe in regards to perf. I was quite impressed with S.DS.P. Now to be fair, this was just counting objects. I am sure adfind.exe will start sneaking ahead abit further when we start processing properties and such. Can’t wait to see! Understand these test were just to see what tests I should focus on. I will posting another entry on more extensive count test with just DSP Using 1.1 and Adfind.

Test 1 700K Users done Remote

DirectorySearcher : 125.9123257
ADFind : 46.3763349
DSP Using DN : 69.5628776
DSP Using 1.1 : 49.4458161

Test 2 700K Users done Remote

DirectorySearcher : 125.0230257
ADFind : 46.4486472
DSP Using DN : 68.9255288
DSP Using 1.1 : 49.0780736

Test 3 700K Users done Remote

DirectorySearcher : 125.0230257
ADFind : 47.9162918
DSP Using DN : 79.6885386
DSP Using 1.1 : 54.152966

Test 1 200K done Local

DirectorySearcher : 121.1063569
ADFind : 55.2775406
DSP Using DN : 67.897922
DSP Using 1.1 : 28.5441615

Test 2 200K done Local

DirectorySearcher : 80.0894455
ADFind : 23.558696
DSP Using DN : 54.3111576
DSP Using 1.1 : 42.3485998

Test 3 200K done Local

DirectorySearcher : 99.1125363
ADFind : 80.1497852
DSP Using DN : 64.3716824
DSP Using 1.1 : 64.1940421

Summary: adfind.exe was faster (by bout 4sec on Avg.) remotely and larger domain, but protocals was faster (by bout 8 sec on Avg.) local on smaller domain.

ADFind: 46.92
DSP Using 1.1: 50.89
DSP Using DN: 72.73
DirectorySearcher: 125.32


DSP Using 1.1: 45.03
ADFind: 52.99
DSP Using DN: 62.19
DirectorySearcher: 100.10

Here the script I ran for the test and how I measured the commands. I am going to play with passing the stats control and see what the server says later.

$SearcherExpression = @’
$searcher = new-object System.DirectoryServices.DirectorySearcher([ADSI]"","(objectclass=user)",@("distinguishedName"))
$searcher.pagesize = 1000

Write-Host "Test 1"
Write-Host ("-"*40)
$myresults1 = "" | select @{n="DirectorySearcher";e={(Measure-command {invoke-expression $SearcherExpression}).TotalSeconds}},
                         @{n="ADFind";e={(Measure-Command { .\adfind -b "dc=corp,dc=lab" -c -f "(objectclass=user)" }).TotalSeconds}},
                         @{n="DSP Using DN";e={(Measure-command { .\Test-DSProtocals.ps1 }).TotalSeconds}},
                         @{n="DSP Using 1.1";e={(Measure-command { .\Test-DSProtocalsSP.ps1 }).TotalSeconds}}
$myresults1 | fl

Write-Host "Test 2"
Write-Host ("-"*40)
$myresults2 = "" | select @{n="ADFind";e={(Measure-Command { .\adfind -b "dc=corp,dc=lab" -c -f "(objectclass=user)" }).TotalSeconds}},
                         @{n="DSP Using 1.1";e={(Measure-command { .\Test-DSProtocalsSP.ps1 }).TotalSeconds}},
                         @{n="DSP Using DN";e={(Measure-command { .\Test-DSProtocals.ps1 }).TotalSeconds}},
                         @{n="DirectorySearcher";e={(Measure-command {invoke-expression $SearcherExpression}).TotalSeconds}}

$myresults2 | fl

Write-Host "Test 3"
Write-Host ("-"*40)
$myresults3 = "" | select @{n="DSP Using DN";e={(Measure-command { .\Test-DSProtocals.ps1 }).TotalSeconds}},
                         @{n="DSP Using 1.1";e={(Measure-command { .\Test-DSProtocalsSP.ps1 }).TotalSeconds}},
                         @{n="DirectorySearcher";e={(Measure-command {invoke-expression $SearcherExpression}).TotalSeconds}},
                         @{n="ADFind";e={(Measure-Command { .\adfind -b "dc=corp,dc=lab" -c -f "(objectclass=user)" }).TotalSeconds}}
$myresults3 | fl


Here is what the output of that Script looks like

S.DS.P : MyTest.ps1 Output

Here is the System.DirectoryServices.Protocols Code

[System.Reflection.assembly]::LoadWithPartialName("system.directoryservices.protocols") | Out-Null
$domain = ([ADSI]"").distinguishedName -replace  ",","." -replace "dc=",""
$DomainDN = "DC=" + $Domain -replace "\.",",DC="
[int]$pageCount = 0
[int]$pageSize = 1000
[int]$count = 0
$connection = New-Object System.DirectoryServices.Protocols.LdapConnection($domain)  
$subtree = [System.DirectoryServices.Protocols.SearchScope]"Subtree"
$filter = "(objectclass=user)"
$searchRequest = New-Object System.DirectoryServices.Protocols.SearchRequest($DomainDN,$filter,$subtree,@("1.1"))  
$pagedRequest = New-Object System.DirectoryServices.Protocols.PageResultRequestControl($pageSize)
$searchRequest.Controls.add($pagedRequest) | out-null
$searchOptions = new-object System.DirectoryServices.Protocols.SearchOptionsControl([System.DirectoryServices.Protocols.SearchOption]::DomainScope)
$searchRequest.Controls.Add($searchOptions) | out-null

while ($true)
    ## increment the pageCount by 1
    ## cast the directory response into a
    ## SearchResponse object
    $searchResponse = $connection.SendRequest($searchRequest)
    ## verify support for this advanced search operation
    if (($searchResponse.Controls.Length -lt 1) -or
        !($searchResponse.Controls[0] -is [System.DirectoryServices.Protocols.PageResultResponseControl]))
        Write-Host "The server cannot page the result set"
    ## cast the diretory control into
    ## a PageResultResponseControl object.
    $pageResponse = $searchResponse.Controls[0]
    ## display the retrieved page number and the number of
    ## directory entries in the retrieved page                    
    #"Page:{0} Contains {1} response entries" -f $pageCount,$searchResponse.entries.count
    $count += $searchResponse.entries.count
    ## display the entries within this page
    ## foreach($entry in $searchResponse.entries){$entry.DistinguishedName}
    ## if this is true, there
    ## are no more pages to request
    if ($pageResponse.Cookie.Length -eq 0){write-Host $count;break}
    ## set the cookie of the pageRequest equal to the cookie
    ## of the pageResponse to request the next page of data
    ## in the send request
    $pagedRequest.Cookie = $pageResponse.Cookie

Get-CitrixApplication (Playing Around Series)

Here is a quick demo of getting a Citrix Application and playing with its properties.

Best Viewed Full Screen

Get the Flash Player to see this content.

Demo File

# First we need to create the MFCOM Object
$mfapp = new-object -com MetaFrameCom.MetaFrameApplication
# To initialize we need to pass the app we want to accesss
# With Applications we need to load the data
# Lets see what we have
$mfapp | Get-Member -type Properties
# Lets look at Users and Groups
$mfapp | select Users,Groups
# How bout Servers
$mfapp.Servers | Select ServerName
# Sessions?
$mfApp.Sessions | ft SessionID,AppName,ClientAddress,ClientHRes,ClientVRes -auto

Get-CitrixFarm (Playing Around Series)

I wanted to show how EASY it is to play with Citrix MFCom so here is a little video.

I also want to note how most of the properties (like Servers,Applications,Zones) all return objects that have their own properties and methods. So you could very easily have these lines in your profile and always have everything just sitting there waiting to be used.

$farm = New-Object -Com ‘MetaframeCOM.MetaFrameFarm’
Write-Host "Loaded Farm Info from $($farm.FarmName)

Best Viewed Full Screen

Get the Flash Player to see this content.

Demo File

# Get Citrix Farm Object
$farm = New-Object -Com ‘MetaframeCOM.MetaFrameFarm’
# Initialize Farm
# Now that we have are farm. Lets make sure we have the one we want by Getting the FarmName
# Lets see what we have to play with
$farm | Get-Member -type Properties
# We have the Farm we want. Some of the Info we want is Admins. So lets Start there
# To View just a list
$farm.Admins | Select FriendlyName
# Lets see what Applications we have
$farm.Applications | ft BrowserName,ParentFolderDN
# To View the Servers
$farm.Servers | ft ServerName,IPAddress,SessionCount
# How bout Sessions?
# Lets look at Print Drivers we Have installed
# If you have multiple Zones you can get the Names Servers and DataCollector for the Zone

Get-CitrixAppServer.ps1 (Citrix Top 10)

This is pretty simple script. It is actually just an adaption from my Get-CitrixApplication.ps1 script posted at the bottom. There is a moment in time when you start thinking in terms of objects and stop thinking in terms of text output you are looking for.

This will make a huge difference in productivity and take you from constantly writing scripts to interactively getting the information you are after, but until you make the transition I will continue to provide scripts that do both 🙂

# Get-CitrixAppServer.ps1
# Brandon Shell [MVP]
# Gets All the Servers for Specific App
$mfApp = New-Object -ComObject MetaFrameCOM.MetaFrameApplication
$mfApp.Servers | %{$_.ServerName}

Here is the script that I use to return an Application object.

# Get-CitrixApplication.ps1
# Brandon Shell [MVP]
# Gets a Citrix Application Object.
$mfApp = New-Object -ComObject MetaFrameCOM.MetaFrameApplication

Then from the commandline I would just do this
PS> Get-CitrixApplication.ps1 “Applications\MyApp1” | %{$_.Servers} | %{$_.ServerName}

While this looks more complicated it is more versatile and easy to change. Lets say I want current User for this app instead.
PS> Get-CitrixApplication.ps1 “Applications\MyApp1” | %{$_.Sessions} | %{$_.UserName}

Get-CitrixPrinterInfo.ps1 (Citrix Top 10)

This script just collects the Printer Information from all the Servers in the Farm. It will output them to a file or you can pipe them and filter them.

Name: Get-CitrixPrinterInfo.ps1
Purpose: Gets Print Driver Info from all the Servers in the Farm and outputs to file or console

# Get-CitrixPrinterInfo.ps1
# Brandon Shell [MVP]
# Gets Print Driver Info from all the Servers in the Farm
function HelpMe{
    Write-Host " Get-CitrixPrinterInfo.ps1:" -fore Green
    Write-Host "   Gets Print Driver Info from all the Servers in the Farm"
    Write-Host " Parameters:" -fore Green
    Write-Host "   -Server                : Optional. Server to Get Print Info From "
    Write-Host "   -File                  : Optional. File Name to Export Info to"
    Write-Host "   -Help                  : Optional. Displays This"
    Write-Host " Examples:" -fore Green
    Write-Host "   Gets Print Info and Exports to File" -fore White
    Write-Host "     .\Get-CitrixPrinterInfo.ps1 -file MyExportFile.txt" -fore Yellow
    Write-Host "   Gets Print Drivers from a Specific Server and outputs DriverName and SourceServer"  -fore White
    Write-Host "     .\Get-CitrixPrinterInfo.ps1 <serverName> | ft DriverName,SourceServer" -fore Yellow

# Check for Help Flag

# Check for File
    if($server) # If -Server was passed we run check just against it and output to screen
        $mfsrv = new-Object -com "MetaframeCOM.MetaframeServer"
        $mfsrv | foreach{"`n$($_.ServerName) `n$(‘-‘*20)";$_.PrinterDrivers| Format-Table} | out-File $file -enc ASCII
    else # We run the check against the whole farm and output results to file
        $mfarm = new-Object -com "MetaframeCOM.MetaframeFarm"
        $mfarm.Servers | foreach{"`n$($_.ServerName) `n$(‘-‘*20)";$_.PrinterDrivers| Format-Table} | out-File $file -enc ASCII
    if($server) # If -Server was passed we run check just against it and output to screen
        $mfsrv = new-Object -com "MetaframeCOM.MetaframeServer"
        $mfsrv | foreach{"`n$($_.ServerName) `n$(‘-‘*20)";$_.PrinterDrivers| Format-Table}
    else # We run the check against the whole farm and output results to screen
        $mfarm = new-Object -com "MetaframeCOM.MetaframeFarm"
        $mfarm.Servers | foreach{"`n$($_.ServerName) `n$(‘-‘*20)";$_.PrinterDrivers| Format-Table}

Next »