Event 2: My way…

Now that event 2 is closed for new entries – time to let you guys know how I would do it. Again: feel free to complain, comment, judge. I hope I will show you few tricks here and there that will help you with next event, or at least: will help you understand what you might have done wrong. Not that I’m assuming you did, I just noticed few gotchas in this scenario that I wanted to addresses. Have I succeeded? As you probably already know, there is no definite answer to that question. Uśmiech


“Why?” answer first

I made some code decisions and before you dig into code I wanted to show you why I made those decisions in the first place. Have you noticed that description mentioned Windows 2000 server? And that it was very vague on whole processors count thing? There is a reason for that. Basically, most of the information you needed was available via WMI in Win32_ComputerSystem class. But if you did your homework you should know, that this class evolved. You can find more details in few places:

I guess you know by know why description was so unclear (at least: that was my impression, I’m not in author’s head or anything Puszczam oczko). Now – to my solutions.

Beginners next

I must say that it was tougher than the first one for me. If you read first part – you should know why. You really had to check for that potentially missing properties in Win32_ComputerSystem and react. Also: you couldn’t assume that based on OS version some properties will be missing: if someone applied above mentioned hot-fix to Windows Server 2003 you would miss opportunity to take more data. What I settled on is:

Get-WmiObject -ComputerName (Get-Content C:\IPList.txt) -Class Win32_ComputerSystem |            
    ForEach-Object {            
        if ($LogicalProcessors = $_.NumberOfLogicalProcessors) {            
            $Processors = $_.NumberOfProcessors            
        } else {            
            $Processors = 'N/A'            
            $LogicalProcessors = $_.NumberOfProcessors            
        }            
            
        $OS = $_.GetRelated('Win32_OperatingSystem').Version            
            
        New-Object PSObject -Property @{            
            Name = $_.Name            
            Processors = $Processors            
            LogicalProcessors = $LogicalProcessors            
            OSVersion = $OS            
            PhysicalMemory = $_.TotalPhysicalMemory  |            
                Add-Member -MemberType ScriptMethod -Value {            
                    "{0:N2} GB" -f ($this / 1gb)            
                } -PassThru -Name ToString -Force            
            
        }            
    }            

I don’t want to use Get-WmiObject twice, so I just use GetRelated method to read Win32_OperatingSystem on remote box for OS version. I also use technique that makes code slightly more concise: assign value to variable inside “if” statement. If right side is $null, or expression on the right will result in an error – you are going straight to “else”. If not, part of job is done, so you just do second part. Finally, trick I picked up from one of attendees last year – override ToString method to get nicer report on memory (in GB) without turning that property into string (you still can sort on it).

Finally: advanced…

With advanced I decided to do few extra things that are maybe not necessary, but I felt they may be useful in the tool like that. First of all I like if my custom objects produced by advanced function look nice. Number of options that can get you there is different depending on PowerShell version you use. I’m using v3 all the time, so I feel urge to add OutputType matadata to any command that produces some output, even if that’s only custom “made up” types. For that to work smoothly though we need to add some information into Extended Type System (ETS).  Another part was credentials: scenarios says that we have admin credentials, but I decided that it may be handy to provide option to specify this credentials as alternate credentials, rather than run whole PowerShell with that privileged account. Those two pieces together (plus meeting requirement for pipeline support) formed my param block (and it’s decoration):

[OutputType('WMI.Inventory')]            
param (            
                
    # Name of computer to be invetoried.            
    [Parameter(            
        ValueFromPipeline = $true,            
        ValueFromPipelineByPropertyName = $true,            
        Mandatory = $true            
    )]            
    [Alias('CN','Name')]            
    [string]$ComputerName,            
            
    # Optional alternate credentials            
    [Management.Automation.PSCredential]            
    [Management.Automation.Credential()]            
    $Credential = [Management.Automation.PSCredential]::Empty            
)            

Next, begin block. I usually don’t do that, but here I decided to create a filter. Feature, that didn’t live up to it’s expectations and was slowly forgotten. Here however I would like to use it instead of gigantic Foreach-Object scriptblock:

filter ConvertTo-WmiInventory {            
        if ($LogicalProcessors = $_.NumberOfLogicalProcessors) {            
            $Processors = $_.NumberOfProcessors            
        } else {            
            $Processors = 'N/A'            
            $LogicalProcessors = $_.NumberOfProcessors            
        }            
            
        $OS = $_.GetRelated('Win32_OperatingSystem').Version            
            
        $Out = New-Object PSObject -Property @{            
            Name = $_.Name            
            Processors = $Processors            
            LogicalProcessors = $LogicalProcessors            
            OSVersion = $OS            
            PhysicalMemory = $_.TotalPhysicalMemory  |            
                Add-Member -MemberType ScriptMethod -Value {            
                    "{0:N2} GB" -f ($this / 1gb)            
                } -PassThru -Name ToString -Force            
            
            }            
        $Out.PSTypeNames.Insert(0,'WMI.Inventory')            
        $Out                 
    }            

As you can see this filter will combine data from remote WMI repository and produce object of pseudo-type we’ve defined in OutputType attribute. What next? We have some common WMI options that we plan to use, so we can define them here too:

$WmiOptions = @{            
        Class = 'Win32_ComputerSystem'             
        ErrorAction = 'Stop'            
    }            

With all that preparations done, we have veeery short process block to create:

try {            
        Get-WmiObject @PSBoundParameters @WmiOptions |            
            ConvertTo-WmiInventory            
    } catch {            
        Write-Warning ("Issue with '{0}': '{1}'" -f             
            $ComputerName, $_.Exception.Message)            
    }            

But function is not everything we need. I assume this is “library” type of script (one that you would dot-source) so next step will be defining and adding formatting data that we need. I would do that in v2 only – in v3 we can do it slightly different way. With much less effort, but unfortunately – with some things we need to drop from view. But first let’s create some ps1xml file on-the-fly and apply it to current session:

$FormatPs1Xml = [Io.Path]::GetTempFileName() |             
    Rename-Item -PassThru -NewName {            
        $_ -replace '\.\w+','.format.ps1xml'            
    }             
            
Set-Content -Path $FormatPs1Xml.FullName -Value @"
<?xml version="1.0" encoding="utf-16"?>
<Configuration>
<ViewDefinitions>
<View>
    <Name>WMI.Inventory</Name>
    <ViewSelectedBy>
        <TypeName>WMI.Inventory</TypeName>
    </ViewSelectedBy>
    <TableControl>
        <TableHeaders>
$(foreach ($Width in 30, 10, 15, 12, 18) {
      @'

            <TableColumnHeader>
                <Width>{0}</Width>
            </TableColumnHeader>
'@ -f $Width
})
        </TableHeaders>
        <TableRowEntries>
            <TableRowEntry>
                <TableColumnItems>
$(foreach ($Item in 'Name', 'OSVersion', 
    'PhysicalMemory', 'Processors', 'LogicalProcessors') {
        @'

                    <TableColumnItem>
                        <PropertyName>{0}</PropertyName>
                    </TableColumnItem>
'@ -f $Item
          
})
                </TableColumnItems>
            </TableRowEntry>
        </TableRowEntries>
    </TableControl>
</View>
</ViewDefinitions>
</Configuration>
"@            
            
Update-FormatData -AppendPath $FormatPs1Xml.FullName            

As you can see – I create temporary file, rename it to name with proper extension for formatting files (Update-FormatData is kind of picky on that), generate necessary XML (without repeating stuff that loop can repeat for me) and finally – I update my format data. End result:

PowerShell-Formatting-with-ps1xml-files

As you can see – I simply reused file I’ve created for beginner event.

v3: better, simpler v2

Now few things that I would do differently in last part if I would target PowerShell v3. I won’t go into details on function part – there are many changes possible there. What did change in PowerShell v3 that suddenly ps1xml files are no longer so valid for nice custom objects? For me it’s Update-TypeData than now supports inline operations:

Update-TypeData -DefaultDisplayPropertySet @(            
    'Name'            
    'OSVersion'            
    'PhysicalMemory'            
    'LogicalProcessors'            
) -TypeName WMI.Inventory -Force            

I just defined special property set for my custom type with single line of code (turned into few lines just to make blog readable Puszczam oczko). This property set will tell PowerShell what is the order of my properties (honestly, with v3 I would take care of order in the function itself) and – what is more important, limits shown properties to four that I care about the most. This way I will get table rather than list by default:

PowerShell-Formatting-with-special-property

Finally, we need to take advantage of OutputType we defined earlier. I don’t know of better way than updating our pseudo-type with same list of properties we actually have in it once it leaves our function:

$Note = @{            
    MemberType = 'NoteProperty'            
    Value = $null            
    TypeName = 'WMI.Inventory'            
}            
            
foreach ($Property in @(            
    'Name'            
    'OSVersion'            
    'PhysicalMemory'            
    'Processors'            
    'LogicalProcessors'            
)) {            
    Update-TypeData @Note -MemberName $Property             
}            

Why is that cool/ important/ valuable/ must have for v3 users? Take a look:

PowerShell-Intellisense-Picks-Up-Note-Properties

We could get same effect using ps1xml file (and with better intellisense support) but now we can get it working in few lines of code. Only because we defined OutputType, and told PowerShell what properties our type has. And with that – I’m going straight to Scripting Games page to find some jewels there. Uśmiech Promise to come back to you once I finished reviewing your scripts with some cool ideas I picked up this year!

Advertisements

13 thoughts on “Event 2: My way…

  1. Thanks for sharing your take on this problem. I was unable to make the deadline (failed to notice GMT) for this event, but I’m looking forward to the next one.

    I guess this may be the place to gain clarification and gripe about the details (or lack thereof) with respect to the challenge.

    First, I will say I do take note of the fact that this is intended to be an exercise in group consensus. I am an IT professional and have been writing scripts and batch files as part of my job for over a decade. I also learned programming with basic 30 years ago ‘for fun’, and had computer science in High School. I mention my credentials only to provide context for my perspective.

    While this is called Scripting Games and should not create any pressure, PowerShell scripting is designed for facilitating IT tasks in a working environment where there are real world consequences for failure. I am confused by your – and others’ – lack of sufficient error checking as a result of trying to keep scripts ‘compact’. In a perfect world, all servers indicated in a list of IPs would be running, but that list could be inaccurate, and a server could be offline for hundreds of reasons. The only thing that is safe to assume is that each and every input may be incorrect. Perhaps a minor gripe, but back to my experience on the job – unexpected errors look ugly and can have serious consequences. Perhaps not when simply listing 5 server characteristics, but bad practice leads to bad habits leads to very bad annual reviews.

    Script output type/format – my initial thought would be to make it slick, a sorted CSV that is launched in Excel at the end of the script, with colors and flashing sparkly gifs, but I am sensing that there are no bonus points for style so I’ll keep that in mind for the future. That’s it for the overall gripes…

    As far as the content you’ve collected in your example, is it OK to critique? Using one WMI class better meets the parameters of the challenge and is certainly more efficient, but it produces some questionable output: TotalPhysicalMemory does not equate to an easily interpreted # of GBs. I don’t know about your boss, but if I told mine a computer had 7.8 GB of memory he’d look at me funny, and as easy as it is for you and for me to correlate that WMI property value to the actual installed RAM, I chose to use the win32_physicalmemory class and the combined Capacity property of each object instance to provide an easily divisible by 1GB number The fact that you converted it to GB is evidence enough that the output should be easily consumed. Likewise for OS version – 6.1 is virtually meaningless for anyone who isn’t deep in the trenches, win32_operatingsystem Name provides a friendlier value. And lastly, NumberOfLogicalProcessors may work OK on some systems, but my single processor/dual core system has 4 logical processors – the parameters of the challenge did not ask for hyperthreads. win32_processor NumberOfCores for systems that can report it (a total of combined processor instances), and a count of the number of objects in that class for number of processors is more accurate and complete. IMHO.

    Thank you so much for developing this fun yet practical series of exercises. I am definitely learning a lot, in spite of my fundamentalist views on how to use a script. I would be interested in your views on comprehensive error checking as a sacrifice for the sake of condensed and compact scripts.

    • First of all – thanks for feedback. 🙂 I won’t go too much into details, just highlight few things that are most important from my perspective.

      Re: errors. I haven’t really paid much attention to it here. Probably I should log wrong IPs rather than write info to the screen. And for beginner event – I wouldn’t bother too much with error handling. Why? Because I can read PowerShell error just fine. And I wouldn’t probably use this snippet as tool, because it isn’t tool yet – it’s just single pipeline, with some additional junk to make it look nicer. But I could not agree more: error handling is necessary. That’s why when I write real tools, try-catch is always there and it logs rather than shows what went wrong. 🙂 And BTW: I tested both tools on some “bad” servers, and got exactly what I asked for: PowerShell errors in beginner category, and warnings in advanced.

      Result of the script: I would draw a line between “data” and “form”. In PowerShell creating nice report from any data is simple, and can be universal (meaning: I can create function/ script that will consume any objects and turn them into nice Excel sheet). But first – I need an object. If I focus on report side – I will have to re-write script for any scenario. If you are building it a separate “blocks” you can mix them easily and write code once, and consume it forever. If I want HTML report rather than excel – I can than change it easily by changing:
      cat c:\IPList.txt | Get-ComputerInventory | Out-ExcelReport
      to:
      cat c:\IPList.txt | Get-ComputerInventory | Out-HtmlReport
      instead of rewriting whole tool, or having tool with multiple outputs possible. If you start thinking about it that way – you will save yourself work. And I don’t like to work too hard. 😉 See how PowerShell normally works: instead of few dozens of parameters on Get-Process to support different outputs, you have cmdlet that returns object, and Format-* and *-Object cmdlets to support many different scenarios. That’s my goal with tools I write. Obviously, at the end of the day I often combine them in single “blob” that gets data, create excel report, sends it to my boss – but that’s not this event asked for, so I just stopped at the point I felt I should. Possible I stopped to early… 🙂

      Regarding WMI: You are right, it should be cores, not logical processors. Same with memory: physical it’s not the same as usable, and question was about former. 🙂 Looks like I wanted to get there too easy. 😉 It didn’t paid of because you, my boss (that how I look at it) didn’t got what you asked for. I guess I focused too much on difference between older OSes and new one. Regarding version: I started off with caption, but than decided that if boss asks for version, than version is what he should get. 😉

      Anyways: great feedback, and it’s pity you didn’t make it for the deadline. Looks like your entry would be much more accurate than mine. 🙂

      • Thanks for the response! Re-reading I sound a bit whiny – I apologize for that. I’m really just trying to wrap my head around ‘down and dirty’ vs. ‘what I get paid for’ which are at completely opposite ends of the spectrum. I perceive the challenge to be deliberately vague in order to elicit the greatest variety in entries – rightfully so. If every nuance of the challenge were explicitly detailed, it would be more difficult to differentiate. While my scripts are not necessarily the most efficient, I try to pay attention to every last detail which is not what the challenge is about, so lessons learned. Again, I look forward to the remaining challenges and thanks for providing an innovative educational experience!

      • “Result of the script: I would draw a line between “data” and “form”. In PowerShell creating nice report from any data is simple, and can be universal (meaning: I can create function/ script that will consume any objects and turn them into nice Excel sheet). But first – I need an object. If I focus on report side – I will have to re-write script for any scenario. If you are building it a separate “blocks” you can mix them easily and write code once, and consume it forever. ” <– This!

  2. Pingback: Event 2: My way… | PowerShell.org

  3. Hello,
    thanks for the article. This is a must read for the ones who love nicely “formatted” output. 🙂

    I’d like you to discuss these few points with you:
    1) The usage of the filter is spot on. Imagining the script without it, it would look a lot worse. Instead of the rarely used keyword filter you could use the more common keyword function (with explicitly stated process block) avoiding the foreach-object you mentioned.

    2) Adding to the PSTypeNames is neat trick but it has its downside. You get different output from the Get-Member and GetType(). Which may cause confusion for people who are trying to build on the script. Getting the type by gm and testing it with -is causes an type not found error (or maybe more usual creating custom function that accepts parameter of type WMI.Inventory. Do you think the nicely formatted output out-weights this issue?

    I would love to see a way to inherit new custom type from the psObject without falling back to the .NET.

    3) What is the try catch part supposed to do? (4th example) What I think it does is: Converting all terminating exceptions to warnings and dumping all their info translating it to just text.

    4) When changing the extension you may be also interested in the [io.path]::ChangeExtension() method. (If you are not already aware of it and just trying to use as much powershell as you could because this is a ScriptingGames blogpost 🙂

    • First of all – thanks for taking a time to provide this valuable feedback. I will try to respond to points:
      1) filter: I think this is something many people relatively new to PowerShell never heard of. I know that I can just replace it with function (or for that matter: even with script block) that contains only process, but I’ve used it on purpose just to let people know that “once upon a time…”. 😉
      2) It’s either this, or Add-Type. Don’t know about anything in between. Maybe prefixing it with: PseudoType.DoNotExist.WMI.Inventory would do…? 😉
      3) Well, this is just a way to show how you can handle information error provides. I guess I should think of better way to use catch block next time. Agree: this one hardly makes any sense. In Polish I would say it’s “art for art”.
      4) No, did not know about the method. And no, I probably wouldn’t use it anyway. 😉 I guess [Io.Path]::GetTempFileName() is misleading enough (method suggests it only gets, when in fact it creates…) Still: great to know such method exists. I bet it’s a lot faster than remove-item, so it may be useful for me in scenarios where performance is more important that Scripting Games principles. 😉

      • 1) What I thought was the main point of using the filter was naming that part of the code so you could move it somewhere else and make the code less cluttered. (Also creating better level of abstraction.) I was not trying to educate you naming all possible alternatives, I was just offering an alternative more people heard of that does just that. ( I don’t see you have beard and wear oldschool glasses you don’t need so I guess the main purpose was not being a PowerShell hipster 😀 )
        2) Yeah nice option :))
        3) I have to disagree on this one. It imho defeats the purpose of the structured error handling. You are reducing the exception that may possibly terminate the execution of the script to just a warning and dumping most of its useful info. So instead of getting stack, position from where the exception originated, inner exceptions etc. You get only “Issue with something something: localhost Null pointer exception.” It puts you in position where you most likely need to replicate the error first to actually get the notion of what happened.
        I am not sure if this makes sense, here I wrote about the exception handling for the first event, there you may hopefully get better idea of what I mean: http://powershell.cz/2013/05/06/scriptinggames-2013-exception-handling-event-1/
        4) Ok.

  4. Pingback: Advanced Event 2 – 2013 Scripting Games | Powershell Reflections

  5. I get weird results overriding ToString in V3.

    In V2, I get these expected results ($a = 5, $b = 0.00 GB, 10 – 5 * 1 = 5)

    PS C:\> $a = 5
    PS C:\> $b = 5
    PS C:\>
    PS C:\> $b = $b |
    >> Add-Member -MemberType ScriptMethod -Value {
    >> “{0:N2} GB” -f ($this / 1gb)
    >> } -PassThru -Name ToString -Force
    >>
    PS C:\> $a
    5
    PS C:\> $b
    0.00 GB
    PS C:\> 10 – 5 * 1
    5

    In V3, I get these unexpected results ($a = 0.00 GB, $b = 0.00 GB, 10 – 5 * 1 = 0.00 GB)

    PS C:\> $a = 5
    PS C:\> $b = 5
    PS C:\>
    PS C:\> $b = $b |
    >> Add-Member -MemberType ScriptMethod -Value {
    >> “{0:N2} GB” -f ($this / 1gb)
    >> } -PassThru -Name ToString -Force
    >>
    PS C:\> $a
    0.00 GB
    PS C:\> $b
    0.00 GB
    PS C:\> 10 – 5 * 1
    0.00 GB

    I’ve only gotten these weird results for ints, but not doubles. I haven’t tried with other data types.
    Do you get this problem in V3, or is it just me?

    • I must say I haven’t seen that, but when I tried it now – I get same results as you do. I would suspect this is a bug. BTW: It’s true also for any other member you add to integer that way, ToString() is just hard to oversee… Try:

      $c = 5 |
      Add-Member -NotePropertyMembers @{
      Int = $true
      } -PassThru
      5 | get-member Int

      Luckily, that is not the case with Int64, so you can just cast result to this type and Add-Member to updated value:
      $c = [Int64]5 |
      Add-Member ScriptMethod ToString {
      "Woohoo"
      } -Force -PassThru
      $c
      5
      5gb

      I haven’t noticed it because all machines I’ve tested on had > 2GB RAM, so none of them ended up as int32.

  6. Lots of good information there. In practice I’d have a hard time justifying investing that much work into a script designed for the stated scenario. In real life, that script would only get run in production maybe a half dozen times before those old servers are gone, rendering much of the code obsolete. Creating a custom inventory object type seems dubious considering the number of different properties you can attribute to a server, and the conbinations of those properties you might need to report on for a particular project or assesment, you’ll end up with a “library” of a multitude of these custom inventory objects, each one with a different set of properties for a different reporting scenario.

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