ConfigMgr: SQL function to convert v_r_system.Distinguished_Name0 to Active Directory location (Organizational Unit or Container)

A lot of times I’m asked to provide the location in Active Directory (Organizational Unit, Container) in a report. I typically use the v_ra_system_systemouname and v_ra_system_systemcontainername by selecting the longest string for each ResourceID, but it’s kind of kludgy.

I found a post on the archived forums that had some code to create a function, but it was limited in that it expects a two-part domain name, such as DC=domain,DC=name. Passing anything different (such as DC=domain,DC=name,DC=local) returns incorrect results.

Based on the post, I was able generalize it and create my own [this uses the string_split function, which is in SQL Server 2016 (13.x) and later]:

create function [dbo].[GetADLocationFromDN] (
    @InputString varchar(max)
)
returns varchar(max)

begin
/*
Given an distinguished name input string:
'CN=XYZ123PW5OASDC,OU=SiteA,OU=Domain Controllers,DC=abc,DC=def,DC=ghi,DC=jkl,DC=com'
we want to return the Active Directory Location:
'ABC.DEF.GHI.JKL.COM/DOMAIN CONTROLLERS/SITEA'
*/
declare @WorkString varchar (max) = ''
declare @Rows int
declare @Counter int = 1

declare @SplitItems table (
    RowNumber int identity (1, 1),
    ParsedValue varchar(max) 
)
/*
Validate that the format of the Input String is:
	not null
	__=_% separated by commas
	has a "DC=_%" RDN (relative distinguished name)
*/
if @InputString is not null
	and (select count (value) from string_split(@InputString, ',') where value not like '__=_%') = 0
	and patindex ('%dc=_%', @InputString) > 0
begin
	/*
	First, leave the first "DC=" and convert the rest of them to "."; so the string becomes:
	'CN=XYZ123PW5OASDC,OU=SiteA,OU=Domain Controllers,DC=abc.def.ghi.jkl.com'
	*/
	select @WorkString = substring (@InputString, 1,  patindex ('%,DC=%', @InputString))
	+ replace (substring (@InputString, patindex ('%,DC=%', @InputString) + 1, len (@InputString)), ',DC=', '.')

	/*
	Put each part of the string into a temporary table (removing
	the prefixed 2 character attribute string and the equal sign)
	*/
	insert into @SplitItems (ParsedValue) select right (value, len(value) - 3) from string_split(@WorkString, ',')

	select @Rows = max(RowNumber) from @SplitItems
	select @Counter = @Rows

	-- Working backwards, concatenate the strings with '/' separators
	while (@Counter > 1)
	begin
		-- Don't start it with a '/'
		if @Counter = @Rows
			set @WorkString = upper ((select ParsedValue from @SplitItems where RowNumber = @Counter))
		else
			set @WorkString = @WorkString + '/' + upper ((select ParsedValue from @SplitItems where RowNumber = @Counter))
		set @Counter = @Counter - 1
	end
end
else
	select @WorkString = NULL

return @WorkString
end

Here is a sample:

ConfigMgr: distmgr.log shows “ERROR CreateVirtualDirectory: Failed to update virtual directory SMS_DP_SMSPKG$. error = Class not registered”

I had a distribution point that was causing this error in the distmgr.log:

creating virtual directory using iis6 api...
ERROR CreateVirtualDirectory: Failed to update virtual directory SMS_DP_SMSPKG$. error = Class not registered
vdHelper.CreateVirtualDirectory() - Failed to CreateVirtualDirectory SMS_DP_SMSPKG$ for DP DISTRIBUTIONPOINT.DOMAIN.NAME. Will retry in 5 seconds

I found this, and while not exactly the same error message, the suggestion was to uninstall and reinstall the IIS 6 WMI Compatibility feature.

I put the distribution point into maintenance mode, uninstalled the IIS 6 WMI Compatibility feature, rebooted, and reinstalled the feature, and then took it back out of maintenance mode.  Immediately after I did, the distribution manager successfully performed whatever operation it was attempting after the “creating virtual directory using iis6 api…” message:

CCMSetup: Module C:\WINDOWS\system32\CcmUsrCse.dll failed to register. HRESULT -2147024770

Trying to install the client on a workstation and was getting the following in the ccmsetup.log:

MSI: Action 14:00:20: SelfRegModules. Registering modules
MSI: Module C:\WINDOWS\system32\CcmUsrCse.dll failed to register.  HRESULT -2147024770.  Contact your support personnel.
MSI: Action 14:00:22: Rollback. Rolling back action:
File C:\WINDOWS\ccmsetup\{AEA27908-1392-47A7-A076-CED9EB968C73}\client.msi installation failed. Error text: ExitCode: 1603
Action: SelfRegModules.
ErrorMessages:
Module C:\WINDOWS\system32\CcmUsrCse.dll failed to register.  HRESULT -2147024770.  Contact your support personnel.
Next retry in 120 minute(s)...

Error -2147024770 (0x8007007E) translates to ERROR_MOD_NOT_FOUND (The specified module could not be found).

I looked at the folder during install and saw the file CcmUsrCse.dll there, so I did not understand why it said the module wasn’t found.

Searching the internet, I found this old post on Microsoft’s website.  It states:

  • “That error …means that Dll cannot register because it requires some other Dll that’s not available.”

The last entry on the post said:

  • “I found that installing the MICROSOFT VISUAL C++ 2005 RUNTIME LIBRARIES (X86) fixed my issue.”

I downloaded and manually installed the latest x64 and x86 versions of the Microsoft Visual C++ Redistributable and then the client installed successfully.

ConfigMgr: Collecting monitor information with hardware inventory and translating UserFriendlyName0 into something readable

I was asked to gather model information of monitors attached to the ConfigMgr clients.  I found an article written by Sherry Kissinger about how to add it to hardware inventory:

  • In your CM Console, Administration, Client Settings, right-click on Default Client Settings, select Properties.
  • Go to “Hardware Inventory” on the left, then “Set Classes…” on the right.
  • Choose “Add…” and connect to a computer to which you have administrative rights.  Specify the WMI namespace root\wmi, and make sure to click “Recursive”.
  • Find “WMIMonitorID” and add it.  Click OK until the Default Settings window is closed.

This added the information into inventory, but shortly thereafter I realized that the UserFriendlyName0 was not so user friendly – the overwhelming majority are comma delimited numbers:

WMIExplorer shows that the values are stored that way:

I figured those to be ASCII values for printable characters, so I checked that using the CHAR function in Excel:

Yup, that’s what it looks like.

I decided to create a function to translate it.  I never add anything to the ConfigMgr database, but I keep a separate database on the SQL Server with various useful functions, so when I have to run a report I call the function:

USE [CM_Supplemental]
GO

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

create function [dbo].[fn_ConvertCommaDelimitedASCIIValuestoString] (@InputString nvarchar (max))
returns nvarchar (max)
as
begin

/*
This function can be used to convert the data stored in 
the UserFriendlyName0 column of the v_GS_WMIMONITORID view.
The data seems to be stored as a string of comma delimited 
decimal ASCII values that can be converted to a string.

A sample is:

'68, 69, 76, 32, 49, 57, 48, 56, 70, 80, 66, 76, 75'

Converting to characters makes it "DEL 1908FPBLK"

If the parameter does not fit the format listed above, the
return value will just be the parameter back again, since 
some of the values in UserFriendlyName0 are actual models
such as "NS-22D510NA19".  Go figure.
*/

declare @WorkString nvarchar (max)
declare @ReturnString nvarchar (max) = ''
declare @ASCIIValue tinyint

-- Check the input string to make sure it is not null,
-- starts with a number, has commas, and there are no 
-- characters other than numbers, commas, and spaces

if @InputString is null
  or charindex (',', @InputString) = 0
  or trim (@InputString) not like '[0-9]%'
  or patindex ('%[^ ,0-9]%', @InputString) > 0
	begin
	set @ReturnString = @InputString
	end
else
	begin
	set @WorkString = trim (@InputString)
	-- Work through the string, one numeric value at a time,
	-- by grabbing the data before the next comma
	while charindex (',', @WorkString) <> 0
		begin
		set @ASCIIValue = try_cast (substring (@WorkString, 0, charindex (',', @WorkString)) as tinyint)
		-- Make sure the numeric value is a valid ASCII value
		-- and can be used in the CHAR function.
		if @ASCIIValue is not null
			begin
			-- Only convert printable characters
			if @ASCIIValue > 31 set @ReturnString = @ReturnString + char (@ASCIIValue)
			end
		-- If it's not valid, just return the supplied input string
		else
			begin
			set @ReturnString = @InputString
			set @WorkString = ''
			end
		-- Trim off the current number and reset @WorkString to get the next
		set @WorkString = substring (@WorkString, charindex (',', @WorkString) + 1, LEN (@WorkString))
		end
	end
-- Get the last number after the final comma
set @ASCIIValue = try_cast (@WorkString as tinyint)

if @ASCIIValue is not null
	begin
	-- Only convert printable characters
	if @ASCIIValue > 31 set @ReturnString = @ReturnString + char (@ASCIIValue)
	end
else
	set @ReturnString = @InputString

return (@ReturnString)
end

I also found that about 1% of the data in SerialNumberID0 and .1% of the data in ManufacturerName0 are also stored in the comma delimited ASCII format, so you can run:

select
  s.Name0
, m.ResourceID
, m.ProductCodeID0
, CM_Supplemental.dbo.fn_ConvertCommaDelimitedASCIIValuestoString (m.UserFriendlyName0) UserFriendlyName0
, CM_Supplemental.dbo.fn_ConvertCommaDelimitedASCIIValuestoString (m.SerialNumberID0) SerialNumberID0
, CM_Supplemental.dbo.fn_ConvertCommaDelimitedASCIIValuestoString (m.ManufacturerName0) ManufacturerName0
, m.UserFriendlyNameLength0
, m.WeekOfManufacture0
, m.YearOfManufacture0
from v_GS_WMIMONITORID m
inner join v_R_System s on s.ResourceID = m.ResourceID

Final note – I did go back to Sherry’s article and noticed she has code there that also translates these fields, but I’m going to stick with mine.

Query the event log for the last interactive logon with PowerShell

I had a workstation that I wanted to know if anyone had interactively logged onto it recently, so I wrote a PowerShell script to check the Security Event Log to find the last time someone logged on.  I found this article that detailed the criteria to look for – EventID 4624, and Logon Type 2 (for interactive) or Logon Type 10 (for Terminal Services or Remote Desktop logons).  Just using EventID and Logon Type turns out to be too broad – I was getting events that were generated by things with Security IDs “Font Driver Host\UMFD-0” and “Window Manager\DWM-1”, so I added LogonProcessName = “User32 “ (note the space at the end) and that seemed to do the trick.

Get-LastInteractiveLogon.ps1:

param([Parameter(Mandatory=$true)][string] $ComputerName)

$LocalLogonXPath = "*[System[Provider[@Name='Microsoft-Windows-Security-Auditing'] and EventID=4624] 
and EventData[Data[@Name='LogonType']='2'] and EventData[Data[@Name='LogonProcessName']='User32 ']]"

$MSTSCLogonXPath = "*[System[Provider[@Name='Microsoft-Windows-Security-Auditing'] and EventID=4624]
and EventData[Data[@Name='LogonType']='10'] and EventData[Data[@Name='LogonProcessName']='User32 ']]"

try
{
	$OldestEvent = Get-WinEvent -ComputerName $ComputerName -LogName Security -MaxEvents 1 -Oldest -ErrorAction Stop
}
catch [Exception]
{
	Write-Error $_.Exception.Message
	Exit
}

# Skip lines for readability
Write-Host
Write-Host "Last Interactive Logon Information:" -ForegroundColor "Yellow"
try
{
	$Logon = Get-WinEvent -ComputerName $ComputerName -LogName Security -MaxEvents 1 -FilterXPath $LocalLogonXPath -ErrorAction Stop
	$Message = $Logon.Message
	Write-Host
	Write-Host "Date:" $Logon.TimeCreated
	Write-Host
	Write-Host $Message.Substring(0, $Message.IndexOf('This event is generated when a logon session is created.'))
}
catch [Exception]
{
 	if ($_.Exception -match "No events were found that match the specified selection criteria")
	{
		Write-Host "No events found";
	}
}

Write-Host
Write-Host "Last Remote Interactive Logon Information:" -ForegroundColor "Yellow"
try
{
	$Logon = Get-WinEvent -ComputerName $ComputerName -LogName Security -MaxEvents 1 -FilterXPath $MSTSCLogonXPath -ErrorAction Stop
	$Message = $Logon.Message
	Write-Host
	Write-Host "Date:" $Logon.TimeCreated
	Write-Host
	Write-Host $Message.Substring(0, $Message.IndexOf('This event is generated when a logon session is created.'))
}
catch [Exception]
{
 	if ($_.Exception -match "No events were found that match the specified selection criteria")
	{
		Write-Host "No events found";
	}
}

Write-Host
Write-Host "Oldest entry in security log is dated" $OldestEvent.TimeCreated -ForegroundColor "Yellow"

Run the script via:

powershell .\Get-LastInteractiveLogon.ps1 COMPUTERNAME

The output looks like:

Notes:

  • The start date of the Security event log is displayed at the end so you know how far back it goes.
  • Have to use -ErrorAction Stop in the try/catch block… apparently it’s a non-terminating error that won’t get caught without it ((Powershell) Catch “Get-WinEvent : No events were found” Get-WinEvent – Stack Overflow).
  • The LogonProcessName “User32” has a blank at the end and it needs to be included:
    • EventData[Data[@Name=’LogonProcessName’]=’User32 ‘]
  • The XPath query can be used in the Event Viewer GUI to filter events:
  • The output generates a lot of extraneous information, so I use the substring function to trim the following text: