The games continue! The second event has closed and voting has begun. There is still time for you to jump in and try your hand at learning Powershell. There is some really good feedback to be had to sharpen your skills.
The second event is based on gathering information about servers that might be eligible for upgrade. You can see the full script requirements for the Advanced track here.
As public voting continues, here is my solution and how I got to this conclusion.
Function Get-ServerReport
{
<#
.SYNOPSIS
Gather computer system information.
.DESCRIPTION
This script will connect to specified computers and gather:
*Computer Name
*Windows Version
*Physical Memory
*Number of Processors
.PARAMETER ComputerName
Specifies the target computer for the report operation. The value can be a fully qualified domain name, a NetBIOS name, or an IP address. To specify the local
computer, use the local computer name, use localhost, or use a dot (.) . The local computer is the default. When the remote computer is in a different domain from the
user, you must use a fully qualified domain name. You can also pipe this parameter value to the Get-ServerReport function.
.PARAMETER Credential
Specifies a user account that has permission to perform this action. The default is the current user. Type a user name, such as "User01", "Domain01\User01", or
User@Contoso.com. Or, enter a PSCredential object, such as an object that is returned by the Get-Credential cmdlet. When you type a user name, you are prompted for a
password.
.EXAMPLE
PS C:\> Get-ServerReport
This command will return the items from the local computer, which is the default value for ComputerName
.EXAMPLE
PS C:\> Get-ServerReport -ComputerName Server01
PhysicalMemory ComputerName Processors WindowsVersion
-------------- ------------ ---------- --------------
2146897920 Server01 2 Microsoft(R) Windows(R) Server 2003, Standard Edition
This command connects and reports on Server01. This assumes that the currently logged on user has access to the specified server.
.EXAMPLE
PS C:\> $credential = Get-Credential CONTOSO\jdoe
PS C:\> Get-ServerReport -ComputerName Server01, Server02 -Credential $credential
PhysicalMemory ComputerName Processors WindowsVersion
-------------- ------------ ---------- --------------
2146897920 Server01 2 Microsoft(R) Windows(R) Server 2003, Standard Edition
4294135808 Server02 2 Microsoft(R) Windows(R) Server 2003, Standard Edition
In this example, two servers are queried and reported. The command is given the specific credential object to allow connection to the remove servers.
.EXAMPLE
PS C:\> Get-Content -Path C:\data\servers.txt | Get-ServerReport
PhysicalMemory ComputerName Processors WindowsVersion
-------------- ------------ ---------- --------------
2146897920 Server01 2 Microsoft(R) Windows(R) Server 2003, Standard Edition
4294135808 Server02 2 Microsoft(R) Windows(R) Server 2003, Standard Edition
8589467648 Server03 2 Microsoft Windows Server 2008 R2 Standard
8589467648 Server04 2 Microsoft Windows Server 2008 R2 Standard
8466395136 Server05 1 Microsoft Windows 7 Enterprise
Server06 ** OFFLINE **
In this example, a list of servers stored in the file at C:\data\servers.txt, is read and piped to the Get-ServerReport function. Server06 could not be contacted for some reason so
the script reports "** OFFLINE **" in the WindowsVersion property.
.INPUTS
System.String[], System.Management.Automation.PSCredential
.OUTPUT
System.Management.Automation.PSCustomObject
.NOTES
Instead of generating an error report or breaking the flow of the data, the script will "inline" an error condition in the WindowsVersion property. If the server is offline (** OFFLINE **)
or access is denied (** ACCESS DENIED **), the indication will be displayed in that column. The Processors and PhysicalMemory properites will be empty in that case.
#>
[CmdletBinding()]
Param(
[Parameter(ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true)]
[String[]]$ComputerName = $ENV:COMPUTERNAME,
[Parameter(Mandatory=$false)]
[System.Management.Automation.PSCredential]$Credential
)
BEGIN
{
}
PROCESS
{
foreach($computer in $ComputerName)
{
Write-Debug "Processing $computer"
$properties = @{
ComputerName=$computer
WindowsVersion=""
PhysicalMemory=""
Processors=""
}
if (Test-Connection -ComputerName $computer -Count 1 -Quiet)
{
Write-Debug "$computer is online."
New-Variable -Name os -Force
New-Variable -Name computerSystem -Force
New-Variable -Name processor -Force
Write-Debug "Variables created."
try
{
if ($credential -and ($computer -ne $ENV:COMPUTERNAME))
{
Write-Debug "Credential object supplied. Not local computer."
$os = Get-WmiObject -ComputerName $computer -Class Win32_OperatingSystem -Property Caption -Impersonation Impersonate -Authentication PacketPrivacy -Credential $credential
$computerSystem = Get-WmiObject -ComputerName $computer -Class Win32_ComputerSystem -Property TotalPhysicalMemory -Impersonation Impersonate -Authentication PacketPrivacy -Credential $credential
$processor = @(Get-WmiObject -ComputerName $computer -Class Win32_Processor -Impersonation Impersonate -Authentication PacketPrivacy -Credential $credential)
}
else
{
Write-Debug "Credential object not supplied or accessing local computer."
$os = Get-WmiObject -ComputerName $computer -Class Win32_OperatingSystem -Property Caption -Impersonation Impersonate -Authentication PacketPrivacy
$computerSystem = Get-WmiObject -ComputerName $computer -Class Win32_ComputerSystem -Property TotalPhysicalMemory -Impersonation Impersonate -Authentication PacketPrivacy
$processor = @(Get-WmiObject -ComputerName $computer -Class Win32_Processor -Impersonation Impersonate -Authentication PacketPrivacy)
}
Write-Debug "Assigning $($os.Caption) to custom object."
$properties.Set_Item("WindowsVersion", "$($os.Caption)")
$properties.Set_Item("PhysicalMemory", $($computerSystem.TotalPhysicalMemory) -as [Int64])
$properties.set_Item("Processors",$($processor.Count) -as [int])
}
catch [System.UnauthorizedAccessException]
{
Write-Debug "Encountered Access Denied error, tagging object."
$properties.set_Item("WindowsVersion", "** ACCESS DENIED **")
}
}
else
{
$properties.set_Item("WindowsVersion","** OFFLINE **")
}
Write-Output -InputObject (New-Object -TypeName PSObject -Property $properties)
}
}
END
{}
}
I am not going to discuss the use of comment-based help for this script. It is there, and if you want to read a really good article on the how’s and why’s of comment-based help, you can check out the official documentation.
The requirements of the advanced event stated we need to be able to pipe computer names into the script. On lines 77-79, you can see where the parameter is declared. Learning from my mistake on the last event, I chose the standard parameter name “ComputerName” for the parameter. Note that I could have (and maybe should have) added an alias tag to allow for other property names that might contain the server name, such as “cn,” “server,” etc. That would allow for more compatibility with other cmdlets that might output the computer name under a different property name.
One additional note, you might notice that the ComputerName parameter is declared as a string array ([string[]]). Doing this allows you to handle input in a variety of ways. The script can handle a single server name passed in with the -ComputerName parameter:
Get-ServerReport -ComputerName Server01
You can pass in a string array of server names:
$servers = Get-Content -Path C:\Temp\Servers.txt
Get-ServerReport -ComputerName $servers
Or you can pipe server names out of another cmdlet:
get-adcomputer -filter * | Get-ServerReport
The second parameter is something I consider to be a best practice. You should be running your computer with an account that has least necessary privileges. (You are, aren’t you?) That means that your user account probably doesn’t have access to the servers that you are trying to interrogate. The credential parameter allows you to pass in a PSCredential object with your admin account.
$cred = get-credential -Username CONTOSO\jdoe
$servers | Get-ServerReport -Credential $cred
Now we get into doing some work with the information we do have. When you start dealing with pipeline input, you have to have a BEGIN/PROCESS/END block. Any code you put in the BEGIN block will be processed one for each time the script is run. This is used to set up a log file, or connections to a database, things like that.
The PROCESS block takes each object that comes in from the pipeline and, well, processes it. This block will run for each object that comes in. There is something interesting that confuses new users to Powershell. If the PROCESS block runs once for each item that comes in, then why do I have a foreach loop inside the PROCESS block? Won’t that handle each of the items? Actually, no. The foreach block is in there on the chance that an array is passed in. If one item is passed in, the foreach will run only once…for the one item that comes in. If an array gets passed, the foreach will enumerate each item in the array to process. So, when pipeline input is received, each object coming in is counted as a single item. Each item causes the PROCESS block to run, and the foreach will iterate once for a single-item array.
Inside the foreach loop, I begin gathering information about each computer and assembling a custom object to be output. I start by creating a hash table to hold the properties I am collecting. The requirements stated that we need the server name (which was provided in the parameters), the installed Windows version, amount of physical RAM, and processor count.
With the hash table created and the computer name populated, I begin by testing if the server is online. The native Powershell command Test-Connection is used similar to the ping.exe command. It is important to use the -Quiet parameter so that the output from this command does not go to the console. All we need is a true or false on the connection result. If the server is online, the Test-Connection will evaluate to true.
Once I have determined the server is online, I new up some variables. One might ask why do you create these variables when they are created as you assign a value to them? The reason is scope. The $computersystem, $os, and $processor variable are not assigned until a little farther down in the if…then statement. Had I let them be assigned as a normal course of the script in the if..then block, then the values of those variables would be lost when the if..then block completed. A variable is generally only known to the part of the script that instantiates it, and any code blocks inside that block. There are exceptions to this rule because you can explicitly set the scope of a variable when you create it. However, you should have a really, really good reason for creating a variable with a larger scope than what it initially has.
The if…then block is interesting. I test for a PSCredential object being passed in and also that the server name is not the name of the local computer. WMI has a little quirk (my term) that you cannot pass a credential parameter to it for a local connection; it just errors out. So to mitigate that error, I check to see if the script is connecting to the local computer. The only difference between the two Get-WmiObject blocks is that one passes the $credential parameter to the Get-WmiObject cmdlets and the other does not. If for some reason there is an access denied error on this block, the script simply assigns a message to the Windows Version property that indicates the error and moves on. Doing this allows the script 1) to alert the user that something was wrong with a particular machine; 2) to be filtered out with a Where-Object cmdlet downstream; and 3) to keep on pumping out data.
OK, so finally, we have some data to report. I am using the set_Item() method of the hash table. I access the properties I need and assign them to the specific hash table key that requires it. In testing, I did find that the “value” argument (the second argument to the set_Item() method) needs to be in quotes. So to make that work, I created a sub-expression [ $($object.property) ] to evaluate the property value out of the object and let the expandable string do its work.
Another point you might notice is that I do not format the data returned; specifically the physical RAM value, and it was a very conscious decision. The idea behind Powershell is to slice and dice data as much or as little as you want. For me to push the data out in a specific unit (GB, MB, etc) would be to force the user to my will. By outputting unformatted numbers, the user can use Select-object cmdlet to process that number with a calculated property. Likewise, with the computer name property, I am outputting the data exactly as it is returned to the script. If the user wants all of the computer names to be in all caps or all lowercase, they can again, use a calculated property in Select-Object to change the format of the output. The bottom line is that unless your script is specifically designed to format something, you should output only objects in the most raw form you are able to (or that makes sense). For one thing, it usually saves you some work, but it also makes it easier for the user to get the data in the form they want or need. If you would like some additional information on calculated properties, you can see a previous article I wrote here.
This brings us to the final part of the script. Using the hash table I created, with whatever values may have been pushed into it, I create the new PSCustomObject passing in that hash table as the properties to use. Once it is created, it is sent to the pipeline by using the Write-Output cmdlet.
There is an END block, as I mentioned, and I included that for form as a visual clue to whomever might be reading this that this is a script that can process pipeline input. It is not necessary to put the END block since there is no code, but to me, it looks incomplete without it. Whenever I see a script with BEGIN/PROCESS/END, I automatically assume we are dealing with pipeline input.
The purpose of the Scripting Games is to help people learn Powershell using real world scenarios and get live feedback from a diverse group of people. This is just one way to solve this problem. You may have another way or other ideas of how my idea could be implemented. If you want to see a truly advanced way to handle this, see this blog article, “Event 2: My way…” by Bartek Bielawski. After seeing his take on this event, I was considerably humbled about my efforts. I certainly learned a lot more than I expected by reading his solution…some things I didn’t even know you could do in Powershell to date.
Anyway, I hope you find the explanation useful. The comment section is open for discussion on my solution. Tell me what you did like or what you didn’t like. As a wise person I spoke to once said, “This point of this is to generate meaningful discussion.” So whether you discuss here or on powershell.org, let’s talk. See you in the next event!
Related Links:
If you like this, let others know...