Learn about Windows PowerShell
(Note: These solutions were written for Advanced Event 2 of the 2010 Scripting Games.)
Advanced Event 2 (Windows PowerShell and VBScript)
Richard Siddaway, Windows PowerShell MVP
Richard Siddaway is technical architect for Serco in the United Kingdom who works on transformation projects in the local government and commercial arena. With more than 20 years’ experience in various aspects of information technology, Richard specializes in the Microsoft environment at an architectural level, especially around Active Directory (AD), Exchange, SQL Server, and infrastructure optimization.
Richard is always looking for the opportunity to automate a process, preferably with Windows PowerShell. Richard founded and currently leads the UK Windows PowerShell User Group. Microsoft has recognized his technical expertise and community activities by presenting him with a Microsoft Most Valued Professional Award. Richard has presented to the Directory Experts Conference, at various events at Microsoft in the UK and Europe, and for other UK User Groups. Richard has a number of articles and technical publications to his credit, and is currently finalizing his first print book. The subject? Windows PowerShell of course.
Blog: http://msmvps.com/blogs/RichardSiddaway/Default.aspx
Book details: http://www.manning.com/siddaway
-------------
I will be providing a Windows PowerShell version and a VBScript version of this solution. I will also compare and contrast the two versions.
The Problem
We need to read the system event log of a machine to find when it started. This is approximated by the time the event log service started. From the information supplied this is contained in Event ID 6005.
After that we need to extend our script to:
Okay, that should keep us busy. We’ll start with the Windows PowerShell version.
Note: I am using Windows PowerShell 2.0 for this explanation. All development will be performed in the Windows PowerShell ISE.
Windows PowerShell
In Windows PowerShell 1.0, we had Get-EventLog. This is still available in Windows PowerShell 2.0 with enhancements. We query the System event log on the given computer and then filter using Where-Object on Event ID 6005. Get-EventLog can accept an Instance ID but not an Event ID. The IDs are related but not identical. Because we only have the Event ID, we have to use Where-Object. This is shown here:
#Requires -version 2.0$evt = Get-EventLog -LogName System -ComputerName rslaptop01 | Where {$_.EventId -eq 6005} | Select -First 1Write-Host "Time Started: $($evt.TimeGenerated.DateTime)"
I then use the Select-Object cmdlet to get the first event, which will contain the time of last startup. The Write-Host cmdlet is used to write out the startup time.
We also have the option of using WMI, as shown here:
$evt = Get-WmiObject -Class Win32_NTLogEvent -Filter "LogFile = 'System' AND EventCode=6005 " -ComputerName rslaptop01 | select -First 1Write-Host "Time Started: $($evt.ConvertToDatetime($evt.TimeGenerated))"
The only issue with this is converting the date to a readable format, but we have the ConvertToDatetime() method as a standard way of achieving this.
Windows PowerShell 2.0 introduced the Get-WinEvent cmdlet that can read traditional and newer-style logs introduced in Windows Vista and later:
#Requires -version 2.0$evt = Get-WinEvent -FilterHashtable @{LogName='System'; ID=6005} ` -MaxEvents 1 -ComputerName rslaptop01Write-Host "Time Started: $($evt.TimeCreated.DateTime)"
In this version we supply the event log name and the event ID in a hash table. We can filter directly and restrict ourselves to the first event found. Write-Host is then used to display the answer. This runs much faster than using Get-EventLog because the filtering all happens in the cmdlet rather than on the pipeline. A best practice is to always restrict the amount of data you are working with as soon as possible.
In both the scripts, we display the DateTime property. This is to produce a display that removes any ambiguity about the date being in dd/mm/yyyyy format or mm/dd/yyyy format.
The filter hash table can work with a number of parameters including:
· LogName=<String[]>
· ProviderName=<String[]>
· Path=<String[]>
· Keywords=<Long[]>
· ID=<Int32[]>
· Level=<Int32[]>
· StartTime=<DateTime>
· EndTime=<DataTime>
· UserID=<SID>
· Data=<String[]>
· *=<String[]>
For the rest of the example, I will work with Get-WinEvent. The following script is what I came up with.
Event2a.ps1
#Requires -version 2.0function Get-Log {<#.SYNOPSIS Retrieves events from the event logs on local or remote machines..DESCRIPTION The function will retrieve events from classic and new style logs on local or remote machines. Alternate credentials can be used where the current user doesn't have permissions on the remote machine..PARAMETER log Name of the event log. Defaults to system log. .PARAMETER id Specific event to retrieve..PARAMETER computer Computer name. Defaults to localhost..PARAMETER date Date for which events are to be retrieved..PARAMETER firstdate Starting date to retrieve events..PARAMETER lastdate Date to end retrieving events.EXAMPLE Get-Log -date "22 March 2010" Gets events from the system log on the local machine that occurred on a given day..EXAMPLE Get-Log -firstdate "20 March 2010" Gets events from the system log on the local machine that occurred after a given day..EXAMPLE Get-Log -lastdate "22 March 2010" Gets events from the system log on the local machine that occurred before a given day..EXAMPLE Get-Log -firstdate "20 March 2010" -lastdate "22 March 2010" Gets events between the 20th and 22nd of March.EXAMPLE Get-Log -log Application -firstdate "20 March 2010" -lastdate "22 March 2010"#>param ( [CmdletBinding()] [string]$log = "System", [int]$id, [string]$computer = 'localhost', [datetime]$date, [datetime]$firstdate, [datetime]$lastdate, [switch]$cred, [regex]$pattern) if ($date -and ($firstdate -or $lastdate)){Throw "Cannot use date together with firsdate or lastdate"} $fht = @{LogName = $log} if ($id){$fht += @{ID = $id}} if ($date){ $fht += @{StartTime = $date; EndTime = $date.AddHours(23).AddMinutes(59).AddSeconds(59)} } if ($firstdate -and !$lastdate){ $fht += @{StartTime = $firstdate} } if (!$firstdate -and $lastdate){ $fht += @{EndTime = $lastdate} } if ($firstdate -and $lastdate){ if ($lastdate -lt $firstdate){Throw "Last date before first date"} else{ $fht += @{StartTime = $firstdate; EndTime = $lastdate} } } if ($cred){ $crd = Get-Credential $s = {Get-WinEvent -FilterHashtable $fht -ComputerName $computer -Credential $crd} } else { $s = {Get-WinEvent -FilterHashtable $fht -ComputerName $computer} } if ($pattern){ Invoke-Command $s | Where {$_.Message -match $pattern} } else { Invoke-Command $s }}
I created the answer as a function because I decided I would eventually create a module of all of the answers. I can then load all of them in one command. Alternatively, you could put the function in a script and dot source it.
. ./event2a.ps1
This loads the function and we can use it as Get-Log, which will return the contents of the system log on the local machine. The script starts with:
#Requires -version 2.0
This checks to see if we are running on Windows PowerShell 2.0 and stops the script if not. The next set of lines between <# and #> supplies the comment-based help. This is a new feature for functions in Windows PowerShell 2.0.
Typing
Get-Help Get-Log
provides exactly the same sort of help that we get with the cmdlets. We have the same options for getting more detailed help. For example
Get-Help Get-Log –full
displays all of the help information.
The parameters are defined using a param statement. I have supplied a data type for all of the parameters and provided default values where applicable. This catches a number of errors; for example, an attempt to supply an Event ID of “swamp” will be rejected and cause an error.
A test is performed to see if the date parameter has been combined with firstdate and/or lastdate. If so, an error is thrown. This could also be achieved using parameter sets if required.
The hash table $fht is built up starting with the log name. One addition to the script would be to test to see if that log existed on the target machine. At the moment, we assume it does. If an Event ID is supplied, we add that to the hash table. The default is that records for all event codes are returned.
The next task is to set the start and end dates for the period we want to examine the entries. There are a number of possibilities for this so we have a number of tests.
A final job is to test if we need alternative credentials. If so, we use the Get-Credential cmdlet to get them.
A script block is created with the required syntax for Get-WinEvent including the computer name. We do one last check to see if we are filtering on a regular expression (man, I still hate them), and we use Invoke-Command to run our script block. If we have a regular expression filter, we use that in the Where-Object cmdlet as shown.
Some examples of using the script:
To display all entries in the system log on the local machine for a given date:
Get-Log -date "22 March 2010"
To display all entries in the system log on the local machine that occurred after the given date:
Get-Log -firstdate "22 March 2010"
To display all entries in the system log on the local machine that occurred before the given date:
Get-Log -lastdate ""22 March 2010"
To display all entries in the system log on the local machine that occurred March 20–22:
Get-Log -firstdate "20 March 2010" -lastdate "22 March 2010"
If the dates are the wrong way around, an error is thrown:
Get-Log -firstdate "22 March 2010" -lastdate "20 March 2010"
Last date before first date:
At C:\scripts\Scripting Games 2010\event2a.ps1:26 char:39+ if ($lastdate -lt $firstdate){Throw <<<< "Last date before first date"} + CategoryInfo : OperationStopped: (Last date before first date:String) [], RuntimeException + FullyQualifiedErrorId : Last date before first date
To display all entries in the application log on the local machine that occurred March 20–22:
Get-Log -log Application -firstdate "20 March 2010" -lastdate "22 March 2010"
The really nice thing about doing this as a function is that I can use it on the pipeline, as the following example shows.
I was also asked to have a look at the VBScript option. I used to use VBScript a lot until I discovered Windows PowerShell. I thought it would be an interesting challenge to go back and try the same task in VBScript.
We are more limited in our options with VBScript in that we have to use WMI to access the logs. The WMI class we need is Win32_NTLogEvent as shown here:
strComputer = "."Set objWMIService = GetObject("winmgmts:" _ & "{impersonationLevel=impersonate}!\\" _ & strComputer & "\root\cimv2") Set colEvents = objWMIService.ExecQuery _ ("SELECT * FROM Win32_NTLogEvent WHERE Logfile = 'System' AND " _ & "EventCode = '6005'")For Each objEvent in colEvents Wscript.Echo "Computer " & objEvent.ComputerName _ & " Started at " & WmiDate(objEvent.TimeWritten) WScript.QuitNextFunction WmiDate(startdate) WmiDate = Mid(startdate, 7, 2) & "/" _ & Mid(startdate, 5, 2) & "/" & Left (startdate, 4) & " " _ & Mid(startdate, 9, 2) & ":" _ & Mid(startdate, 11, 2) & ":" _ & Mid(startdate, 13, 2) End Function
This is the direct equivalent of the Windows PowerShell script, which is shown again here:
The computer name is set to “.” to represent the local machine. We then create an object for WMI. We have to create a full WQL query, which we run to create a collection of objects containing events.
This is a more cumbersome way of working compared to the Get-WmiObject of Windows PowerShell. We use WQL in the –filter parameter of Get-WmiObject, but only the part after the WHERE statement.
Unfortunately it is not easy to select individual objects from the collection we create in VBScript. I start iterating through the collection and write out the start time. Because I want only the first record, I can quit the script. Compare this approach to the select –first 1 we use in Windows PowerShell. I know which I prefer.
A final problem is that WMI returns the date in a format that is not very friendly. We need to create a VBScript function (remember the difference between a subroutine and a function in VBScript) to reformat that data into a more readable format. We do that by using a series of string manipulation functions to perform the reformatting. This is supplied as a function on the Windows PowerShell object.
The ability to use string substitution in the Windows PowerShell example makes writing the output a bit simpler.
So far I would say that the Windows PowerShell script is simpler and quicker to produce. If you have a template for using WMI with VBScript scripts, you might save a bit of development time and the function to convert the date format should ideally be available already.
Let’s see what happens when we move on to the main event. The script for this is shown here:
' get the argumentsSet objNamed = WScript.Arguments.Namedif objNamed.Count = 0 ThenWScript.Echo "type cscript event2a.vbs /help:? to get the parameters"WScript.QuitEnd ifstrComputer = objNamed.Item("computer")if strComputer = "" ThenWScript.Echo "computer is required"WScript.Echo "type cscript event2a.vbs /help:? to get the parameters"WScript.Quit End IfstrLog = objNamed.Item("log")if strlog = "" ThenWScript.Echo "log is required"WScript.Echo "type cscript event2a.vbs /help:? to get the parameters"WScript.Quit End IfstrEvent = objNamed.Item("event")date1 = objNamed("date1")date2 = objNamed("date2")help = objNamed("help")userid = objNamed("userid")password = objNamed("password")if help <> "" ThenWScript.Echo "Use as "WScript.Echo "cscript event2a.vbs /computer:""."" /log:system /event:6005 /date1:""03/22/2010"" /date2:""03/23/2010"" "WScript.Echo "cscript event2a.vbs /help:?"WScript.Echo "Parameters: "WScript.Echo "computer - name of remote computer. Use ""."" for local computer"WScript.Echo "log - name of event log to interrogate"WScript.Echo ""WScript.Echo "computer and log ARE REQUIRED"WScript.Echo ""WScript.Echo "event - Event ID to retrieve"WScript.Echo "date1 - starting date to get events for"WScript.Echo "date2 - ending date to get events for"WScript.Echo "userid - userid for a remote machine"WScript.Echo "password - password for a remote machine"WScript.QuitEnd ifif password <> "" ThenSet objLocator = CreateObject("WbemScripting.SWbemLocator")Set objWMIService = objLocator.ConnectServer _ (strComputer, "\root\cimv2", userid, password)ElseSet objWMIService = GetObject("winmgmts:" _ & "{impersonationLevel=impersonate}!\\" _ & strComputer & "\root\cimv2")End If' get time zone offsetSet colTimeZone = objWMIService.ExecQuery _ ("SELECT * FROM Win32_TimeZone")For Each objTimeZone in colTimeZone strBias = objTimeZone.BiasNextif strevent = "*" or strevent = "" Then strQuery = "SELECT * FROM Win32_NTLogEvent WHERE Logfile = '" & strLog & "'"Else strQuery = "SELECT * FROM Win32_NTLogEvent WHERE Logfile = '" & strLog _ & "' AND EventCode = '" & strEvent & "'"End If' create UTC datesif date1 <> "" AND date2 <> "" Then sdate = UTCdate(date1) & "000000.000000-" & Cstr(strBias) edate = UTCdate(date2) & "235959.000000-" & Cstr(strBias) strQuery = strQuery & " AND TimeWritten > '" & sdate & "'" _ & " AND TimeWritten < '" & edate & "'"End If'Wscript.Echo strQuery'Wscript.Quit Set colEvents = objWMIService.ExecQuery(strQuery)For Each objEvent in colEvents Wscript.Echo "Computer Name:" & objEvent.ComputerName Wscript.Echo "Event Code:" & objEvent.EventCode Wscript.Echo "Message:" & objEvent.Message Wscript.Echo "Record Number:" & objEvent.RecordNumber Wscript.Echo "Source Name:" & objEvent.SourceName Wscript.Echo "Time Written:" & WmiDate(objEvent.TimeWritten) Wscript.Echo "Event Type:" & objEvent.EventType Wscript.Echo "User:" & objEvent.UserNext' produces date as mm/dd/yyyyyFunction WmiDate(startdate) WmiDate = Mid(startdate, 5, 2) & "/" _ & Mid(startdate, 7, 2) & "/" & Left (startdate, 4) & " " _ & Mid(startdate, 9, 2) & ":" _ & Mid(startdate, 11, 2) & ":" _ & Mid(startdate, 13, 2) End FunctionFunction UTCdate(indate) UTCdate = Year(indate) smonth = Month(indate) if Len(smonth) = 1 Then smonth = "0" & smonth End if sday = Day(indate) if Len(sday) = 1 Then sday = "0" & sday End if UTCdate = UTCdate & smonth & sdayEnd Function
This compares directly to this code as seen previously:
$fht += @{EndTime = $lastdate} } if ($firstdate -and $lastdate){ if ($lastdate -lt $firstdate){Throw "Last date before first date"} else{ $fht += @{StartTime = $firstdate; EndTime = $lastdate} } } if ($cred){ $crd = Get-Credential $s = {Get-WinEvent -FilterHashtable $fht -ComputerName $computer -Credential $crd} } else { $s = {Get-WinEvent -FilterHashtable $fht -ComputerName $computer} } if ($pattern){ Invoke-Command $s | Where {$_.Message -match $pattern} } else { Invoke-Command $s }}
Starting at the top of the script, we get the arguments for the script. All of the arguments are named, so we want the WScript.Arguments.Named object. If no arguments are returned, we return an error message that tells the user how to get the help information.
The first argument is the computer. Again, if it is blank, we remind the user it is required. A similar code block checks the log name.
The other arguments are retrieved. I’ve named them to be obvious about what they contain. If help has been asked for, we display the information. By now I’m getting tired of typing WScript.Echo.
If the password is not blank (userid could be checked as well—ideally, you should check both non-blank if one is), we use the SWbemLocator to connect to the remote machine. This is the only way to connect using alternative credentials. If your current credentials give you permissions or you are connecting to the local machine, you don’t need the userid/password so we use the normal winmgmts way to connect to WMI.
The time zone is acquired. I’ve assumed all machines are in the same time zone, but different time zones could be factored in if needed. The offset from UTC is derived as the bias.
We build the query string depending on the parameters and after converting the dates to UTC format, add them to the query string. We can then run our query and iterate through the results to display to the screen.
We have already seen the function to turn a UTC date into a readable date. The second function takes the input date and turns it into UTC format. The date is reformatted to be YYYYMMDD with the slashes stripped out. The times are added for start of day and end of day as appropriate, and we add the time zone bias. The times are added when we use the dates rather than in the function.
An example of running the script is shown in the following image.
Two approaches to the same problem. So what did I learn from doing this?
1. I wouldn’t go back to VBScript after four years of Windows PowerShell.
2. Windows PowerShell does a lot more for you.
3. It reminded me why scripting was a minority activity among administrators. If Windows PowerShell achieves nothing else, the opening up of automation and scripting to a wider audience is a great move forward.
If you want to know exactly what we will be looking at tomorrow, follow us on Twitter or Facebook. If you have any questions, send e-mail to us at scripter@microsoft.com or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
Ed Wilson and Craig Liebendorfer, Scripting Guys