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**
Comments
Anonymous
November 05, 2007
Dear Friends, Another very good friend send me this article to fix a problem that we have and i thinkAnonymous
November 05, 2007
Dear Friends, Another very good friend send me this article to fix a problem that we have and i thinkAnonymous
November 06, 2007
http://blogs.msdn.com/rslaten/archive/2007/09/18/the-mystery-of-the-lost-hardware-inventory.aspx