The PowerShell Iron Scripter: My solution to prequel puzzle 2

As I mentioned in my previous blog article, each week leading up to the PowerShell + DevOps Global Summit 2018, PowerShell.org will be posting an iron scripter prequel puzzle on their website. As their website states, think of the iron scripter as the successor to the scripting games.

If you haven't done so already, I recommend reading my solution to the Iron Scripter prequel puzzle 1 because some things are glossed over in this blog article that were covered in detail in that previous one.

Prequel puzzle 2 provides you with an older script that looks like it was written back in the VBScript days. It retrieves information about the operating system, memory, and logical disks. This entry is fairly similar to my previous one as it queries all of the remote computers at once using a CIM session which is created with the New-CimSession cmdlet that was introduced in PowerShell version 3.0. Using the CIM cmdlets instead of the older WMI ones allows it to run in PowerShell Core 6.0.

The built-in DriveType enumeration is used for parameter validation and tabbed expansion / intellisense of the DriveType parameter.

prequel2-ironscripter1a.jpg

I also decided to add the operating system ReleaseId which has to be retrieved from the registry.

  1#Requires -Version 3.0
  2function Get-MrSystemInfo {
  3
  4<#
  5.SYNOPSIS
  6    Retrieves information about the operating system, memory, and logical disks from the specified system.
  7
  8.DESCRIPTION
  9    Get-MrSystemInfo is an advanced function that retrieves information about the operating system, memory,
 10    and logical disks from the specified system.
 11
 12.PARAMETER CimSession
 13    Specifies the CIM session to use for this function. Enter a variable that contains the CIM session or a command that
 14    creates or gets the CIM session, such as the New-CimSession or Get-CimSession cmdlets. For more information, see
 15    about_CimSessions.
 16
 17.PARAMETER DriveType
 18    Specifies the type of drive to query the information for. By default, all drive types are returned, but they can be
 19    narrowed down to a specific type of drive such as only fixed disks. The parameter autocompletes based on the built-in
 20    DriveType enumeration.
 21
 22.EXAMPLE
 23     Get-MrSystemInfo
 24
 25.EXAMPLE
 26     Get-MrSystemInfo -DriveType Fixed
 27
 28.EXAMPLE
 29     Get-MrSystemInfo -CimSession (New-CimSession -ComputerName Server01, Server02)
 30
 31.EXAMPLE
 32     Get-MrSystemInfo -DriveType Fixed -CimSession (New-CimSession -ComputerName Server01, Server02)
 33
 34.INPUTS
 35    None
 36
 37.OUTPUTS
 38    Mr.SystemInfo
 39
 40.NOTES
 41    Author:  Mike F Robbins
 42    Website: http://mikefrobbins.com
 43    Twitter: @mikefrobbins
 44#>
 45
 46    [CmdletBinding()]
 47    [OutputType('Mr.SystemInfo')]
 48    param (
 49        [Microsoft.Management.Infrastructure.CimSession[]]$CimSession,
 50
 51        [System.IO.DriveType]$DriveType
 52    )
 53
 54    $Params = @{
 55        ErrorAction = 'SilentlyContinue'
 56        ErrorVariable = 'Problem'
 57    }
 58
 59    if ($PSBoundParameters.CimSession) {
 60        $Params.CimSession = $CimSession
 61    }
 62
 63    $OSInfo = Get-CimInstance @Params -ClassName Win32_OperatingSystem -Property CSName, Caption, Version, ServicePackMajorVersion, ServicePackMinorVersion,
 64                                                 Manufacturer, WindowsDirectory, Locale, FreePhysicalMemory, TotalVirtualMemorySize, FreeVirtualMemory
 65
 66    $ReleaseId = Invoke-CimMethod @Params -Namespace root\cimv2 -ClassName StdRegProv -MethodName GetSTRINGvalue -Arguments @{
 67                 hDefKey=[uint32]2147483650; sSubKeyName='SOFTWARE\Microsoft\Windows NT\CurrentVersion'; sValueName='ReleaseId'}
 68
 69    if ($PSBoundParameters.DriveType) {
 70        $Params.Filter = "DriveType = $($DriveType.value__)"
 71    }
 72
 73    $LogicalDisk = Get-CimInstance @Params -ClassName Win32_LogicalDisk -Property SystemName, DeviceID, Description, Size, FreeSpace, Compressed
 74
 75    foreach ($OS in $OSInfo) {
 76
 77        foreach ($Disk in $LogicalDisk | Where-Object SystemName -eq $OS.CSName) {
 78            if (-not $PSBoundParameters.CimSession) {
 79                $ReleaseId.PSComputerName = $OS.CSName
 80            }
 81
 82            [pscustomobject]@{
 83                ComputerName = $OS.CSName
 84                OSName = $OS.Caption
 85                OSVersion = $OS.Version
 86                ReleaseId = ($ReleaseId | Where-Object PSComputerName -eq $OS.CSName).sValue
 87                ServicePackMajorVersion = $OS.ServicePackMajorVersion
 88                ServicePackMinorVersion = $OS.ServicePackMinorVersion
 89                OSManufacturer = $OS.Manufacturer
 90                WindowsDirectory = $OS.WindowsDirectory
 91                Locale = [int]"0x$($OS.Locale)"
 92                AvailablePhysicalMemory = $OS.FreePhysicalMemory
 93                TotalVirtualMemory = $OS.TotalVirtualMemorySize
 94                AvailableVirtualMemory = $OS.FreeVirtualMemory
 95                Drive = $Disk.DeviceID
 96                DriveType = $Disk.Description
 97                Size = $Disk.Size
 98                FreeSpace = $Disk.FreeSpace
 99                Compressed = $Disk.Compressed
100                PSTypeName = 'Mr.SystemInfo'
101            }
102
103        }
104
105    }
106
107    foreach ($p in $Problem) {
108        Write-Warning -Message "An error occurred on $($p.OriginInfo). $($p.Exception.Message)"
109    }
110
111}

The function shown in the previous example outputs the raw data returned by the cmdlets as a single type of object with a custom name. None of the disk or memory sizes are converted in case the person working with this function wants to use that raw information. The only exception is that locale is converted to decimal instead of returning it as the default hexadecimal value.

Who really wants to see drives or memory returned in bytes or kilobytes when they run a PowerShell command? A types.ps1xml file is used to extend the types of the returned object so the default output is much more user friendly.

  1<Types>
  2    <Type>
  3      <Name>Mr.SystemInfo</Name>
  4      <Members>
  5        <MemberSet>
  6          <Name>PSStandardMembers</Name>
  7          <Members>
  8            <PropertySet>
  9              <Name>DefaultDisplayPropertySet</Name>
 10              <ReferencedProperties>
 11                <Name>ComputerName</Name>
 12                <Name>OSName</Name>
 13                <Name>OSVersion</Name>
 14                <Name>ReleaseId</Name>
 15                <Name>ServicePack</Name>
 16                <Name>OSManufacturer</Name>
 17                <Name>WindowsDirectory</Name>
 18                <Name>LocaleName</Name>
 19                <Name>AvailableRAM(GB)</Name>
 20                <Name>TotalVM(GB)</Name>
 21                <Name>AvailableVM(GB)</Name>
 22                <Name>Drive</Name>
 23                <Name>DriveType</Name>
 24                <Name>Size(GB)</Name>
 25                <Name>FreeSpace(GB)</Name>
 26                <Name>PercentUsed</Name>
 27                <Name>Compressed</Name>
 28              </ReferencedProperties>
 29            </PropertySet>
 30          </Members>
 31        </MemberSet>
 32      </Members>
 33    </Type>
 34    <Type>
 35      <Name>Mr.SystemInfo</Name>
 36      <Members>
 37      <ScriptProperty>
 38          <Name>ServicePack</Name>
 39          <GetScriptBlock>
 40            "$($this.ServicePackMajorVersion).$($this.ServicePackMinorVersion)"
 41          </GetScriptBlock>
 42        </ScriptProperty>
 43      </Members>
 44    </Type>
 45    <Type>
 46      <Name>Mr.SystemInfo</Name>
 47      <Members>
 48        <ScriptProperty>
 49          <Name>LocaleName</Name>
 50          <GetScriptBlock>
 51            ([System.Globalization.CultureInfo]($this.Locale)).Name
 52          </GetScriptBlock>
 53        </ScriptProperty>
 54      </Members>
 55    </Type>
 56    <Type>
 57      <Name>Mr.SystemInfo</Name>
 58      <Members>
 59        <ScriptProperty>
 60          <Name>AvailableRAM(GB)</Name>
 61          <GetScriptBlock>
 62            "{0:N2}" -f ($this.AvailablePhysicalMemory / 1MB)
 63          </GetScriptBlock>
 64        </ScriptProperty>
 65      </Members>
 66    </Type>
 67    <Type>
 68      <Name>Mr.SystemInfo</Name>
 69      <Members>
 70        <ScriptProperty>
 71          <Name>TotalVM(GB)</Name>
 72          <GetScriptBlock>
 73            "{0:N2}" -f ($this.TotalVirtualMemory / 1MB)
 74          </GetScriptBlock>
 75        </ScriptProperty>
 76      </Members>
 77    </Type>
 78    <Type>
 79      <Name>Mr.SystemInfo</Name>
 80      <Members>
 81        <ScriptProperty>
 82          <Name>AvailableVM(GB)</Name>
 83          <GetScriptBlock>
 84            "{0:N2}" -f ($this.AvailableVirtualMemory / 1MB)
 85          </GetScriptBlock>
 86        </ScriptProperty>
 87      </Members>
 88    </Type>
 89    <Type>
 90      <Name>Mr.SystemInfo</Name>
 91      <Members>
 92        <ScriptProperty>
 93          <Name>Size(GB)</Name>
 94          <GetScriptBlock>
 95            "{0:N2}" -f ($this.Size / 1GB)
 96          </GetScriptBlock>
 97        </ScriptProperty>
 98      </Members>
 99    </Type>
100    <Type>
101      <Name>Mr.SystemInfo</Name>
102      <Members>
103        <ScriptProperty>
104          <Name>FreeSpace(GB)</Name>
105          <GetScriptBlock>
106            "{0:N2}" -f ($this.FreeSpace / 1GB)
107          </GetScriptBlock>
108        </ScriptProperty>
109      </Members>
110    </Type>
111    <Type>
112      <Name>Mr.SystemInfo</Name>
113      <Members>
114        <ScriptProperty>
115          <Name>PercentUsed</Name>
116          <GetScriptBlock>
117            "{0:N2}" -f (100 - ($this.FreeSpace / $this.Size * 100))
118          </GetScriptBlock>
119        </ScriptProperty>
120      </Members>
121    </Type>
122</Types>

If you're interested in learning more about types in PowerShell, I recommend taking a look at the About_Types.ps1xml help topic.

Even through I've specified the default properties to return in the types.ps1xml file that was previously listed, I overwrite those defaults in a format.ps1xml file for its table view. I could have also provided a list view in the format.ps1xml file which would have eliminated the need to list the default ones in the types.ps1xml file, but I wanted to show how one of these files could be used to overwrite the other in one scenario, but not another. I've only shown the pertinent portion of the format.ps1xml file in the following example. See my IronScripter repository on GitHub for the entire file and module.

 1<View>
 2    <Name>Mr.SystemInfo</Name>
 3    <ViewSelectedBy>
 4        <TypeName>Mr.SystemInfo</TypeName>
 5    </ViewSelectedBy>
 6    <TableControl>
 7        <TableHeaders>
 8                <TableColumnHeader>
 9                <Width>16</Width>
10            </TableColumnHeader>
11            <TableColumnHeader>
12                <Width>50</Width>
13            </TableColumnHeader>
14            <TableColumnHeader>
15                <Width>18</Width>
16            </TableColumnHeader>
17            <TableColumnHeader>
18                <Width>6</Width>
19            </TableColumnHeader>
20            <TableColumnHeader>
21                <Width>18</Width>
22            </TableColumnHeader>
23        </TableHeaders>
24        <TableRowEntries>
25            <TableRowEntry>
26                <TableColumnItems>
27                    <TableColumnItem>
28                        <PropertyName>ComputerName</PropertyName>
29                    </TableColumnItem>
30                    <TableColumnItem>
31                        <PropertyName>OSName</PropertyName>
32                    </TableColumnItem>
33                    <TableColumnItem>
34                        <PropertyName>AvailableRAM(GB)</PropertyName>
35                    </TableColumnItem>
36                    <TableColumnItem>
37                        <PropertyName>Drive</PropertyName>
38                    </TableColumnItem>
39                    <TableColumnItem>
40                        <PropertyName>FreeSpace(GB)</PropertyName>
41                    </TableColumnItem>
42                </TableColumnItems>
43            </TableRowEntry>
44            </TableRowEntries>
45    </TableControl>
46</View>

As with learning more about types, if you're interested in learning more about modifying the default display of objects in PowerShell, I recommend taking a look at the About_Format.ps1xml help topic.

The types.ps1xml file has to be specified in the TypesToProcess section of the module manifest and the format.ps1xml file must be specified in the FormatsToProcess section. Once again, I'm only showing the relevant portion of the module manifest.

 1# Type files (.ps1xml) to be loaded when importing this module
 2TypesToProcess = 'MrIronScripter.types.ps1xml'
 3
 4# Format files (.ps1xml) to be loaded when importing this module
 5FormatsToProcess = 'MrIronScripter.format.ps1xml'
 6
 7# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
 8# NestedModules = @()
 9
10# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
11FunctionsToExport = 'Get-MrMonitorInfo', 'Get-MrSystemInfo'
12
13# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
14CmdletsToExport = ''
15
16# Variables to export from this module
17# VariablesToExport = @()
18
19# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
20AliasesToExport = ''

Running the function returns a nice looking table view with five properties. Notice the memory and freespace are returned in gigabytes even though the function itself didn't convert those values to gigabytes. That's because those properties are defined as script properties in the types.ps1xml file.

prequel2-ironscripter2a.jpg

Piping to Format-List shows additional properties and their values, but not all of the properties.

prequel2-ironscripter3a.jpg

Piping to Format-List and specifying all properties returns all of them. Certain commands will still hide some of their properties even in this scenario and you'll either need to add the Force parameter or pipe to Select-Object -Property * instead to view all of them along with their values.

prequel2-ironscripter4a.jpg

Piping to Get-Member sheds some light on the different types of properties that are returned by this function.

prequel2-ironscripter5a.jpg

The Get-MrSystemInfo function and the associated files shown in this blog article can be downloaded from my IronScripter repository on GitHub. They can also be installed from the PowerShell Gallery using Install-Module which is part of the PowerShellGet module that ships with PowerShell version 5.0 and higher. PowerShellGet can be downloaded and installed on PowerShell version 3.0 and higher, although the module shown in this blog article requires at least PowerShell version 4.0.

1Install-Module -Name MrIronScripter -Force
2Get-Module -Name MrIronScripter -ListAvailable

prequel2-ironscripter6a.jpg

If you have a previous version installed and want to install the most recent version, Update-Module could be used instead.

1Get-Module -Name MrIronScripter -ListAvailable
2Update-Module -Name MrIronScripter -Force
3Get-Module -Name MrIronScripter -ListAvailable

prequel2-ironscripter7a.jpg

Want to learn how to write PowerShell functions and script modules from a former winner of the advanced category in the scripting games and a multiyear recipient of both Microsoft’s MVP and SAPIEN Technologies’ MVP award? Then you'll definitely want to attend my Writing award winning PowerShell functions and script modules session at the PowerShell + DevOps Global Summit 2018.

µ