The Mystery of the Lost Hardware Inventory

In large environments, with tens of thousands of SMS 2003 clients, inventory may get "lost" for various reasons. An example of a lost inventory is below:

  • An SMS 2003 Advanced Client reports a full inventory upon install
  • The client continues to report delta inventory of changes
  • The client reports a delta inventory but that inventory gets "lost"
  • The client continues to report delta inventory but the changes in the "lost" inventory never appear in the database
  • The client gets resync'd, sending up a full inventory, and the changes that were "lost" now appear

Clients don't get resync'd under normal conditions therefore that inventory may not ever appear in the database. This can become an issue for inventory items such as security updates (staying applicable in the database but not applicable on the client). System Center Configuration Manager 2007 contains architectural changes which prevent this scenario from occurring. Specifically SCCM 2007 serializes software and hardware inventory reporting so that missing reports, which are based on serial numbers instead of timestamps, trigger a re-synchronization of inventory at the client.

So if you don't plan on upgrading to SCCM 2007 in the near future how might you prevent inventory from being "lost"? I documented some steps that should assist with detecting, and possibly resolving, the rare scenario when clients send up inventory that somehow disappears before getting loaded into the database. These steps are simply an example of how one might do this and the scripts are not tested and do not contain any type of error handling, logging, or reporting.

Step # 1 – Modify the SMS_DEF.MOF

  • Modify the Win32_OperatingSystem section of the MOF to report "LocalDateTime".  This is required because the "Workstation Status" section shows "Last Hardware Scan" but there isn't a way to keep the history of this data therefore we need a consistent time we can key in on that writes to a history table.

  • Create a new class in the SMS_DEF.MOF to pick up a new WMI class which we'll create a populate later:

    //SMSInventoryHistory
     [SMS_Report(TRUE),
    SMS_Group_Name("SMS Inventory"),
    SMS_Class_ID("MICROSOFT|SMS_INVENTORY|1.0"),
    Namespace("\\\\\\\\.\\\\root\\\\SMSINVENTORY")]
    class InvHistory: SMS_Class_Template
    {
    [SMS_Report(TRUE),key]
    datetime   ScanTime;
    [SMS_Report(TRUE)]
    uint32   KeyIncrement;
    [SMS_Report(TRUE)]
    string   ScanType;
    };

  • Notice the KeyIncrement field.  This is there because we want to report every instance of this class for every inventory.  It is for this reason that we don't need history enabled for this class so it can be disabled by using KB836432 (This still applies to SMS 2003 and likely SCCM 2007).

Step # 2 – Create a recurring advertisement, which should probably run at least twice as often as hardware inventory (if not more), which runs client.vbs

  • Creates a new "SMSInventory" class under "root" if it doesn't exist
  • Gets the last hardware scan from root\ccm\invagt:InventoryActionStatus
  • Creates a new instance of InvHistory in the SMSInventory namespace if the hardware scan is new
  • If the inventory is already recorded then update the "KeyIncrement" field so SMS reports this in every delta
  • Cleans up any instances older than 14 days by default to keep the delta inventory from growing too large
 'client.vbs
'rslaten
'08/15/2007

'Determines when to clean out old inventory instances
iDays = 14

'Connect to WMI locally
Set oLocator = CreateObject("WbemScripting.sWbemLocator")
Set oSvc = oLocator.ConnectServer(".", "root")

'Create a custom namespace to hold data if it doesn't already exist
Set oNamespaces = oSvc.InstancesOf("__namespace")
bFound = False
For each oNamespace in oNamespaces
    If oNamespace.Name = "SMSInventory" Then
        bFound = True
        Exit For
    End If
Next

If Not bFound Then
    Set oNamespace = oSvc.Get("__namespace")
    Set oCustomNameSpace = oNamespace.SpawnInstance_
    oCustomNamespace.name = "SMSInventory"
    oCustomNamespace.Put_()

    'Create a custom class to hold data
    wbemCimtypeUint32 = 19
    wbemCimtypeDatetime = 101
    wbemCimtypeString = 8
    Set oWMI = GetObject("winmgmts:root\SMSInventory")
    Set oClass = oWMI.Get()
    oClass.Path_.Class = "InvHistory"
    oClass.Properties_.add "ScanTime", wbemCimtypeDatetime
    oClass.Properties_("ScanTime").Qualifiers_.add "key", true
    oClass.Properties_.add "KeyIncrement", wbemCimtypeUint32
    oClass.Properties_.add "ScanType", wbemCimtypeString
    oClass.Put_()
End If

'Get timezone
Set oSvc = oLocator.ConnectServer(".", "root\cimv2")
Set collTimeZone = oSvc.ExecQuery("select Bias, DaylightBias from Win32_TimeZone")
For Each oTimeZone in collTimeZone
    iTimeZone = oTimeZone.Bias - oTimeZone.DaylightBias
Next

'Get last Hinv report date and convert to local time
Set oSvc = oLocator.ConnectServer(".", "root\ccm\invagt")
HActionID = """{00000000-0000-0000-0000-000000000001}"""
Set oHinv = oSvc.Get("InventoryActionStatus.InventoryActionID=" & _
    HActionID)
Const CONVERT_TO_LOCAL_TIME = true
'dLastReportDate = ConvertToGMT(oHinv.LastReportDate)
dLastReportDate = oHinv.LastReportDate
dLastReportDate = WMIDateStringToDate(dLastReportDate)
Set dNewTime = CreateObject("WbemScripting.SWbemDateTime")
dNewTime.SetVarDate dLastReportDate, CONVERT_TO_LOCAL_TIME
dLastReportDate = dNewTime.GetVarDate(CONVERT_TO_LOCAL_TIME)
dLastReportDate = DateAdd("n", iTimeZone, dLastReportDate)
dLastReportDate = GetWMIDate(dLastReportDate, "+000")

'Set last Hinv report date
Set oSvc = oLocator.ConnectServer(".", "root\SMSInventory")
Set oHinvHistory = oSvc.InstancesOf("InvHistory")

'First instance in class
If oHinvHistory.Count = 0 Then
    Set oHinv = oSvc.Get("InvHistory").SpawnInstance_
    oHinv.ScanTime = dLastReportDate
    oHinv.KeyIncrement = 0
    oHinv.ScanType = "Hardware"
    oHinv.Put_()
Else
    bFound = False
    
    'Calculate date to clean up old instances
    Set dExpire = CreateObject("WbemScripting.SWbemDateTime")
    dExpire.SetVarDate Now, CONVERT_TO_LOCAL_TIME
    dRegular = dExpire.GetVarDate(CONVERT_TO_LOCAL_TIME)
    dExpireDate = DateAdd("d", -iDays, dRegular)
    dExpire.SetVarDate dExpireDate, CONVERT_TO_LOCAL_TIME
    
    'Updating all instances
    For each oInstance in oHinvHistory
        If dLastReportDate = oInstance.ScanTime Then
            bFound = True
        End If
        
        'Clean up old instances
        If oInstance.ScanTime < dExpire Then
            oInstance.Delete_
        Else
            'Increment the KeyIncrement property to force SMS to pick
            'up the instance during inventory
            oInstance.KeyIncrement = oInstance.KeyIncrement + 1
            oInstance.Put_
        End If
    Next
    
    'Create a new instance if needed
    If Not bFound Then
        Set oHinv = oSvc.Get("InvHistory").SpawnInstance_
        oHinv.ScanTime = dLastReportDate
        oHinv.KeyIncrement = 0
        oHinv.ScanType = "Hardware"
        oHinv.Put_()    
    End If
End If

Function ConvertToGMT(dDate)
    ConvertToGMT = Replace(dDate, "-000", iTimeZone)
End Function

Function WMIDateStringToDate(dtmInstallDate)
     WMIDateStringToDate = CDate(Mid(dtmInstallDate, 5, 2) & "/" & _
     Mid(dtmInstallDate, 7, 2) & "/" & Left(dtmInstallDate, 4) _
     & " " & Mid (dtmInstallDate, 9, 2) & ":" & _
     Mid(dtmInstallDate, 11, 2) & ":" & Mid(dtmInstallDate, _
     13, 2))
End Function

Function GetWMIDate(vd,strOffset)
    On Error Resume Next
    GetWMIDate = Year(vd) & AddZero(Month(vd)) & AddZero(Day(vd)) & _
            AddZero(Hour(vd)) & AddZero(Minute(vd)) & _
            AddZero(Second(vd)) & ".000000" & strOffset
End Function

Function AddZero(pNum)
    On Error Resume Next
    If pNum <=9 then
        AddZero = "0" & pNum
    Else AddZero = pNum
    End If
End Function

Step # 3 – Periodically check to see if there is any missing hardware inventory by running server.vbs

  • Queries the SMS database for OS LocalDateTime and the custom SMSInventory ScanTime
  • Compares the ScanTime to LocalDateTime, searching for missing LocalDateTime entries.  To find a match the LocalDateTime must be within 5 minutes from the ScanTime.  If any clients take longer than 5 minutes to complete a hardware inventory cycle then this may need to be increased.
  • For any ScanTime without a matching LocalDateTime, we display the fact that there is a missing inventory.
 'server.vbs
'rslaten
'08/15/2007

'Globals
Dim aClients(), iClients, iInstanceCount, iTotal

'Get SMS Namespace
sSMSNameSpace = GetSMSNameSpace()

'Connect to site server
Set oWMI = GetObject("winMgmts:" & sSMSNameSpace)

'Query clients and create and array of client objects
sQuery = "select distinct SMS_R_System.Name, SMS_R_System.SMSUniqueIdentifier," & _
    " SMS_G_System_SMS_INVENTORY.ScanTime, SMS_GH_System_OPERATING_SYSTEM.LocalDateTime" & _
    " from  SMS_R_System inner join SMS_G_System_SMS_INVENTORY on " & _
    "SMS_G_System_SMS_INVENTORY.ResourceID = SMS_R_System.ResourceId inner" & _
    " join SMS_GH_System_OPERATING_SYSTEM on " & _
    "SMS_GH_System_OPERATING_SYSTEM.ResourceID = SMS_R_System.ResourceId " & _
    "where SMS_G_System_SMS_INVENTORY.ScanTime is not NULL and " & _
    "SMS_GH_System_OPERATING_SYSTEM.LocalDateTime is not NULL"

Set aCollection = oWMI.ExecQuery(sQuery)
iTotal = aCollection.count
For Each oInstance In aCollection
    If sLastName <> oInstance.SMS_R_System.Name Then
        'Make sure this isn't the first iteration in the array
        If iInstanceCount <> 0 Then
            'Add to master array
            ReDim Preserve aClients(iClients)
            Set aClients(iClients) = oClient
            iClients = iClients + 1
        End If
        
        'Now create new client object
        Set oClient = New Client
        oClient.sClient = oInstance.SMS_R_System.Name
        oClient.sGUID = oInstance.SMS_R_System.SMSUniqueIdentifier
    End If
    
    If sLastScan <> oInstance.SMS_G_System_SMS_Inventory.ScanTime Then
        oClient.AddSMSInventoryScanDate(oInstance.SMS_G_System_SMS_Inventory.ScanTime)
    End If
    
    If sLastOSScan <> oInstance.SMS_GH_System_OPERATING_SYSTEM.LocalDateTime Then
        oClient.AddOSScanDate(oInstance.SMS_GH_System_OPERATING_SYSTEM.LocalDateTime)
    End If 

    sLastName = oInstance.SMS_R_System.Name
    sLastScan = oInstance.SMS_G_System_SMS_Inventory.ScanTime
    sLastOSScan = oInstance.SMS_GH_System_OPERATING_SYSTEM.LocalDateTime
    iInstanceCount = iInstanceCount + 1
    
    'If this is the last instance add it to the array
    If iInstanceCount = iTotal Then
        'Add to master array
        ReDim Preserve aClients(iClients)
        Set aClients(iClients) = oClient
        iClients = iClients + 1    
    End If
Next

'Loop through clients in memory to check for missing inventory
For each oClient in aClients
    aScans = oClient.aScans
    WScript.Echo "******************************************"
    WScript.Echo "Client = " & oClient.sClient
    WScript.Echo "GUID = " & oClient.sGUID
    For each oScan in oClient.aScans
        If not oScan.isOS Then
            Set oMatch = new Match
            oMatch.dScan = WMIDateStringToDate(oScan.dDate)
            oMatch.bMatch = false
            
            'Find a matching OS time (look for match within 5 minutes)
            For each oTime in aScans
                If oTime.isOS Then
                    If DateDiff("n", oMatch.dScan, _
                        WMIDateStringToDate(oTime.dDate)) < 5 and _
                        DateDiff ("n", oMatch.dScan, _
                        WMIDateStringToDate(oTime.dDate)) > -5 Then             
                        oMatch.dOS = WMIDateStringToDate(oTime.dDate)
                        oMatch.bMatch = true
                    End If
                End If
            Next
            
            'Make sure we found a match
            If not oMatch.bMatch Then
                WScript.Echo "*PROBLEM FOUND*"
                WScript.Echo "SMS Inventory Report = " & oMatch.dScan
                WScript.Echo "*NO MATCHING OS DATE FOUND*"
            Else
                WScript.Echo oMatch.dScan & " = " & oMatch.dOS
            End If
        End If
    Next
    WScript.Echo "******************************************"
Next

'Get regular date
Function WMIDateStringToDate(dtmInstallDate)
    WMIDateStringToDate = CDate(Mid(dtmInstallDate, 5, 2) & "/" & _
        Mid(dtmInstallDate, 7, 2) & "/" & Left(dtmInstallDate, 4) _
            & " " & Mid (dtmInstallDate, 9, 2) & ":" & _
                Mid(dtmInstallDate, 11, 2) & ":" & Mid(dtmInstallDate, _
                    13, 2))
End Function


'Gets SMS Namespace from local site server
Function GetSMSNameSpace()
    Set refWMI = GetObject("winMgmts:\root\sms")    
    Set colNameSpaceQuery = refWMI.ExecQuery("select * from SMS_ProviderLocation")
    For Each refitem in colNameSpaceQuery
        GetSMSNameSpace = refitem.NamespacePath
    Next
End Function

Class Client
    Public aScans()
    Private i
    Public sClient
    Public sGUID
    
    Public Sub AddOSScanDate(dScanDate)
        'Check for duplicates
        bFound = false
        For Each oScan in aScans
            If dScanDate = oScan.dDate And _
                oScan.isOS = true Then
                bFound = true
                Exit For
            End If
        Next
        
        If Not bFound Then
            Set oScan = New Scan
            oScan.dDate = dScanDate
            oScan.isOS = true
            ReDim Preserve aScans(i)
            Set aScans(i) = oScan
            i = i + 1
        End If
    End Sub
    
    Public Sub AddSMSInventoryScanDate(dScanDate)
        'Check for duplicates
        bFound = false
        For Each oScan in aScans
            If dScanDate = oScan.dDate And _
                oScan.isOS = false Then
                bFound = true
                Exit For
            End If
        Next
        
        If Not bFound Then        
            Set oScan = New Scan
            oScan.dDate = dScanDate
            oScan.isOS = false
            ReDim Preserve aScans(i)
            Set aScans(i) = oScan
            i = i + 1    
        End If
    End Sub
End Class

Class Scan
    Public dDate
    Public isOS
End Class

Class Match
    Public dScan
    Public dOS
    Public bMatch
End Class

Step # 4 – Force a resync on the clients that have "lost" inventory

  • The method to delete the local inventory cache is the known and documented method of doing this.
  • There may be a way to use the MP API to create a server side policy, but this would require significant work and a non-script (C++) application.  Internally, as you can see with SQL profiler, we execute the sp_RC_InvResync stored procedure but as you know executing these directly isn't recommended or supported.

These examples do not contain error checking and need to be optimized for performance.  The examples have also only been tested in a very small test environment using only the scenario documented at the top of this post.  If this method is to be used in a production environment more code work and testing needs to be done, particularly on the server end, to improve the detection logic, add history to which clients resyncs have been sent to, and add reporting capabilities.

**Steps 3 and 4 might not be required.  During testing I found a potentially unforeseen benefit of running the client.vbs script on clients with "lost" inventory.  Since every instance of the InvHistory class is reported during every inventory, when an instance is listed as "Update" but isn't in the database, dataloader will automatically request a resync for that client.  More testing of this finding needs to be done to verify that this will work, and if it does it should simplify detecting which clients have lost inventory**