Summary: Microsoft community contributor, Boe Prox, provides expert commentary for 2012 Scripting Games Advanced Event 2.
Microsoft Scripting Guy, Ed Wilson, is here. Boe Prox is the expert commentator for Advanced Event 2.
Boe is currently a senior systems administrator with BAE Systems. He has been in the IT industry since 2003, and he has been working with Windows PowerShell since 2009. Boe looks to script whatever he can, whenever he can. He is also a moderator on the Official Scripting Guys Forum. Check out his current projects published on CodePlex: PoshWSUS and PoshPAIG.
Blog: Learn PowerShell | Achieve More
Guest posts on Hey, Scripting Guy! Blog
Twitter: @proxb
This is an event that relates very nicely to something that would be done in the “real world.” Tracking your services on each server is something that should be done daily to ensure that everything is working normally and to make sure that there are no anomalies occurring.
This event requires us to perform a query against local and/or remote servers to pull back all of the services and write out the data to a CSV file. This is definitely a job for Windows PowerShell!
Requirements
This event is not without requirements to give you more of a challenge, so let’s take a look at the key requirements:
- You need to present your findings in a spreadsheet that includes the following information: the server name, the service name, the start mode of the service, the user account used to start the service, and the current status of the service.
- You should use the easiest method possible to display the information in a spreadsheet.
- You must include the ability to run the script on a local computer and on a remote computer.
- You must include the ability to run the script with alternate credentials when operating against a remote computer, and you should impersonate the logged on user when running locally.
- You do not need to add comment-based Help for this scenario, but additional points are awarded if you do include appropriate comment-based Help such as the description, examples, and parameters.
- If the script requires admin rights to run, you should check to ensure that the script is running with admin rights. If those rights are not present, you should display an appropriate message and exit.
- For the purposes of this exercise, do not write a module. You should make your script completely standalone and have no external dependencies. Therefore, everything needed should be put in this script. Failure to do so will cost you points.
Now that we know what is required for this script, we can begin to dive into the code and see what is going on. I will take the code apart in chunks to explain what I am doing and point out the areas that meet each requirement in addition to other areas that I feel are worth mentioning. I included some extra features for this script, and I will explain my reasoning behind this when I get to them.
The code
Function Get-ServiceData {
<#
.SYNOPSIS
Command used to find all services with option to write to a CSV File
.DESCRIPTION
Command used to find all services with option to write to a CSV File
.PARAMETER Computername
A single or collection of systems to perform the query against
.PARAMETER Credential
Alternate credentials to use for query of services
.PARAMETER ToCSV
Name of CSV to write the results of the query to
.PARAMETER Throttle
Number of asynchronous jobs that will run at a time
.PARAMETER ShowProgress
Displays the progress of the services query
.NOTES
Author: Boe Prox
Created: 14March2012
.EXAMPLE
Get-ServiceData
Description
-----------
Retrieves all services from the local system
.EXAMPLE
$Servers = 'Server1','Server2','Server3'
Get-ServiceData -Computername $Servers -ShowProgress
Description
-----------
Retrieves all services from the remote servers and displays a progress bar
.EXAMPLE
$Servers = Get-Content Servers.txt
$Servers | Get-ServiceData -ShowProgress -Throttle 10
Description
-----------
Retrieves all services from the remote servers while running 10 runspace jobs at a time
and displays a progress bar to show the status of each runspace job.
.EXAMPLE
$Servers = Get-Content Servers.txt
$Servers | Get-ServiceData -Credential (Get-Credential) -ToCSV (Join-Path $pwd report.csv)
Description
-----------
Retrieves all services for each system in the Servers.txt file while using the supplied
alternate credentials and outputs the data to a CSV file.
#>
#Requires -Version 2.0
[cmdletbinding(
DefaultParameterSetName = 'NonCSV'
)]
Param (
[parameter(ValueFromPipeline = $True,ValueFromPipeLineByPropertyName = $True)]
[Alias('CN','__Server')]
[string[]]$Computername = $Env:Computername,
[parameter()]
[System.Management.Automation.PSCredential]$Credential,
[parameter(ParameterSetName = 'CSV')]
[ValidateNotNullOrEmpty()]
[String]$ToCSV,
[parameter()]
[int]$Throttle = 5,
[parameter()]
[switch]$ShowProgress
)
If you are going to create an advanced function, you definitely have to have inline Help within that function. This does not mean that you write your own Help function and offer that. It means that you use the Help in Windows PowerShell to accomplish this task. I included examples about how to use this function so others can reference it when trying to run the command.
Also included is [cmdletbinding()], which makes this advanced function advanced by adding extra parameters such as –Verbose and –WhatIf. I also make sure that I add a default parameter value for my ComputerName parameter to point to the local machine if nothing is given. Also, if no file name is given with ToCSV, it will throw an error until a file name is specified.
Begin {
#Function that will be used to process runspace jobs
Function Get-RunspaceData {
[cmdletbinding()]
param(
[switch]$Wait,
[switch]$ShowProgress
)
Do {
$more = $false
Foreach($runspace in $runspaces) {
If ($runspace.Runspace.isCompleted) {
$Script:Report += $runspace.powershell.EndInvoke($runspace.Runspace) |
Select SystemName,Name,State,StartMode,StartName
$runspace.powershell.dispose()
$runspace.Runspace = $null
$runspace.powershell = $null
$Script:i++
If ($PSBoundParameters['ShowProgress']) {
Write-Progress -Activity 'Services Query' -Status ("Processing Runspace: {0}" -f $runspace.computer) `
-PercentComplete (($i/$totalcount)*100)
}
} ElseIf ($runspace.Runspace -ne $null) {
$more = $true
}
}
If ($more -AND $PSBoundParameters['Wait']) {
Start-Sleep -Milliseconds 100
}
#Clean out unused runspace jobs
$temphash = $runspaces.clone()
$temphash | Where {
$_.runspace -eq $Null
} | ForEach {
Write-Verbose ("Removing {0}" -f $_.computer)
$Runspaces.remove($_)
}
} while ($more -AND $PSBoundParameters['Wait'])
}
Write-Verbose ("Performing inital Administrator check")
$usercontext = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
$IsAdmin = $usercontext.IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")
#Counter for Write-Progress
$Script:i = 0
#Main collection to hold all data returned from runspace jobs
$Script:report = @()
Write-Verbose ("Building hash table for WMI parameters")
$wmihash = @{
Query = "SELECT SystemName,Name,StartMode,State,Startname FROM Win32_Service"
ErrorAction = "Stop"
}
#Supplied Alternate Credentials?
If ($PSBoundParameters['Credential']) {
$wmihash.credential = $Credential
}
#Define hash table for Get-RunspaceData function
$runspacehash = @{}
If ($PSBoundParameters['ShowProgress']) {
$runspacehash.ShowProgress = $True
}
#Define Scriptblock for runspaces
$scriptblock = {
Param (
$Computer,
$wmihash
)
Write-Verbose ("{0}: Checking network connection" -f $Computer)
If (Test-Connection -ComputerName $Computer -Count 1 -Quiet) {
#Check if running against local system and perform necessary actions
Write-Verbose ("Checking for local system")
If ($Computer -eq $Env:Computername) {
$wmihash.remove('Credential')
} Else {
$wmihash.Computername = $Computer
}
Try {
Get-WmiObject @wmihash
} Catch {
Write-Warning ("{0}: {1}" -f $Computer,$_.Exception.Message)
Break
}
} Else {
Write-Warning ("{0}: Unavailable!" -f $Computer)
Break
}
}
Write-Verbose ("Creating runspace pool and session states")
$sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
$runspacepool = [runspacefactory]::CreateRunspacePool(1, $Throttle, $sessionstate, $Host)
$runspacepool.Open()
Write-Verbose ("Creating empty collection to hold runspace jobs")
$Script:runspaces = New-Object System.Collections.ArrayList
$totalcount = $computername.count
}
The Begin block is pretty meaty but it sets up a lot of the code that will be processed later in the function.
A few of the requirements are covered here with an initial check for Administrator rights and allowing the code to run on a local or a remote system. A gotcha is also presented here because the use of alternate credentials will not work against a local system.
If a local system is included with other remote systems, the alternate credentials are simply removed from the runspace.
Although I do not have time to dive into everything here, I will say that this is where I set up a script block that handles each client computer, use two hash tables for splatting parameters (one for the Get-WMIObject cmdlet and another for a function that will be defined later). I also make use of the [runspacefactory] accelerator that allows me to set up some background runspace jobs and also allows throttling those runspaces, which allows for quicker processing of the queries. I chose this over using the *-Job cmdlets because there is not an easy way to throttle jobs, and this is more of a lightweight method to run jobs in a background runspace.
To quickly jump into an example, the following screenshots show the amount of time it took to run this function by using the background runspaces versus using a more synchronous command, ForEach, against 132 systems.
Get-ServiceData Function
Using ForEach
As you can see, the difference is about 220 seconds. While not a massive amount of time, it is still a nice improvement over the synchronous way using ForEach. Credit for the throttling technique goes to Windows PowerShell MVP, Dr. Tobias Weltner, for his webcast, Speeding Up PowerShell: Multithreading, which explained the use of [runspacefactory] to provide throttling.
Process {
Write-Verbose ("Validating that current user is Administrator or supplied alternate credentials")
If (-Not ($Computername.count -eq 1 -AND $Computername[0] -eq $Env:Computername)) {
#Now check that user is either an Administrator or supplied Alternate Credentials
If (-Not ($IsAdmin -OR $PSBoundParameters['Credential'])) {
Write-Warning ("You must be an Administrator to perform this action against remote systems!")
Break
}
}
ForEach ($Computer in $Computername) {
#Create the powershell instance and supply the scriptblock with the other parameters
$powershell = [powershell]::Create().AddScript($ScriptBlock).AddArgument($computer).AddArgument($wmihash)
#Add the runspace into the powershell instance
$powershell.RunspacePool = $runspacepool
#Create a temporary collection for each runspace
$temp = "" | Select-Object PowerShell,Runspace,Computer
$Temp.Computer = $Computer
$temp.PowerShell = $powershell
#Save the handle output when calling BeginInvoke() that will be used later to end the runspace
$temp.Runspace = $powershell.BeginInvoke()
Write-Verbose ("Adding {0} collection" -f $temp.Computer)
$runspaces.Add($temp) | Out-Null
Write-Verbose ("Checking status of runspace jobs")
Get-RunspaceData @runspacehash
}
}
The Process block does just that: processes all of the computers and assigns each computer as a background runspace. More requirements are met here, including the finishing check of Administrator rights if running against remote systems. If there is only a local system and it is the only system in the collection, alternate credentials are not required nor are Administrator rights because any local user can perform a query against WMI.
End {
Write-Verbose ("Finish processing the remaining runspace jobs: {0}" -f (@(($runspaces | Where {$_.Runspace -ne $Null}).Count)))
$runspacehash.Wait = $true
Get-RunspaceData @runspacehash
Write-Verbose ("Closing the runspace pool")
$runspacepool.close()
If ($PSBoundParameters['ShowProgress']) {
#Close the Write-Progress bar so it does not affect the displaying of data when completed.
Write-Progress -Activity 'Services Query' -Status 'Completed' -Completed
}
If ($PSBoundParameters['ToCSV']) {
Write-Verbose ("Writing report to CSV: {0}" -f $ToCSV)
$Report | Export-Csv -Path $ToCSV -NoTypeInformation
} Else {
Write-Verbose ("Displaying Report")
Write-Output $Report
}
}
}
The End block finishes up the remaining runspace jobs, and depending on what the user chose for output (CSV or no CSV), the function will output the data to the console or write all of the data to a specified CSV file (meeting an important requirement). I chose to use Export-CSV because it meets the requirement for the easiest method possible to create a CSV file. If I was using Windows PowerShell 3.0, I could have thrown this in with the runspace script block and used the –Append parameter to further increase the performance of this function instead of holding all of the data until the end and writing out to the CSV file.
Let’s give it a quick run to see it in action and view the final output.
$Servers = Get-Content .\servers.txt
Get-ServiceStartMode -Computername $servers -ShowProgress -ToCSV "C:\users\boe\desktop\report.csv"
As you can see, all of the data that was required is on the spreadsheet to view.
The script is available to download from the Script Center Repository.
~Boe
2012 Scripting Games Guest Commentator Week will continue tomorrow when we will present the scenario for Event 3.
I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
Ed Wilson, Microsoft Scripting Guy