A Namespace Scripting Solution
In this issue:
- Creating a Basic Query
- Parallel Performance
- Creating a Function
- Extending the Type
- Adding Formatting
- A Bonus Function
- Summary
At the end of last month, I left you with another PowerShell scripting challenge. I wanted you to use PowerShell to enumerate all WMI/CIM Namespaces on a computer. I wanted the output to show the namespace path, the namespace name, and the computer name. This should be the minimum output. A WMI namespace is like a folder in a file system. Each namespace can contain classes and other namespaces. The root namespace is called "root". You challenge is to be able to recursively enumerate all namespaces starting from "root" on a local or remote computer. For bonus points, I asked you to include the number of classes within each namespace that are NOT system classes, i.e. __ACE. System classes are those that start with a double underscore (__).
For extra bonus points, include custom formatting to present the output in a table.
Let me share how I approached the problem. Of course, I went overboard in my solution, but hopefully, you will pick up some useful techniques along the way.
Creating a Basic Query
Whenever you are building a CIM-based tool, you should be able to begin by using your computer as a test.
$Computername = $env:COMPUTERNAME
$Namespace = 'Root'
In WMI, a namespace is defined as its own class, with a single property of Name.
PS C:\> Get-CimClass __Namespace
NameSpace: ROOT/CIMV2
CimClassName CimClassMethods CimClassProperties
------------ --------------- ------------------
__NAMESPACE {} {Name}
This means, I can query for instances of namespaces using Get-CimInstance.
PS C:\> Get-CimInstance -Namespace $Namespace -ClassName __Namespace
Name PSComputerName
---- --------------
subscription
DEFAULT
CIMV2
msdtc
Cli
Intel_ME
SECURITY
HyperVCluster
SecurityCenter2
RSOP
PEH
StandardCimv2
WMI
MSPS
directory
Policy
Lenovo
virtualization
Interop
Hardware
ServiceModel
SecurityCenter
Microsoft
Appv
I asked for a custom object which included a property that showed the number of non-system classes in each namespace. I can use Get-CimClass to get the classes in a namespace. System classes start with a double underscore (__), so I can filter those out.
PS C:\> (Get-CimClass -Namespace 'Root/Cimv2' -ComputerName $Computername | where { $_.CimClassName -notMatch '^__' }).Count
1250
With this, I can query for all namespaces in the Root namespace and include the class count.
Get-CimInstance -ClassName __Namespace -Namespace $Namespace -ComputerName $Computername |
ForEach-Object {
#construct the namespace path
$ns = Join-Path $Namespace $_.Name
[PSCustomObject]@{
PSTypename = 'CimNamespaceInfo'
Name = $_.Name
Namespace = $ns
Classes = (Get-CimClass -Namespace $ns -ComputerName $Computername | Where-Object { $_.CimClassName -NotMatch '^__' }).Count
Computername = $_.PSComputerName
}
}
This gives me the output I want for the first level of namespaces.
Name Namespace Classes Computername
---- --------- ------- ------------
subscription Root\subscription 17 PROSPERO
DEFAULT Root\DEFAULT 24 PROSPERO
CIMV2 Root\CIMV2 1254 PROSPERO
msdtc Root\msdtc 32 PROSPERO
Cli Root\Cli 22 PROSPERO
Intel_ME Root\Intel_ME 36 PROSPERO
...
Parallel Performance
My initial code is processing each namespace sequentially. This can be slow if there are many namespaces or the namespace has many classes. To speed this up, I can use ForEach-Object -Parallel to process multiple namespaces at once. This requires PowerShell 7+.
Get-CimInstance -ClassName __Namespace -Namespace $Namespace -ComputerName $Computername |
ForEach-Object -parallel {
#construct the namespace path
$ns = Join-Path $using:Namespace $_.Name
[PSCustomObject]@{
PSTypename = 'CimNamespaceInfo'
Name = $_.Name
Namespace = $ns
Classes = (Get-CimClass -Namespace $ns -ComputerName $using:Computername | Where-Object { $_.CimClassName -NotMatch '^__' }).Count
Computername = $_.PSComputerName
}
}
Because each parallel runspace is isolated, I need to use $using: to reference variables from the parent scope. I get the same results, but a little faster.
> I tested with thread jobs as well, but found ForEach-Object -Parallel to be simpler for this case and faster.
I want to make it clear, that the parallel processing is running locally where I am using PowerShell 7. The remote computer can be running Windows PowerShell. When crafting a PowerShell script or function, you need to be able to visualize where each part of the code is running.
Creating a Function
Let's take what I have so far and put it into a function. I'll call it Measure-CimNamespace.
function Measure-CimNamespace {
[CmdletBinding()]
param(
[string]$Namespace = 'Root',
[string]$ComputerName = $env:COMPUTERNAME
)
Get-CimInstance -ClassName __Namespace -Namespace $Namespace -ComputerName $Computername |
ForEach-Object -parallel {
#construct the namespace path
$ns = Join-Path $using:Namespace $_.Name
[PSCustomObject]@{
PSTypename = 'CimNamespaceInfo'
Name = $_.Name
Namespace = $ns
Classes = (Get-CimClass -Namespace $ns -ComputerName $using:Computername | where { $_.CimClassName -NotMatch '^__' }).Count
Computername = $_.CimSystemProperties.ServerName.ToUpper()
} #CimNamespaceInfo
#TODO: recurse through each child namespace
} #foreach-object
}
I decided to pull the computername from the result and not rely on PSComputerName, or the function parameter.
> This version has no error handling.
It works as expected.
PS C:\> Measure-CimNamespace -ComputerName cadenza
Name Namespace Classes Computername
---- --------- ------- ------------
Cli Root\Cli 22 CADENZA
DEFAULT Root\DEFAULT 24 CADENZA
subscription Root\subscription 17 CADENZA
msdtc Root\msdtc 32 CADENZA
...