Scripting Games 2013: Event 2 ‘Favorite’ and ‘Not So Favorite’

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"
  1. Line 1: Help text is lacking good examples and a Description
  2. 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.
  3. Line 19: Lack of pipeline support for this really hurts the submissions ability to pass content from a text file through to the script.
  4. Really should be a function if in the Advanced category; makes it more of a usable tool
  5. Line 19: Instead of using Mandatory for the parameter, consider setting a default value such as $Env:Computername
  6. Line 22,23: Alias are bad news! Don’t use them
  7. 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()
    }
}
  1. Excellent use of help. Shows multiple examples of using this function and clearly defines parameters
  2. 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.
  3. 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.
  4. Use of  [net.dns]::GetHostByAddress() was unnecessary as the WMI call would return a usable hostname for the object output.
  5. 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.
  6. Handling of the credential against local system issue was nicely done to avoid issues.
  7. Use of splat tables to pass into cmdlet is great.
  8. 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

  1. This isn’t a CSV file, so I am unsure as to why Import-Csv is being used for the list of IPs.
  2. 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.
  3. 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.
  4. 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.
  5. 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

  1. Great use of a one-liner approach to solve this issue
  2. Like the use of ForEach-Object to iterate through each of the items in the text file.
  3. Custom objects with the Select-Object @{n=’name’;e={$_}} allows for object output that can be sorted, exported to a file, etc…
  4. 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!

About Boe Prox

Microsoft Cloud and Datacenter MVP working as a SQL DBA.
This entry was posted in 2013 Scripting Games Judges Notes, powershell, Scripting Games 2013 and tagged , , . Bookmark the permalink.

3 Responses to Scripting Games 2013: Event 2 ‘Favorite’ and ‘Not So Favorite’

  1. Pingback: Scripting Games 2013: Event 2 Notes | PowerShell.org

  2. Pingback: Scripting Games 2013: Event 2 Notes | Learn Powershell | Achieve More

  3. Pingback: Scripting Games 2013: Event 2 ‘Favorite’ and ‘Not So Favorite’ | PowerShell.org

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s