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.

#Requires -Version 3.0
function Get-MrSystemInfo {

<#
.SYNOPSIS
Retrieves information about the operating system, memory, and logical disks from the specified system.

.DESCRIPTION
Get-MrSystemInfo is an advanced function that retrieves information about the operating system, memory,
and logical disks from the specified system.

.PARAMETER CimSession
Specifies the CIM session to use for this function. Enter a variable that contains the CIM session or a command that
creates or gets the CIM session, such as the New-CimSession or Get-CimSession cmdlets. For more information, see
about_CimSessions.

.PARAMETER DriveType
Specifies the type of drive to query the information for. By default, all drive types are returned, but they can be
narrowed down to a specific type of drive such as only fixed disks. The parameter autocompletes based on the built-in
DriveType enumeration.

.EXAMPLE
Get-MrSystemInfo

.EXAMPLE
Get-MrSystemInfo -DriveType Fixed

.EXAMPLE
Get-MrSystemInfo -CimSession (New-CimSession -ComputerName Server01, Server02)

.EXAMPLE
Get-MrSystemInfo -DriveType Fixed -CimSession (New-CimSession -ComputerName Server01, Server02)

.INPUTS
None

.OUTPUTS
Mr.SystemInfo

.NOTES
Author:  Mike F Robbins
Website: https://mikefrobbins.com
Twitter: @mikefrobbins
#>

[CmdletBinding()]
[OutputType('Mr.SystemInfo')]
param (
[Microsoft.Management.Infrastructure.CimSession[]]$CimSession,

[System.IO.DriveType]$DriveType
)

$Params = @{
ErrorAction = 'SilentlyContinue'
ErrorVariable = 'Problem'
}

if ($PSBoundParameters.CimSession) {
$Params.CimSession = $CimSession
}

$OSInfo = Get-CimInstance @Params -ClassName Win32_OperatingSystem -Property CSName, Caption, Version, ServicePackMajorVersion, ServicePackMinorVersion,
Manufacturer, WindowsDirectory, Locale, FreePhysicalMemory, TotalVirtualMemorySize, FreeVirtualMemory

$ReleaseId = Invoke-CimMethod @Params -Namespace root\cimv2 -ClassName StdRegProv -MethodName GetSTRINGvalue -Arguments @{
hDefKey=[uint32]2147483650; sSubKeyName='SOFTWARE\Microsoft\Windows NT\CurrentVersion'; sValueName='ReleaseId'}

if ($PSBoundParameters.DriveType) {
$Params.Filter = "DriveType = $($DriveType.value__)"
}

$LogicalDisk = Get-CimInstance @Params -ClassName Win32_LogicalDisk -Property SystemName, DeviceID, Description, Size, FreeSpace, Compressed

foreach ($OS in $OSInfo) {

foreach ($Disk in $LogicalDisk | Where-Object SystemName -eq $OS.CSName) {
if (-not $PSBoundParameters.CimSession) {
$ReleaseId.PSComputerName = $OS.CSName
}

[pscustomobject]@{
ComputerName = $OS.CSName
OSName = $OS.Caption
OSVersion = $OS.Version
ReleaseId = ($ReleaseId | Where-Object PSComputerName -eq $OS.CSName).sValue
ServicePackMajorVersion = $OS.ServicePackMajorVersion
ServicePackMinorVersion = $OS.ServicePackMinorVersion
OSManufacturer = $OS.Manufacturer
WindowsDirectory = $OS.WindowsDirectory
Locale = [int]"0x$($OS.Locale)"
AvailablePhysicalMemory = $OS.FreePhysicalMemory
TotalVirtualMemory = $OS.TotalVirtualMemorySize
AvailableVirtualMemory = $OS.FreeVirtualMemory
Drive = $Disk.DeviceID
DriveType = $Disk.Description
Size = $Disk.Size
FreeSpace = $Disk.FreeSpace
Compressed = $Disk.Compressed
PSTypeName = 'Mr.SystemInfo'
}

}

}

foreach ($p in $Problem) {
Write-Warning -Message "An error occurred on $($p.OriginInfo). $($p.Exception.Message)"
}

}

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.

<Types>
<Type>
<Name>Mr.SystemInfo</Name>
<Members>
<MemberSet>
<Name>PSStandardMembers</Name>
<Members>
<PropertySet>
<Name>DefaultDisplayPropertySet</Name>
<ReferencedProperties>
<Name>ComputerName</Name>
<Name>OSName</Name>
<Name>OSVersion</Name>
<Name>ReleaseId</Name>
<Name>ServicePack</Name>
<Name>OSManufacturer</Name>
<Name>WindowsDirectory</Name>
<Name>LocaleName</Name>
<Name>AvailableRAM(GB)</Name>
<Name>TotalVM(GB)</Name>
<Name>AvailableVM(GB)</Name>
<Name>Drive</Name>
<Name>DriveType</Name>
<Name>Size(GB)</Name>
<Name>FreeSpace(GB)</Name>
<Name>PercentUsed</Name>
<Name>Compressed</Name>
</ReferencedProperties>
</PropertySet>
</Members>
</MemberSet>
</Members>
</Type>
<Type>
<Name>Mr.SystemInfo</Name>
<Members>
<ScriptProperty>
<Name>ServicePack</Name>
<GetScriptBlock>
"$($this.ServicePackMajorVersion).$($this.ServicePackMinorVersion)"
</GetScriptBlock>
</ScriptProperty>
</Members>
</Type>
<Type>
<Name>Mr.SystemInfo</Name>
<Members>
<ScriptProperty>
<Name>LocaleName</Name>
<GetScriptBlock>
([System.Globalization.CultureInfo]($this.Locale)).Name
</GetScriptBlock>
</ScriptProperty>
</Members>
</Type>
<Type>
<Name>Mr.SystemInfo</Name>
<Members>
<ScriptProperty>
<Name>AvailableRAM(GB)</Name>
<GetScriptBlock>
"{0:N2}" -f ($this.AvailablePhysicalMemory / 1MB)
</GetScriptBlock>
</ScriptProperty>
</Members>
</Type>
<Type>
<Name>Mr.SystemInfo</Name>
<Members>
<ScriptProperty>
<Name>TotalVM(GB)</Name>
<GetScriptBlock>
"{0:N2}" -f ($this.TotalVirtualMemory / 1MB)
</GetScriptBlock>
</ScriptProperty>
</Members>
</Type>
<Type>
<Name>Mr.SystemInfo</Name>
<Members>
<ScriptProperty>
<Name>AvailableVM(GB)</Name>
<GetScriptBlock>
"{0:N2}" -f ($this.AvailableVirtualMemory / 1MB)
</GetScriptBlock>
</ScriptProperty>
</Members>
</Type>
<Type>
<Name>Mr.SystemInfo</Name>
<Members>
<ScriptProperty>
<Name>Size(GB)</Name>
<GetScriptBlock>
"{0:N2}" -f ($this.Size / 1GB)
</GetScriptBlock>
</ScriptProperty>
</Members>
</Type>
<Type>
<Name>Mr.SystemInfo</Name>
<Members>
<ScriptProperty>
<Name>FreeSpace(GB)</Name>
<GetScriptBlock>
"{0:N2}" -f ($this.FreeSpace / 1GB)
</GetScriptBlock>
</ScriptProperty>
</Members>
</Type>
<Type>
<Name>Mr.SystemInfo</Name>
<Members>
<ScriptProperty>
<Name>PercentUsed</Name>
<GetScriptBlock>
"{0:N2}" -f (100 - ($this.FreeSpace / $this.Size * 100))
</GetScriptBlock>
</ScriptProperty>
</Members>
</Type>
</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.

<View>
<Name>Mr.SystemInfo</Name>
<ViewSelectedBy>
<TypeName>Mr.SystemInfo</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Width>16</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>50</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>18</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>6</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>18</Width>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>ComputerName</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>OSName</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>AvailableRAM(GB)</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Drive</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>FreeSpace(GB)</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</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.

# Type files (.ps1xml) to be loaded when importing this module
TypesToProcess = 'MrIronScripter.types.ps1xml'

# Format files (.ps1xml) to be loaded when importing this module
FormatsToProcess = 'MrIronScripter.format.ps1xml'

# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
# NestedModules = @()

# 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.
FunctionsToExport = 'Get-MrMonitorInfo', 'Get-MrSystemInfo'

# 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.
CmdletsToExport = ''

# Variables to export from this module
# VariablesToExport = @()

# 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.
AliasesToExport = ''

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.

Install-Module -Name MrIronScripter -Force
Get-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.

Get-Module -Name MrIronScripter -ListAvailable
Update-Module -Name MrIronScripter -Force
Get-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.

µ