Summary: Microsoft MVP, Richard Siddaway, provides expert commentary for 2012 Scripting Games Advanced Event 5.
Microsoft Scripting Guy, Ed Wilson, is here. Richard Siddaway is the expert commentator for Advanced Event 5.
Richard has been working with Microsoft technologies for over 22 years, and he spent time in most IT roles including analyst-programmer, server administration, support, DBA and architect. He has been interested in automation techniques (including automating job creation and submission on mainframes many years ago!). He has used VBScript and WMI since it became available on NT 4. Windows PowerShell caught his interest when he first heard about it and he has been using it since the early beta versions.
He founded and runs the UK PowerShell User Group, and he is a Windows PowerShell MVP. He has given numerous talks about Windows PowerShell at various events in the UK, Europe, and the USA. He is a frequent speaker for Windows PowerShell user groups worldwide. He has a number of articles published about Windows PowerShell, including expert commentary’s on the Microsoft Scripting Games for which he has been a judge for the last two years. Windows PowerShell in Practice (Manning) was published in June 2010 and he is currently finishing PowerShell and WMI (Manning) to be published in 2012. Another book about Windows PowerShell 3.0 will be published towards the end of 2012.
Blogs:
Richard Siddaway's Blog: Of PowerShell and Other Things
PowerShell for Windows Admins
IT Knowledge Exchange: Get Answers from Your Peers
I’m going to approach this as I would when producing a production script. The starting point is the requirements that we get from the event scenario.
Requirements
You are an analyst on the server team of a medium sized organization. You are studying the performance and reliability of various servers on the network and decide to produce a report that lists the number of errors from all the traditional logs on a particular server. The code you use should be capable of running against a local computer, or against an arbitrary number of remote computers.
Other requirements:
- You will use impersonation for all remote connections, so you do not need to be able to supply credentials.
- You should not display errors due to permissions or due to no events matching your filter.
- You should not make changes to the users’ environment. If you do, you should change them back at the end of the script. Modification to the users’ environment following script completion will cost you points.
- Your script should run without prompting against the local computer.
- Your output should be organized such that the largest source of errors appears at the top of the output.
Design
When I start to design a script I note all of the features, issues and constraints I have to consider to fulfil the requirements. The parts in italics are the features I will build into the script.
My design notes:
- Needs to run in Windows PowerShell 2.0 and 3.0. Therefore, use #Requires –version 2 to set a minimum Windows PowerShell version for running the script.
- Only working with traditional logs. Can use Get-EventLog. WMI is alternative, but it’s a bit more complicated.
- Use an ErrorAction of SilentlyContinue to avoid displaying unnecessary error messages when accessing logs.
- Remote Registry Service must be running on the remote machine. Need to be able to start it, save state, and reset at end of script.
- Have to assume DCOM is available on the remote machine and that the firewall is configured to allow traffic. Could work round that by using CIM cmdlets in Windows PowerShell 3.0 in place of WMI and/or WSMAN cmdlets to read the event logs. Still a problem with starting remote registry service.
- Won’t assume the Windows PowerShell remoting is available. It would make the answer much easier to write. In a production environment, could probably get remoting enabled as a default.
- Must work remotely on multiple machines. Get-EventLog has –Computername parameter.
- Output to include log name and number of events in Error category. Make Error default, but allow for other types. Use ValidateSet attribute on parameter.
- Should include computer name in output. Use New-Object and set a custom type name.
- Output object but think of how to use it for well formatted report. Show examples in Help by using Format-Table –GroupBy.
- Test connectivity to remote machine, and provide warning if can’t reach it. Use Quiet mode on Test-Connection.
- Provide a default computer name. LocalHost doesn’t work with Get-EventLog, so need to test and modify.
- Sort BEFORE output so largest output first.
- Work on pipeline, accept single or no computer or an array of computer names.
- Need to add Help. Use comment-based Help.
- Create as an Advanced Function so can use in a future module. Should always consider how to use scripts.
- Use Write-Verbose and Write-Debug to comment.
The full script is shown at the end of the post, but I’ll discuss the major design points here.
The best place to check that you are running with elevated privileges is at the beginning of processing. Advanced functions have a BEGIN, PROCESS, and END blocks. The BEGIN block runs when the first object in the pipeline hits the function; PROCESS runs for every object, and END runs after the last object has been processed. Putting the privileges check in the BEGIN block means that we only do it once.
The ForEach ($computer in $computername ){…} block that starts the PROCESS block ensures that every computer is processed if we pass multiple computers to the ComputerName parameter.
I find that testing if a machine can be pinged is useful because it enables the machine to be skipped gracefully. If you want to test if it’s there, use the Count parameter set to 1. One answer is just as good as four in this case.
The Remote Registry service state is saved in a variable so that it can be used to set the service back to its original state. I need to use WMI to discover the service start mode, but that’s a Read-only property as far as WMI is concerned, so I need to use Set-Service to perform the modifications.
Getting a list of event logs on the remote machine and skipping those that are empty will speed processing. I always use Measure-Object to count things for me—it’s there, and I don’t need to worry about possible syntax issues.
I’m outputting objects, but I have sorted them for a given machine in descending order. If you are working with multiple machines using the GroupBy parameter with Format-Table gives a good display.
"dc02", "webr201", "server02" |
Get-EventEntryCount |
Format-Table -Property Logname, EntryType,
EntryCount -GroupBy ComputerName -AutoSize
The results are shown in the image that follows. Pipe it into a file and you have a report fit for any manager. I’ve added comment-based Help to the function. I always put my Help at the end of the function. It’s a personal preference because I prefer to go straight to the code.
What else could we do to this function?
- Allow multiple entry types, such as Error and Warning
- Use remoting (but would need to know or test that it is configured)
- Run as a background job
- Use workflows in PowerShell 3.0
Tips
Here are some of things that I discovered, or tripped over, or that are just plain useful:
- Use Write-Verbose and Write-Debug as comments in addition to supplying additional information when called.
- Remote Registry service must be running to access log information on a remote machine.
- Remember to use the ComputerName parameter when you want to access logs on remote machines
Working with event logs is explained in more detail in my books, PowerShell in Practice and PowerShell and WMI.
The Script
Here is the final script:
#Requires -Version 2
function Get-EventEntryCount{
[CmdletBinding()]
param (
[parameter(Position=0,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true)]
[Alias("CN", "Computer")]
[string[]]$computername="$env:COMPUTERNAME",
[parameter(Position=1)]
[ValidateSet("Error", "Information", "FailureAudit", "SuccessAudit", "Warning", "All", "*")]
[string]$eventtype="Error"
)
BEGIN{
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
$testadmin = (New-Object Security.Principal.WindowsPrincipal $currentUser).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
if (!$testadmin){
Throw "Must be run with elevated privileges"
}
}#begin
PROCESS{
foreach ($computer in $computername ){
switch ($computer) {
"." {$computer="$env:COMPUTERNAME"}
"localhost" {$computer="$env:COMPUTERNAME"}
}
Write-Verbose "Processing computer: $computer"
$data = @()
if (Test-Connection -ComputerName $computer -Count 1 -Quiet){
Write-Verbose "Starting Remote Registry service on $computer"
$origrrsrv = Get-WmiObject -Class Win32_Service -Filter "Name='RemoteRegistry'" `
-ComputerName $computer
if ($origrrsrv.StartMode -eq "Disabled") {
Set-Service -Name RemoteRegistry -ComputerName $computer -StartupType "Manual"
}
if ($origrrsrv.State -ne "Running") {
$origrrsrv.StartService() | Out-Null
}
Write-Verbose "Retrieving logs for $computer"
Get-EventLog -List -ComputerName $computer |
foreach {
Write-Verbose "Processing log: $($_.Log)"
$logcount = New-Object -TypeName PSObject -Property @{
ComputerName = $computer
LogName = $($_.Log)
EntryType = $eventtype
EntryCount = 0
}
$logcount.PSTypeNames[0] = "LogEntryCount"
if ($eventtype -eq "All" -or $eventtype -eq "*") {
Write-Debug "Processing all entries"
$logcount = $_.Entries.Count
}
elseif ($_.Entries.Count -gt 0) {
Write-Debug "Processing event type $eventtype"
$n = Get-EventLog -LogName $($_.Log) -EntryType $eventtype -ComputerName $computer `
-ErrorAction SilentlyContinue
if ($n -ne $null){
Write-Debug "Entries found"
$logcount.EntryCount = $n | Measure-Object | select -ExpandProperty Count
}
} # end if entries
else {
Write-Verbose "$($computer): $($_.Log) is empty"
}
$data += $logcount
if ($origrrsrv.State -eq "Stopped") {
$origrrsrv.StopService() | Out-Null
}
if ($origrrsrv.StartMode -eq "Disabled") {
Set-Service -Name RemoteRegistry -ComputerName $computer -StartupType "Disabled"
}
} # end of log processing foreach
}
else {
Write-Warning "Cannot contact $computer"
} # end if ping
Write-Output $data | sort EntryCount -Descending
} ## end computer foreach
}#process
END{}#end
<#
.SYNOPSIS
Counts the number of entries of a given type
in the event logs of a system
.DESCRIPTION
One or more computers - from pipeline or parameter - are
accessed to read the envent logs and count the entries of a
given type. Empty logs are tested and the count is set to zero
.PARAMETER computername
Name of computer for which log information
is to be retrieved
.PARAMETER eventtype
Log entry type to count.
Accepted values are -
"Error", "Information", "FailureAudit",
"SuccessAudit", "Warning", "All", "*"
.EXAMPLE
Get-EventEntryCount
Accesses logs on local machine. Peforms default display
.EXAMPLE
Get-EventEntryCount -computername "." |
Format-Table LogName, EntryCount -GroupBy ComputerName -AutoSize
Accesses logs on local machine. Format display and group by computer
.EXAMPLE
"dc02", "webr201", "server02" |
Get-EventEntryCount |
Format-Table -Property Logname, EntryType,
EntryCount -GroupBy ComputerName -AutoSize
Accesses logs on remote machines. Computer names accepted from pipeline.
Format display and group by computer
.EXAMPLE
Get-EventEntryCount -computername "dc02", "webr201", "server02" |
Format-Table -Property Logname, EntryType, EntryCount -GroupBy ComputerName -AutoSize
Accesses logs on remote machines. Computer names accepted as array.
Format display and group by computer
.INPUTS
Computer name - string or string array
Envent type - string. Must be member of set
.OUTPUTS
Returns a LogEntryCount custom object with
properties:
ComputerName - name of computer
LogName - name of log
EntryType - Type of log entry
EntryCount - count of entries
.NOTES
.LINK
#>
}
~Richard
2012 Scripting Games Guest Commentator Week Part 2 will continue Monday when we will present the scenario for Event 6. Join me tomorrow for a wrap-up blog about the 2012 Scripting Games.
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