Event 2 is in the books and with that, it is time to take a look at all of the scripts submitted and make the difficult decisions as to which ones I liked and which ones I didn’t quite like. Remember, just because a script landed on my ‘Not so Favorite’ list doesn’t mean it was terrible. It was just that I felt that there were some things here and there that could have been looked at a little differently.
So without further ado, lets dig into the submissions!
Advanced Category – Not So Favorite Submission
<# .SYNOPSIS This script runs an inventory of OS, CPU cores and installed RAM on a list of computers .DESCRIPTION . .PARAMETER FilePath The path to the computer list text file .EXAMPLE .\inventory.ps1 C:\Computernames.txt .NOTES Author: Posh_London Date: May 2013 #> # == Define params == # param([Parameter(Mandatory=$true)][string]$filepath) $output = @() $Servers = gc $filepath $Servers | % { $processor = gwmi win32_processor -computername $_ $os = gwmi win32_operatingsystem -computername $_ $m = gwmi win32_computersystem -computername $_ $memGB = $m.totalphysicalmemory/1gb $memory = [math]::round($memGB) $osout = $os.caption $cores = $processor.numberofcores $servername = $m.name $output +="$ServerName $osout $cores CPU Cores $memory GB RAM" $output +="`n" } write-host $output -foregroundcolor "yellow"
- Line 1: Help text is lacking good examples and a Description
- Line 19: FilePath is not really a good choice for Parameters, especially when you might want to pass Computernames through to it. Computername would be the better choice in this case.
- Line 19: Lack of pipeline support for this really hurts the submissions ability to pass content from a text file through to the script.
- Really should be a function if in the Advanced category; makes it more of a usable tool
- Line 19: Instead of using Mandatory for the parameter, consider setting a default value such as $Env:Computername
- Line 22,23: Alias are bad news! Don’t use them
- Line 23-37: No object at all is being outputted at all! In fact, you can’t even output this to a file because Write-Host is being used or do things such as sorting or manipulating of the objects. Recommend that the user read up outputting objects using New-Object to create custom objects that can then be exported to files or manipulated.
Advanced Category – Favorite Submission
<# .SYNOPSIS Get inventory data for specified computer system. .DESCRIPTION Get inventory data for provided host using wmi. Data proccessing use multithreading and support using timeouts in case of wmi problems. Target computer system must be reacheble using ICMP Echo. Provide ComputerName specified by user and HostName used by OS. Also provide OS version, CPU and memory info. .PARAMETER ComputerName Specifies the target computer for data query. .PARAMETER ThrottleLimit Specifies the maximum number of WMI operations that can be executed simultaneously .PARAMETER Timeout Specifies the maximum time in second command can run in background before terminating this thread. .PARAMETER ShowProgress Show progress bar information .EXAMPLE PS > Get-AssetInfo -ComputerName test1 ComputerName : hp-test1 OSCaption : Microsoft Windows 8 Enterprise Memory : 5,93 GB Cores : 2 Sockets : 1 Description ----------- Query information ablout computer test1 .EXAMPLE PS > Get-AssetInfo -ComputerName test1 -Credential (get-credential) | fromat-list * -force ComputerName : hp-test1 OSCaption : Microsoft Windows 8 Enterprise OSVersion : 6.2.9200 Cores : 2 OSServicePack : 0 Memory : 5,93 GB Sockets : 1 PSComputerName : test1 Description ----------- Query information ablout computer test1 using alternate credentials .EXAMPLE PS > get-content C:\complist.txt | Get-AssetInfo -ThrottleLimit 100 -Timeout 60 -ShowProgress Description ----------- Query information about computers in file C:\complist.txt using 100 thread at time with 60 sec timeout and showing progressbar .NOTES Required: Powershell 2.0 Info: WMI prefered over CIM as there no speed advantage using cimsessions in multitheating against old systems. #> function Get-AssetInfo { [CmdletBinding()] Param ( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Position=0)] [ValidateNotNullOrEmpty()] [Alias('DNSHostName','PSComputerName')] [string[]] $ComputerName, [Parameter(Position=1)] [ValidateRange(1,65535)] [int32] $ThrottleLimit = 32, [Parameter(Position=2)] [ValidateRange(1,65535)] [int32] $Timeout = 120, [Parameter(Position=3)] [switch] $ShowProgress, [Parameter(Position=4)] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty ) Begin { Write-Verbose -Message 'Creating local hostname list' $IPAddresses = [net.dns]::GetHostAddresses($env:COMPUTERNAME) | Select-Object -ExpandProperty IpAddressToString $HostNames = $IPAddresses | ForEach-Object { try { [net.dns]::GetHostByAddress($_) } catch { # We do not care about errors here... } } | Select-Object -ExpandProperty HostName -Unique $LocalHost = @('', '.', 'localhost', $env:COMPUTERNAME, '::1', '127.0.0.1') + $IPAddresses + $HostNames Write-Verbose -Message 'Creating initial variables' $runspacetimers = [HashTable]::Synchronized(@{}) $runspaces = New-Object -TypeName System.Collections.ArrayList $bgRunspaceCounter = 0 Write-Verbose -Message 'Creating Initial Session State' $iss = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() foreach ($ExternalVariable in ('runspacetimers', 'Credential', 'LocalHost')) { Write-Verbose -Message "Adding variable $ExternalVariable to initial session state" $iss.Variables.Add((New-Object -TypeName System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList $ExternalVariable, (Get-Variable -Name $ExternalVariable -ValueOnly), '')) } Write-Verbose -Message 'Creating runspace pool' $rp = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool(1, $ThrottleLimit, $iss, $Host) $rp.Open() Write-Verbose -Message 'Defining background runspaces scriptblock' $ScriptBlock = { [CmdletBinding()] Param ( [Parameter(Position=0)] [string] $ComputerName, [Parameter(Position=1)] [int] $bgRunspaceID ) $runspacetimers.$bgRunspaceID = Get-Date if (Test-Connection -ComputerName $ComputerName -Quiet -Count 1 -ErrorAction SilentlyContinue) { try { Write-Verbose -Message "WMI Query: $ComputerName" $WMIHast = @{ ComputerName = $ComputerName ErrorAction = 'Stop' } if ($LocalHost -notcontains $ComputerName) { $WMIHast.Credential = $Credential } $WMICompSystem = Get-WmiObject @WMIHast -Class Win32_ComputerSystem $WMIOS = Get-WmiObject @WMIHast -Class Win32_OperatingSystem $WMIProc = Get-WmiObject @WMIHast -Class Win32_Processor if (@($WMIProc)[0].NumberOfCores) #Modern OS { $Sockets = @($WMIProc).Count $Cores = ($WMIProc | Measure-Object -Property NumberOfLogicalProcessors -Sum).Sum } else #Legacy OS { $Sockets = @($WMIProc | Select-Object -Property SocketDesignation -Unique).Count $Cores = @($WMIProc).Count } #region Create custom output object #Due to some bug setting scriptblock directly as value can cause 'NullReferenceException' in v3 host $MethodOptions = @{ Name = 'ToString' MemberType = 'ScriptMethod' PassThru = $true Force = $true Value = [ScriptBlock]::Create(@" "{0:N1} {1}" -f @( switch -Regex ([math]::Log(`$this,1024)) { ^0 { (`$this / 1), ' B' } ^1 { (`$this / 1KB), 'KB' } ^2 { (`$this / 1MB), 'MB' } ^3 { (`$this / 1GB), 'GB' } ^4 { (`$this / 1TB), 'TB' } default { (`$this / 1PB), 'PB' } } ) "@ ) } $myObject = New-Object -TypeName PSObject -Property @{ 'PSComputerName' = $ComputerName 'ComputerName' = $WMICompSystem.DNSHostName 'OSCaption' = $WMIOS.Caption 'OSServicePack' = $WMIOS.ServicePackMajorVersion 'OSVersion' = $WMIOS.Version 'Memory' = $WMICompSystem.TotalPhysicalMemory | Add-Member @MethodOptions 'Cores' = $Cores 'Sockets' = $Sockets } $myObject.PSObject.TypeNames.Insert(0,'My.Asset.Info') $defaultProperties = @('ComputerName','OSCaption', 'Memory', 'Cores', 'Sockets') $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet(‘DefaultDisplayPropertySet’,[string[]]$defaultProperties) $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet) $myObject | Add-Member MemberSet PSStandardMembers $PSStandardMembers #endregion Write-Output -InputObject $myObject } catch { Write-Warning -Message ('{0}: {1}' -f $ComputerName, $_.Exception.Message) } } else { Write-Warning -Message ("{0}: Unavailable" -f $ComputerName) } } function Get-Result { [CmdletBinding()] Param ( [switch]$Wait ) do { $More = $false foreach ($runspace in $runspaces) { $StartTime = $runspacetimers.($runspace.ID) if ($runspace.Handle.isCompleted) { Write-Verbose -Message ('Thread done for {0}' -f $runspace.IObject) $runspace.PowerShell.EndInvoke($runspace.Handle) $runspace.PowerShell.Dispose() $runspace.PowerShell = $null $runspace.Handle = $null } elseif ($runspace.Handle -ne $null) { $More = $true } if ($Timeout -and $StartTime) { if ((New-TimeSpan -Start $StartTime).TotalSeconds -ge $Timeout -and $runspace.PowerShell) { Write-Warning -Message ('Timeout {0}' -f $runspace.IObject) $runspace.PowerShell.Dispose() $runspace.PowerShell = $null $runspace.Handle = $null } } } if ($More -and $PSBoundParameters['Wait']) { Start-Sleep -Milliseconds 100 } foreach ($threat in $runspaces.Clone()) { if ( -not $threat.handle) { Write-Verbose -Message ('Removing {0} from runspaces' -f $threat.IObject) $runspaces.Remove($threat) } } if ($ShowProgress) { $ProgressSplatting = @{ Activity = 'Getting asset info' Status = '{0} of {1} total threads done' -f ($bgRunspaceCounter - $runspaces.Count), $bgRunspaceCounter PercentComplete = ($bgRunspaceCounter - $runspaces.Count) / $bgRunspaceCounter * 100 } Write-Progress @ProgressSplatting } } while ($More -and $PSBoundParameters['Wait']) } } Process { foreach ($Computer in $ComputerName) { $bgRunspaceCounter++ $psCMD = [System.Management.Automation.PowerShell]::Create().AddScript($ScriptBlock).AddParameter('bgRunspaceID',$bgRunspaceCounter).AddParameter('ComputerName',$Computer) $psCMD.RunspacePool = $rp Write-Verbose -Message ('Starting {0}' -f $Computer) [void]$runspaces.Add(@{ Handle = $psCMD.BeginInvoke() PowerShell = $psCMD IObject = $Computer ID = $bgRunspaceCounter }) Get-Result } } End { Get-Result -Wait if ($ShowProgress) { Write-Progress -Activity 'Getting asset info' -Status 'Done' -Completed } Write-Verbose -Message "Closing runspace pool" $rp.Close() $rp.Dispose() } }
- Excellent use of help. Shows multiple examples of using this function and clearly defines parameters
- Line 58-69: Great use of parameter attributes to handle various situations. I like the use of [Alias()] with the ByPropertyName attribute for the pipeline. This allows you to pipe a call from Get-ADComputer directly into this function and it will chug right along to perform the query.
- Line 82,83: Great use of handling the Credential parameter. By doing it this way, you have a few ways to handle the Credential input (domain\username, username, pscredential object or just use the –Credential param and allow it to prompt for input.
- Use of [net.dns]::GetHostByAddress() was unnecessary as the WMI call would return a usable hostname for the object output.
- I will give kudos to the use of custom runspaces to make a more efficient query. Great job at handling the runspaces and the implementation of a timer to handle runspaces that might be hung up.
- Handling of the credential against local system issue was nicely done to avoid issues.
- Use of splat tables to pass into cmdlet is great.
- Line 206-210: Smart use of enhancing the output of the object. Haven’t seen most of this done before.
Beginner Category – Not So Favorite Submission
#Pulls IPs from txt file. Gets Machine Name, Number of Physical CPUs, Number of cores per CPU, RAM in GBs, and Windows OS install ipcsv C:\ScriptingGames\Event2\IPList.txt | gwmi Win32_ComputerSystem | Format-List Name,NumberOfProcessors, NumberOfLogicalProcessors, @{Name="RAM"; Expression={[math]::round($($_.TotalPhysicalMemory/1GB), 2)}} ; gwmi win32_OperatingSystem |Format-list name
Code formatted so it can be viewed better
- This isn’t a CSV file, so I am unsure as to why Import-Csv is being used for the list of IPs.
- Get-WMIObject doesn’t support pipeline input at all. Check Get-Help Get-Wmiobject –Parameter Computername. You can also check out my article on using Trace-Command to troubleshoot pipeline issues here. This causes the line to fail instantly. Instead, use a ForEach loop and use the –Computername parameter with the current item in the loop.
- Format-List is a no-no. Once you use a Format* cmdlet, you have lost all ability to use this object for anything such as sorting, outputting to a file, etc… See this article for more information.
- A better approach would have been to save the output of each WMI call to variables and then use New-Object to output a custom object that lists all of the information as well as allowing the use to pipe the object to another cmdlet or export to a file.
- The second pipe to Get-WMIObject will fail pretty much like the first; again this should have been used with the –Computername parameter and saved to a variable for future object outputting.
Beginner Category – Favorite Submission
Get-Content -LiteralPath 'C:\IPLIST.txt' | ForEach-Object { (Get-WmiObject -Class "Win32_ComputerSystem" -ComputerName $_ | Select-Object -Property Name,@{n="Total Memory (GB)";e={$_.TotalPhysicalMemory / 1GB}}, @{n="Cores";e={$_.NumberOfLogicalProcessors}},@{n="Processors";e={$_.NumberOfProcessors}} | Add-Member -Name "OS Version" -Value $(Get-WmiObject -Class Win32_OperatingSystem -ComputerName $_ | Select-Object -ExpandProperty Caption) -MemberType NoteProperty -PassThru) }
Code formatted so it can be viewed better
- Great use of a one-liner approach to solve this issue
- Like the use of ForEach-Object to iterate through each of the items in the text file.
- Custom objects with the Select-Object @{n=’name’;e={$_}} allows for object output that can be sorted, exported to a file, etc…
- Clever use of Add-Member to handle the Caption property in the current object
That’s it for Event 2! I will say that it was very tough deciding on which submissions I wanted as ‘Favorite’ as there were some great submissions, which meant that I really had to be picky about what I liked. Same for the ‘Not So Favorites’, it is never easy to find one that I just don’t like as much as another, but it goes with the territory.
I always appreciate feedback on how you think I did with picking the submissions. Disagree with my decisions? Let me know! I am just one person out of many and as we all know, not everyone has the same opinions on what they think is a better or not so good solution. As long as we can provide a learning experience, then we all win in this competition!
Pingback: Scripting Games 2013: Event 2 Notes | PowerShell.org
Pingback: Scripting Games 2013: Event 2 Notes | Learn Powershell | Achieve More
Pingback: Scripting Games 2013: Event 2 ‘Favorite’ and ‘Not So Favorite’ | PowerShell.org