Summary: Microsoft PFE, Gary Siepser, provides expert commentary for 2012 Scripting Games Advanced Event 3.
Microsoft Scripting Guy, Ed Wilson, is here. Gary Siepser is the expert commentator for 2012 Scripting Games Advanced Event 3.
Gary is a premier field engineer (PFE) in Customer Services and Support (CSS) at Microsoft. He works with customers that have a limited technology focus, and he helps premier support customers get the most from their Microsoft infrastructure. He used to be a messaging PFE, supporting Exchange Server, and he is a Microsoft Certified Master in Exchange 2007. But he has morphed to 100% Windows PowerShell support engagements.
He delivers premier workshops and provides services such as custom-scoped onsite Windows PowerShell knowledge transfer and sample solutions to help customers solve their needs. He is the Windows PowerShell tech lead within his PFE organization, and he focuses on IP development, internal readiness, and Train-the-Trainer (TTT); recruits instructors; and promotes Windows PowerShell throughout PFE and by proxy to customers.
Blog: Gary's $this and that about PowerShell and Exchange
Microsoft Services Premier Support site
When looking at the scenario for this event, it reminded me of "the old days" in my career when I was a system admin, and this sort of thing was something we really did do. This jumped right out at me as a Group Policy or even "old school" account-based log-in script. With all the client machines running Windows 7 or Windows 8 Consumer Preview, utilizing the newer Windows PowerShell scripts in Group Policy is also an option. Bottom line, I think this solution should be a single script that is run and creates the log file. I am only covering the actual script in this solution because this is the Scripting Games and not the "Group Policy Games.” I will assume that getting the policy set up and any possible boot-strapper type of script (for older style log-in scripts) is just plain boring and not script-like.
Whenever I come up against a solution begging for a script, I start with the "If?", and then move on to the "How?" In this case, the question of "if" is really about where to get the required data. WMI jumped out as the likely first choice, and in the end, it was my final choice. Right away, I knew where several of the items of information were, but I had a play around a find a few others. I was able to retrieve enough info to satisfy me from four WMI classes:
- win32_computersystem
- win32_Operatingsystem
- win32_logicaldisk
- win32_printer
I say "enough to satisfy" because I made some sacrifices. For example, I noticed during testing that mapped drives when disconnected don’t show up as instances at all from Win32_logicaldisk. This bugged me a little, because as a sys-admin I might want to know if someone actually does have a persistent mapping—it just happens to be disconnected right now. Thus the dilemma: to use a simple solution that is perhaps not as robust, or to spend more time trying to determine if there is a better solution. As I have heard from the Microsoft product groups on a few occasions, "To ship is to choose." In this case, I decided to go with the clean WMI solution and not spend more time than needed on this script. In the end, I am happy enough with the data I am retrieving to submit this solution for this esteemed audience.
After I determined that I knew where to get the data, the next challenge was the "How?" For me, this is typically the easier part, because when I have that secret sauce (the WMI part), I find wrapping some automation around it easy (after doing it for enough years, it seems easy, but it was not always so). I already knew Windows PowerShell has a few ways to append to a text file, so I went with the Out-File cmdlet and its Append switch. Keep reading to see how I decided to parameterize the use of this switch in the solution.
We all should strive to write self-helping and reusable code when we can, so I tried to follow that principle in this solution. I provided script-level comment-based Help and function-level Help. Also, where it made sense, I parameterized settings to make things more flexible and modular. I created parameters at the script level for the file path (with a default value as called for in the event) and for log file overwrite/append behavior.
My dirty little secret is that the overwrite option was really there for testing the code, so I could more easily clean the file out and run it many times. I decided it could be a cool feature, and I left it in there to share. I do think the way I implemented it was sort of cool. I realized that I wanted opposite behavior of the append switch, so I used the "!" (the bang or not operator) to negate the value of my switch parameter. Then, when calling Out-File, I used the notation where I follow the parameter name with a colon and the value (rather than a space, because a switch won’t work with a space and argument). Mostly switches are used without a value, but they optionally can have it, so that part looks like this:
-Append:(!$OverwriteFile)
Another aspect in my solution that you will notice is that the function I wrote to actually do the data collection outputs a raw custom object. Whenever you pass information from one place to another (in this case from a function to the calling code), you should try to pass real, usable objects that have properties to hold the info you want to pass on.
Generally, you are passing out an array of objects that are of the same type. When you do this, it makes working with those objects far easier and effective. It’s "the Windows PowerShell way." To create the custom object and populate properties at the same time, I simply used the New-Object cmdlet with the Property parameter coupled with a hash table of the values. You'll see it right in the middle of the solution, inside the function. Here is a snippet of just that part:
$outputObject = New-Object PSObject -Property @{
TimeStampofLogEntry = (Get-Date)
UserName = $rawWMIComputer.UserName
ComputerFullName = "$($rawWMIComputer.Name).$($rawWMIComputer.Domain)"
OperatingSystemVersion = $rawWMIOS.Version
OperatingSystemServicePackVersion = $rawWMIOS.ServicePackMajorVersion
DefaultPrinterName = $DefaultPrinter.Name
NetworkDrives = $NetworkDrives
ComputerLastBootTime = $rawWMIOS.ConvertToDateTime($rawWMIOS.LastBootUpTime)
LastBootState = $rawWMIComputer.BootupState
}
There was one other challenge that I think is worth mentioning, and that was how to output and ultimately display the drive mappings. Looking carefully at the example of acceptable output for the event, I decided that I could match it exactly by burying a new custom object inside the property that holds the drive mappings on the main custom object. That is a lot think about, but if you examine the solution, you'll see that I created a pretty simple loop through the drive mapping instances from WMI and populated a custom object. When rendered during the formatting for Out-File, a custom object flattens into a nice hash table structure that is a little odd looking, yet very readable in text. The pertinent snippet from the solution is here:
$NetworkDrives = $rawWMINetDrive | ForEach-Object {New-Object PSObject -Property @{"DriveLetter" = $_.DeviceID ; "Path" = $_.ProviderName} | Select-Object DriveLetter,Path}
Select-Object at the end of the pipe simply controls the order of the properties because, by nature, a hash-table (used to populate the properties) does not preserve an ordered entry.
Most of the remaining script is pretty self-explanatory between the comment-based Help, and the line comments throughout the code. I hope you can pick up a trick or two from this solution—but most of all, I cannot wait to see all the solutions that are submitted by contestants to see new and completely creative solutions for this event. Here is a screenshot of the log file that my snippet creates:
Here is the actual solution:
<#
.Synopsis
This script creates a log file with diagnostic related information.
.Description
This script uses a function to collect various pieces of information
from WMI in order to create an ongoing log file for diagnostic
purposes.
The following details are collected and logged:
- Time Stamp of Log Entry
- User Name
- Computer Full Name
- Operating System Version
- Operating System Service Pack Version
- Default Printer Name
- Network Drives
- Computer Last Boot Time
- Last Boot State
.PARAMETER OutputFile
The OutputFile parameter allows the specification of the log file
name to which the log information is written. By default log
information is appended, though the file can be overwritten
using the OverwriteFile parmater. If any part of the folder path
is missing, it will be created (assuming it has permission).
Default File Locaiton: $Env:SystemDrive\Logonlog\logonstatus.txt
.PARAMETER OverwriteFile
The OverwriteFile paramter allows the log file to be overwritten,
thus clearing it out. The default behavior is the append to the
end of the file
.Example
PS C:\> c:\scriptfile.ps1
This example runs the script with default settings which will append
to the log file and write in the default location, in this case that
would be c:\Logonlog\logonstatus.txt. The folder Logonlog will be
created if not present.
.EXAMPLE
PS C:\> c:\scriptfile.ps1 -outputfile c:\logs\logfile.txt -overwritefile
This example uses a specified path to the log file and overwrites the file
if present (and not read-only, hidder or system).
#>
[CmdletBinding()]
Param(
[string]$OutputFile = "$Env:SystemDrive\Logonlog\logonstatus.txt",
[switch]$OverwriteFile
)
Function Get-LogonInfo
{
<#
.Synopsis
The Get-LogonInfo function collects and outputs diagnostic information
.Description
The Get-LogonInfo function collects and outputs diagnostic information
about a system. The function was designed with the intent of this
information being written to a log file, but as it outputs a custom
object, the output could be used for any purpose.
The following details are collected and output:
- Time Stamp of Log Entry
- User Name
- Computer Full Name
- Operating System Version
- Operating System Service Pack Version
- Default Printer Name
- Network Drives
- Computer Last Boot Time
- Last Boot State
.PARAMETER Computername
The computername paramter allows the specification of a computername
for this function. The default value is the local computer. Since
all information is collected thorugh WMI, it easily can be collected
from a remote machine, though this isnt neccessarily in line with its
inteneded purpose when created.
.Example
PS C:\> Get-LogonInfo
When called in its default form, the function simply outputs a custom object
with all related information
.EXAMPLE
PS C:\> Get-LogonInfo -computername SERVER01
The function is called and will return a custom object with information from
a remote machine, in this case SERVER01
#>
[CmdletBinding()]
Param([string]$Computername = ".")
#Collect WMI classes for log information
$rawWMIComputer = Get-WmiObject win32_computersystem -Property UserName,Name,Domain,BootupState -ComputerName $computername
$rawWMIOS = Get-WmiObject win32_Operatingsystem -Property Version,LastBootUpTime,ServicePackMajorVersion -ComputerName $computername
$rawWMINetDrive = Get-WmiObject win32_logicaldisk -Property deviceid,providername -Filter 'drivetype=4' -ComputerName $computername
$rawWMIDefaultPrinter = Get-WmiObject win32_printer -filter 'default=True' -Property name -ComputerName $computername
#Handle output for no default printer found
If ($rawWMIDefaultPrinter -eq $null)
{
$DefaultPrinter = New-Object PSObject -Property @{Name="No Default Printer Found"}
}
Else
{
$DefaultPrinter = $rawWMIDefaultPrinter
}
#Handle No Mapped Drives and creating array of custom objects for found mapped drives
If ($rawWMINetDrive -eq $null)
{
$NetworkDrives = "No Mapped Drives Found"
}
Else
{
$NetworkDrives = $rawWMINetDrive | ForEach-Object {New-Object PSObject -Property @{"DriveLetter" = $_.DeviceID ; "Path" = $_.ProviderName} | Select-Object DriveLetter,Path}
}
#Create custom object for function output of collected data
$outputObject = New-Object PSObject -Property @{
TimeStampofLogEntry = (Get-Date)
UserName = $rawWMIComputer.UserName
ComputerFullName = "$($rawWMIComputer.Name).$($rawWMIComputer.Domain)"
OperatingSystemVersion = $rawWMIOS.Version
OperatingSystemServicePackVersion = $rawWMIOS.ServicePackMajorVersion
DefaultPrinterName = $DefaultPrinter.Name
NetworkDrives = $NetworkDrives
ComputerLastBootTime = $rawWMIOS.ConvertToDateTime($rawWMIOS.LastBootUpTime)
LastBootState = $rawWMIComputer.BootupState
}
#Create simple array of property names to control ordering on the custom object
$PropertyNamesOrdered = @(
'TimeStampofLogEntry'
'UserName'
'ComputerFullName'
'OperatingSystemVersion'
'OperatingSystemServicePackVersion'
'DefaultPrinterName'
'NetworkDrives'
'ComputerLastBootTime'
'LastBootState'
)
#Apply the ordering to the custom object output
$outputObject = $outputObject | Select-Object $PropertyNamesOrdered
#Return from function and explicity pass the custom object
Return $outputObject
} #End Function Get-LogonInfo
#Ensure that we have the leading folders for the path specific from the param
$Folderpath = Split-Path $OutputFile
If ((Test-Path $Folderpath) -eq $false) {md $Folderpath | Out-Null}
#Finally, run the function to output the custom object, and then use out-file to write the log file
Get-LogonInfo | Out-File $OutputFile -Append:(!$OverwriteFile) -Force -Width 120
Good luck, and most of all, have fun!
~Gary Siepser
The 2012 Scripting Games Guest Commentator Week will continue tomorrow when we will present the scenario for Event 4.
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