Thursday 28 November 2013

Custom WMI Classes and reporting into SCCM 2012

Using Powershell, its possible to create CUSTOM WMI Classes and then include these in the Hardware Inventory for all devices.   This example will cover obtaining monitor information for desktop machines.

Creating the new class

To start, run Powershell and run the command Get-WMIObject -class MonitorDetails and you should get the error:


The script below will create the class, and populate it with the monitor details for your machine
:

#
# This script reads the EDID information stored in the registry for the currently connected monitors
# and stores their most important pieces of identification (Name, Size, Serial Number etc) in WMI
# for later retrieval by SCCM
#

# Reads the 4 bytes following $index from $array then returns them as an integer interpreted in little endian
 

function Get-LittleEndianInt($array, $index)
{
    # Create a new temporary array to reverse the endianness in
    $temp = @(0) * 4
    [Array]::Copy($array, $index, $temp, 0, 4)
    [Array]::Reverse($temp)
   
    # Then convert the byte data to an integer
    [System.BitConverter]::ToInt32($temp, 0)
}

# Creates a new class in WMI to store our data
function Create-Wmi-Class()

{
    $newClass = New-Object System.Management.ManagementClass("root\cimv2", [String]::Empty, $null);

    $newClass["__CLASS"] = "MonitorDetails";

    $newClass.Qualifiers.Add("Static", $true)
    $newClass.Properties.Add("DeviceID", [System.Management.CimType]::String, $false)
    $newClass.Properties["DeviceID"].Qualifiers.Add("key", $true)
    $newClass.Properties["DeviceID"].Qualifiers.Add("read", $true)
    $newClass.Properties.Add("ManufacturingYear", [System.Management.CimType]::UInt32, $false)
    $newClass.Properties["ManufacturingYear"].Qualifiers.Add("read", $true)
    $newClass.Properties.Add("ManufacturingWeek", [System.Management.CimType]::UInt32, $false)
    $newClass.Properties["ManufacturingWeek"].Qualifiers.Add("read", $true)
    $newClass.Properties.Add("DiagonalSize", [System.Management.CimType]::UInt32, $false)
    $newClass.Properties["DiagonalSize"].Qualifiers.Add("read", $true)
    $newClass.Properties["DiagonalSize"].Qualifiers.Add("Description", "Diagonal size of the monitor in inches")
    $newClass.Properties.Add("Manufacturer", [System.Management.CimType]::String, $false)
    $newClass.Properties["Manufacturer"].Qualifiers.Add("read", $true)
    $newClass.Properties.Add("Name", [System.Management.CimType]::String, $false)
    $newClass.Properties["Name"].Qualifiers.Add("read", $true)
    $newClass.Properties.Add("SerialNumber", [System.Management.CimType]::String, $false)
    $newClass.Properties["SerialNumber"].Qualifiers.Add("read", $true)
    $newClass.Properties.Add("DeviceFound", [System.Management.CimType]::UInt32, $false)
    $newClass.Properties["DeviceFound"].Qualifiers.Add("read", $true)
    $newClass.Put()
}

# Check whether we already created our custom WMI class on this PC, if not, create it
[void](Get-WMIObject MonitorDetails -ErrorAction SilentlyContinue -ErrorVariable wmiclasserror)
if ($wmiclasserror)

{
    try { Create-Wmi-Class }
    catch

    {
        "Could not create WMI class"
        Exit 1
    }
}

# Iterate through the monitors in Device Manager
$monitorInfo = @()
Get-WMIObject Win32_PnPEntity -Filter "Service='monitor'" | % { $k=0 }

{
    $mi = @{}
    $mi.Caption = $_.Caption
    $mi.DeviceID = $_.DeviceID
    # Then look up its data in the registry
    $path = "HKLM:\SYSTEM\CurrentControlSet\Enum\" + $_.DeviceID + "\Device Parameters"
    $edid = (Get-ItemProperty $path EDID -ErrorAction SilentlyContinue).EDID

    # Some monitors, especially those attached to VMs either don't have a Device Parameters key or an EDID value. Skip these
    if ($edid -ne $null)

    {
        # Collect the information from the EDID array in a hashtable
        $mi.Manufacturer += [char](64 + [Int32]($edid[8] / 4))
        $mi.Manufacturer += [char](64 + [Int32]($edid[8] % 4) * 8 + [Int32]($edid[9] / 32))
        $mi.Manufacturer += [char](64 + [Int32]($edid[9] % 32))
        $mi.ManufacturingWeek = $edid[16]
        $mi.ManufacturingYear = $edid[17] + 1990
        $mi.DeviceFound = 1
        $mi.HorizontalSize = $edid[21]
        $mi.VerticalSize = $edid[22]
        $mi.DiagonalSize = [Math]::Round([Math]::Sqrt($mi.HorizontalSize*$mi.HorizontalSize + $mi.VerticalSize*$mi.VerticalSize) / 2.54)

        # Walk through the four descriptor fields
        for ($i = 54; $i -lt 109; $i += 18)

        {
            # Check if one of the descriptor fields is either the serial number or the monitor name
            # If yes, extract the 13 bytes that contain the text and append them into a string
            if ((Get-LittleEndianInt $edid $i) -eq 0xff)

            {
                for ($j = $i+5; $edid[$j] -ne 10 -and $j -lt $i+18; $j++) { $mi.SerialNumber += [char]$edid[$j] }
            }
            if ((Get-LittleEndianInt $edid $i) -eq 0xfc)

            {
                for ($j = $i+5; $edid[$j] -ne 10 -and $j -lt $i+18; $j++) { $mi.Name += [char]$edid[$j] }
            }
        }
       
        # If the horizontal size of this monitor is zero, it's a purely virtual one (i.e. RDP only) and shouldn't be stored
        if ($mi.HorizontalSize -ne 0)

        {
            $monitorInfo += $mi
        }
    }
   
}

$monitorInfo

# Clear WMI
Get-WmiObject MonitorDetails | Remove-WmiObject

# And store the data in WMI
$monitorInfo | % { $i=0 }

{
    [void](Set-WmiInstance -Path \\.\root\cimv2:MonitorDetails -Arguments @{DeviceID=$_.DeviceID; ManufacturingYear=$_.ManufacturingYear; `
    ManufacturingWeek=$_.ManufacturingWeek; DiagonalSize=$_.DiagonalSize; Manufacturer=$_.Manufacturer; Name=$_.Name; SerialNumber=$_.SerialNumber; DeviceFound=$_.DeviceFound})
    $i++
}


So now, running Get-WMIObject -class MonitorDetails should return some information (2 monitors shown as attached to this device)



Now we need to get the newly created class into SCCM. First of all, we'll add it to the inventoried class list.

Updating SCCM Client Base Classes for Hardware Inventory

Run the SCCM Management Console
  • Click on Administration Section
  • Click on Client Settings
  • Right click on Default Client Settings, then properties (Has to be the default one to select the new class). If you have additional client settings you can enable and disable the class for collection afterwards, but it must first be selected via the Default Client Settings
  • Click Hardware Inventory
  • Click Set Classes and you'll see the following screen


  • Click the Add Button, then click Connect

  • Specify the machine name where the class was created above then click Connect
  • Locate the newly created class MonitorDetails, tick the box, then click OK



When you get returned to the Hardware Inventory Class screen, the new class MonitorDetails will be listed and will be selected. If you have other custom Client Settings, deselect the new class at this point otherwise it will be picked up for all devices that the Default Client Settings are assigned to.



If you have other custom Client Settings, go into each relevant one, and under Hardware Inventory, Set Classes, and then tick the box for MonitorDetails.

Before you click OK to save your settings, its a good idea at this point to load up CMTRACE and load up the log file <SCCMInstallFolder>\Logs\dataldr.log - you will see entries go through for the new class as its added to the SCCM database as a new table (in this case you'll see 2 new tables created in SQL - dbo.MONITORDETAILS_DATA and dbo.MONITORDETAILS_HIST), you will also see SQL views created (dbo.v_GS_MONITORDETAILS)

So now the new class has been selected, and the relevant SQL structures updated - we now need to tell our client devices to use and create the new WMI class and report information back to SCCM.

Client Baseline Settings

Back to the main SCCM Management Console
  • Click on Assets and Compliance Section
  • Click on Compliance Settings, Configuration Items
  • Click on Create Configuration Item ( I won't detail all the screens here - skipping to the "Settings" section - specify name, description, supported platforms etc as you see fit.
  • You want to create a new Settings with a Setting Type of Script and a Data type of String. Now we'll specify the Discovery Script and the Remediation Script



Discovery Script (Script Language : Powershell)

$ClassInfo = Get-WMIObject MonitorDetails -ErrorAction SilentlyContinue -ErrorVariable wmiclasserror

if ($wmiclasserror) { $ClassFound = 0 } else { $ClassFound = 1 }

$ClassFound


Remediation Script (Script Language : Powershell)

The Remediation script is the Powershell script at the top of this post - just copy and paste it in...

At this point we now need to specify a Compliance Rule
  • Make sure the Rule Type is Value, and the the Value Equals 1
  • Tick the box for Run the specified remediation script...and save


  • Once the configuration item is created, create a Configuration Baselines and add the new item above.
  • And finally, Deploy the Configuration Baseline to a collection - at this point don't forget to tick the box Remediate noncompliant rules when supported, otherwise the new class won't be created on your devices!

Now thats all done, when you look at the hardware inventory for any device, the monitor details will now be listed (Devices, right click, Start, Resource Explorer).



And finally, run a report to retrieve your new custom class!

SELECT * FROM v_GS_MONITORDETAILS







2 comments:

  1. Hi Andy,
    I need help in creating custom class in WMI and putting data from powershell script.

    ReplyDelete
  2. when i run the script it completes successfully. but afterwards i run 'Get-WMIObject -class MonitorDetails' and it doesnt error, but it is blank. anyone have any ideas?

    ReplyDelete