- Create a certificate-signed RDP shortcut via Group Policy - Fri, Aug 9 2019
- Monitor web server uptime with a PowerShell script - Tue, Aug 6 2019
- How to build a PowerShell inventory script for Windows Servers - Fri, Aug 2 2019
In the earlier 2000s, a command-line utility that returned dumb strings would have been OK, but today we have smart objects with PowerShell! Let's build a wrapper around the dcdiag tool with PowerShell to bring this handy tool into the current times.
If you run dcdiag by itself, you can see that it returns some useful information, but this is just loose text. Dcdiag does not return objects we can parse and manipulate as we can in PowerShell.
To wrap this tool in PowerShell, we'll need to build a function. I'll call my function Invoke-DcDiag. We can get crazy with this, but to keep it simple, I want to pass a single domain controller (DC) to dcdiag, so I'll create this function with a parameter called DomainController.
function Invoke-DcDiag { param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$DomainController ) ## Code goes here }
Once I've built the skeleton, I can then add in the code. To run dcdiag against a domain controller, I need to use the /s switch, so I'll add that to my function. I need to capture the output, so I'll assign it to a variable. To mimic the code that will go into the function, I'll assign $DomainController a value of DC representing the name of the DC in my environment.
$DomainController = 'DC' $result = dcdiag /s:$DomainController
I've now captured the output to a variable. That's great and all, but we now need to parse the output. Let's get our regex on! Before we get that far though, I need to define what the output should look like. Eyeballing our sample output, it seems like for every test that happens, dcdiag always returns one of two strings:
<DomainControllerName> passed test <TestName> <DomainControllerName> failed test <TestName>
I want PowerShell to return an object for each test result with a TestName, TestResult, and an EntityName to represent a domain controller, NT Directory Service (NTDS) partition, or whatever else the test is running against.
We'll need to parse each line in the string. One way to do this is to use Select-String. Select-String has a parameter called Pattern that allows us to try to match a string against a regular expression. Without teaching you regex, I'll give you the answer here. The regex you need for this is '\. (.*) \b(passed|failed)\b test (.*)'.
When you run it as is, you may think that either I'm crazy or it doesn't work. One of these things is right.
PS> $dcDiagOutput | select-string -pattern '\. (.*) \b(passed|failed)\b test (.*)' ......................... DC passed test Connectivity ......................... DC passed test Advertising ......................... DC passed test FrsEvent ......................... DC passed test DFSREvent ......................... DC passed test SysVolCheck ......................... DC passed test KccEvent ......................... DC passed test KnowsOfRoleHolders
However, PowerShell is hiding regex groups from you. Each regex group contains each of the properties we'd like the resulting output to have. We reference these groups by looking at the various indexes in the Groups array that the Matches property contains that Select-String returns.
You can see below that I'm looping over each match, assigning each group value to an object property we'd like, and then returning the object.
$result | select-string -pattern '\. (.*) \b(passed|failed)\b test (.*)' | foreach { $obj = @{ TestName = $_.Matches.Groups[3].Value TestResult = $_.Matches.Groups[2].Value Entity = $_.Matches.Groups[1].Value } [pscustomobject]$obj }
Bringing this code all back into our function makes the function look like this:
function Invoke-DcDiag { param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$DomainController ) $result = dcdiag /s:$DomainController $result | select-string -pattern '\. (.*) \b(passed|failed)\b test (.*)' | foreach { $obj = @{ TestName = $_.Matches.Groups[3].Value TestResult = $_.Matches.Groups[2].Value Entity = $_.Matches.Groups[1].Value } [pscustomobject]$obj } }
At this time, we bring this function into our session, run it via Invoke-DcDiag -DomainController DC, and voila, dcdiag for the modern age!
Subscribe to 4sysops newsletter!
PS> Invoke-DcDiag -DomainController DC TestResult Entity TestName ---------- ------ -------- passed DC Connectivity passed DC Advertising passed DC FrsEvent passed DC DFSREvent passed DC SysVolCheck passed DC KccEvent passed DC KnowsOfRoleHolders passed DC MachineAccount passed DC NCSecDesc passed DC NetLogons passed DC ObjectsReplicated passed DC Replications passed DC RidManager passed DC Services passed DC SystemLog passed DC VerifyReferences passed ForestDnsZones CheckSDRefDom passed DomainDnsZones CheckSDRefDom passed Schema CheckSDRefDom passed Schema CrossRefValidation passed Configuration CheckSDRefDom passed Configuration CrossRefValidation passed techsnips CheckSDRefDom passed techsnips CrossRefValidation passed techsnips.local LocatorCheck passed techsnips.local Intersite
Hi,
nice script!!1
How can I put the whole Output to the Body of an email?
Frank
Hi Frank,
there is no straight-forward way. Send-EmailMessage accepts only type String as Body and this is customobject. You would need to convert it to string, but that would loose the format and the output would be unreadable.
Guess you need to create a HTML code from the output and pass it to Body parameter together with -BodyAsHTML switch.
Leos
@Frank
You can try this:
Hi Luc,
I tried that, the output is a System.Collections.Hashtable and my output was something like this
@Frank
Leos is right, I forgot the Out-String cmdlet.
Here is the fixed code, which I have tested.
Thanks Leos for your eagle eye!
Hi all,
thx for your anwers!
I not want to use the whole function. Only sorted Output for:
$result = dcdiag /s:$DomainController
$result | select-string -pattern '\. (.*) \b(passed|failed)\b test (.*)' | foreach {
$obj = @{
Entity = $_.Matches.Groups[1].Value
TestResult = $_.Matches.Groups[2].Value
TestName = $_.Matches.Groups[3].Value
}
}
$Body = [pscustomobject]$obj | Sort-Object -Property TestResult | ConvertTo-Html -Head $style | Out-String
But i will not work.Ayn Idea?
Frank
Got it right…;-)
$result = dcdiag /s:$DomainController
$result | select-string -pattern '\. (.*) \b(passed|failed)\b test (.*)' | foreach {
$obj = "" | Select-Object TestResult,TestName,Target
$obj.Target = $_.Matches.Groups[1].Value
$obj.TestResult = $_.Matches.Groups[2].Value
$obj.TestName = $_.Matches.Groups[3].Value
$Output += $obj
}
$Body = $Output | Sort-Object -Property TestResult | ConvertTo-Html -Head $style | Out-String
Hi, can you help me to understand why I don’t receive any output?
after imported the function correctly, I run “Invoke-DcDiag -DomainController ” but the output is empty.
It seems to be a problem with this part of the script select-string -pattern ‘\. (.*) \b(passed|failed)\b test (.*)’
because if I change the “-pattern” filter it works…
@Manlio
What's the output when you run the command directly?
@Luke Fullenwarth
the output is correct as it should be , but the issue seems to be the "select-string" filter that's not working correctly, without that and applying another kind of "filter" it works.
Giving back, I added a few things
Hi,
My results are in portuguese. It isn't returning nothing, I have changed the "passed|failed" and still doesn't work.
Here is a sample of my dcdiag command:
@Vandrey
You must replace english words with portugues words in the following command (line 9 of the function):
Probably you have to replace:
@Luc,
Yup.
I've tried that… but the problem is that in portuguese the "failed" translation is "não passou" (like 'not passed').
So it breaks the script. But no problem! Thanks for helping!
@Vandrey
How does it brake the script? What's the error message?
@Luc,
Sorry for the delay… I got the dengue fever and I'm returning to work only today.
There was no error, the problem was that since the translation to portuguese used two separated words, each word appeared on different columns.
I'll try to run it on the server directly, it's on English language.
Thanks!
Worked as expected directly on the server. Thanks!
Hi, I added this simple modification for use if running on DC
$DC = (Get-WmiObject -Class Win32_ComputerSystem -Property Name).Name
Invoke-DcDiag -DomainController $DC
@Marcelo
Yes, that works but is a bit overkill…
There is an environment variable which stores the computer name.
Usually, I set the ComputerName variable with the local computer name as the default value.
Thus when you want to run a script or function on the local computer you don't need to specify the ComputerName parameter.
If use the last version by Pascal, it works fine!!
thank you
Hi,
Thanks for your help. In french it's à little different cause there are several line for test name + result
Démarrage du test : Advertising
……………………. Le test Advertising
de DC a réussi
Démarrage du test : FrsEvent
……………………. Le test FrsEvent
de DC a réussi
How to catch it ?
Hi Damien, Please check Luc's solution above in the comment section(Vandrey Trindade's comment).
Try to write dcdiag output via /f to log file and put it into $result via get-content. This worked for german output.
&dcdiag /f:$env:TEMP\dcdiag.log
$result = Get-Content $env:TEMP\dcdiag.log
That doesn't work cause in french result is on 2 lines and not in 1 line like english or portugese
Understood. You would need some modification in regex expression. Will get back to you as I complete testing new regex in my lab.
Excellent script and suggestions. Just a thought about DNS using /test:dns. Can we incorporate that some how?