AD Operation Validation class

‘Sup PSHomies,

I’m like a dog with a bone… 😛

2016 was all about operation validation for me. I did a series on Active Directory snapshot, report and validation that was well received by the community! Classes will definitely make the user experience more pleasant! I decided to refactor the code to a class 😉 Got a lot of ground to cover so let’s dive in!

Here’s a screenshot of the ADInfrastructure (was what popped in my mind at the time) class properties and methods:


The focus will be on Active Directory’s forest, domain, sites, sitelinks, subnets and domaincontrollers.

These are the methods I’ve worked out so far (work in progress):

GetCurrentConfig() will populate the necessary properties using the right activedirectory cmdletsgetcurrentconfig

ImportADSnapshot() will import a saved XML file to ADSnapshot property.importadsnapshot

ExportADSnapshot() saves the class object as a XML file. This will generate a new XML file using the current $exportDate valueexportadsnapshot

RunValidation() deserves it own section… 😉


This is where the the operation validation will take place. This was a bit of a challenge getting the method right, but I think it worked out just fine… I should explain…

In my first attempt I ran the validation directly from the method. You can invoke the Describe block just like a function. That wasn’t the challenge, have a look at the It blockitblock

The test is hard coded to use $this as source and $this.ADSnapshot as target. I blogged about some possible validation gotchas a while back. To remedy this, I decided to use a scriptblock. You can also provide parameters to a scriptblock . In this case I provided ($Source, $Target) as parameters. This will make interchanging  input easier of which I’ll explain the advantages later on.


Ok so the scriptblock was a good idea. One of the things I wanted to do was save the validation results. That’s where I ran into something interesting. The input has to be a *.tests.ps1 file(s). I tried using the scriptblock as input but that didn’t work. I visited the github page to see if scriptblock is supported as a feature, it isn’t. In order to save the results I would first have to save the test to a file. The scriptblock made that part a lot easier. As a workaround, this isn’t an issue.The file is generate every time RunValidation is executed, an inconvenience at most.  A scriptblock feature  would make for a cleaner approach.

Quick side step: There’s a poll on twitter for anyone interested in casting a vote 🙂 Only 5 more days left…


The test results are saved in ValidationResults. That’s RunValidation in a nutshell.

It’s quite a bit of code, so I’ll post that at the end of the blog. Here’s what you can expect if you try it out. First up, a simple verification of the current configuration against a saved snapshot

#region Verify Current Configuration against a snapshot
$snapShotDate = '12012017'
$ADInfra = [ADInfrastructure]::New()
$ADInfra.snapshotDate = $snapShotDate
$ADInfra.ActionHistory | Select-Object -Property TimeGenerated,Tags,MessageData

Before you get startetd, you need to instantiate the class. GetCurrentConfig() will save the information to the properties. ExportADSnapshot() will create a ADSnapshot-($exportDate).xml file. ImportADSnapshot will import any existing snapshot file of a given $snapshotDate formatted as ‘ddMMyyyy’.

Because I’m verifying the current configuration with a snapshot without any changes all the tests will pass.


No surprises.

For the next example, I wanted to validate against a manual configuration. This is where the scriptblock really made a difference. I added a non-existent DC to  $ADVerifyConfig for testing purposes.


#region verify a manual Configuration a against a snapshot
$primaryDC = 'DC-DSC-01.pshirwin.local'
$ADVerifyConfig = @{
Forest = @{
Name = 'pshirwin.local'
ForestMode = 'Windows2012R2Forest'
DomainNamingMaster = $primaryDC
SchemaMaster = $primaryDC
GlobalCatalogs = @(
Domain = @{
DistinguishedName = 'DC=pshirwin,DC=local'
InfrastructureMaster = $primaryDC
PDCEmulator = $primaryDC
RIDMaster = $primaryDC
Sites = @(
Name = 'Default-First-Site-Name'
Name = 'Branch01'
SiteLinks = @(
Cost = 100
ReplicationFrequencyInMinutes = 180
Subnets = @(
Name = ''
Site = 'CN=Branch01,CN=Sites,CN=Configuration,DC=pshirwin,DC=local'
Location = $null
DomainControllers = @(
Name = 'DC-DSC-01'
Enabled = $true
IsGlobalCatalog = $true
IsReadOnly = $false
IPv4Address = ''
Name = 'DC-DSC-02'
Enabled = $true
IsGlobalCatalog = $true
IsReadOnly = $false
IPv4Address = ''
$ADVerifyInfra = [ADInfrastructure]::New(
$ADVerifyInfra.snapshotDate = $snapShotDate
$ADVerifyInfra.ActionHistory | Select-Object -Property TimeGenerated,Tags,MessageData

The class is instantiated differently this time. The values are added externally. If you run GetCurrentConfig() at any point, it will rewrite the default values. Here are the results of the snapshot vs manual source first.



The snapshot only has one DomainController, we never get to the second DomainController. Now if we switch parameters from positions…



Ah! DC-DSC-02 doesn’t exist in the snapshot so it will fail! There are always two sides to consider. RunValidation() makes it easier to test and verify both sides…

Bonus round


I recently discovered the Information stream in PowerShell v5. I decided to make use of Write-Information to log activities as I go along. This makes for easier troubleshooting of actions and/or sequences of methods being executed, couldn’t hurt… 😉



Saving the validation test result enables you to process the results in different ways.

For starters you can use Format-Pester by Erwan Quélin to generate documentation of the results. Now because it’s an object you can just as easily run a query:

$ADVerifyInfra.validationResults.Results.TestResult.Where{$_.Passed -eq $false}

You can even send a high-level overview to Slack (It’s on my to-do list).

Whew! I think I’ve covered all the essentials… Ok as promised the code:

Class ADInfrastructure{
#Default Constructor
$this.Forest = $frs
$this.Domain = $dom
$this.Sites = $sit
$this.Sitelinks = $stl
$this.Subnets = $sub
$this.DomainControllers = $dcs
$MessageData = "Get current Active Directory configuration"
$this.ActionHistory += Write-Information -MessageData $MessageData 6>&1 -Tags 'Get','CurrentConfig','AD' | Select-Object *
$this.Forest = $(Get-ADForest)
$this.Domain = $(Get-ADDomain)
$this.DomainControllers = $(Get-ADDomainController -Filter *)
$this.Sites = $(Get-ADReplicationSite -Filter *)
$this.Subnets = $(Get-ADReplicationSubnet -Filter *)
$this.Sitelinks = $(Get-ADReplicationSiteLink -Filter *)
if(Test-Path "$($this.exportFolder)\ADSnapshot-$($This.snapshotDate).xml"){
$this.ADSnapshot = Import-Clixml "$($this.exportFolder)\ADSnapshot-$($This.snapshotDate).xml"
$MessageData = "Imported ADSnapshot from $($this.SnapshotDate)"
$this.ActionHistory += Write-Information -MessageData $MessageData 6>&1 -Tags 'Get','ADSnapshot','Found' | Select-Object *
$MessageData = "ADSnapshot from $($this.SnapshotDate) not found"
Write-Warning -Message $MessageData
$this.ActionHistory += Write-Information -MessageData $MessageData 6>&1 -Tags 'Get','ADSnapshot','Missing' | Select-Object *
$MessageData = "Saving ADSnapshot"
$this.ActionHistory += Write-Information -MessageData $MessageData 6>&1 -Tags 'Save','ADSnapshot' | Select-Object *
$exportDate = Get-Date -Format ddMMyyyy
$this | Export-Clixml "$($this.exportFolder)\ADSnapshot-$($exportDate).xml" -Encoding UTF8
#Something with Tags
$MessageData = "Validating AD Configuration against saved snapshot from $($this.SnapShotDate)"
$this.ActionHistory += Write-Information -MessageData $MessageData 6>&1 -Tags 'Validation','ADSnapshot','CurrentConfig','AD' | Select-Object *
$sbValidation = {
Describe 'AD Forest configuration operational readiness' -Tags Forest {
Context 'Verifying Forest Configuration'{
it "Forest Name $($Source.Forest.Name)" {
$Source.Forest.Name |
Should be $Target.Forest.Name
it "Forest Mode $($Source.Forest.ForestMode)" {
$Source.Forest.ForestMode |
Should be $Target.Forest.ForestMode
it "$($Source.Forest.DomainNamingMaster) is DomainNamingMaster" {
Should be $Target.Forest.DomainNamingMaster
it "$($Source.Forest.DomainNamingMaster) is SchemaMaster"{
$Source.Forest.SchemaMaster |
Should be $Target.Forest.SchemaMaster
Describe 'AD GlobalCatalog configuration operational readiness' -Tags GlobalCatalog {
Context 'Verifying GlobalCatalogs'{
$Source.Forest.GlobalCatalogs |
it "Server $($_) is a GlobalCatalog"{
$Target.Forest.GlobalCatalogs.Contains($_) |
Should be $true
Describe 'AD Domain configuration operational readiness' -Tags Domain{
Context 'Verifying Domain Configuration'{
it "Domain DN is $($Source.Domain.DistinguishedName)" {
$Source.Domain.DistinguishedName |
Should be $Target.Domain.DistinguishedName
it "$($Source.Domain.InfrastructureMaster) is InfrastructureMaster"{
$Source.Domain.InfrastructureMaster |
Should be $Target.Domain.InfrastructureMaster
it "$($Source.Domain.PDCEmulator) is PDCEmulator"{
$Source.Domain.PDCEmulator |
Should be $Target.Domain.PDCEmulator
it "$($Source.Domain.RIDMaster) is RIDMaster"{
$Source.Domain.RIDMaster |
Should be $Target.Domain.RIDMaster
Describe 'AD DomainControllers configuration operational readiness' -Tags DomainControllers {
$lookupDC = $Target.DomainControllers | Group-Object -AsHashTable -AsString -Property Name
ForEach($dc in $Source.DomainControllers){
Context "Verifying DC $($dc.Name) Configuration"{
it "Is enabled " {
$dc.Enabled | Should be $lookupDC.$($dc.Name).Enabled
it "Is GC " {
$dc.IsGlobalCatalog | Should be $lookupDC.$($dc.Name).IsGlobalCatalog
it "ReadOnly is $($dc.IsReadOnly) " {
$dc.IsReadOnly| Should be $lookupDC.$($dc.Name).IsReadOnly
it "IPv4Address is $($dc.IPv4Address)" {
$dc.IPv4Address | Should be $lookupDC.$($dc.Name).IPv4Address
Describe 'AD Sites operational readiness' -Tags Sites {
Context 'Verifying Sites'{
$Source.Sites |
it "Site $($_.Name)"{
$Target.Sites.Name.Contains($_.Name) |
Should be $true
Describe 'AD Subnets operational readiness' -Tags Subnets{
$lookupSubnets = $Target.SubNets | Group-Object -AsHashTable -AsString -Property Name
ForEach($subnet in $Source.Subnets){
Context "Verifying Subnet $($subnet.Name)"{
it "Subnet name is $($subnet.Name)"{
$subnet.Name | Should be $lookupSubnets.$($subnet.Name).Name
it "Subnet location is $($subnet.Location)"{
$subnet.Location | Should be $lookupSubnets.$($subnet.Name).Location
it "Subnet associated site is $($subnet.Site)"{
$subnet.Site | Should be $lookupSubnets.$($subnet.Name).Site
Describe 'AD Sitelinks operational readiness' -Tags SiteLinks {
$lookupSiteLinks = $Target.Sitelinks | Group-Object -AsHashTable -AsString -Property Name
ForEach($sitelink in $Source.Sitelinks){
Context "Verifying Sitelink $($sitelink.Name)"{
it "Sitelink name is $($sitelink.Name)"{
$sitelink.Name | Should be $lookupSiteLinks.$($sitelink.Name).Name
it "Sitelink cost is $($sitelink.Cost)"{
$sitelink.Cost | Should be $lookupSiteLinks.$($sitelink.Name).Cost
it "Sitelink replication frequency (min) is $($sitelink.ReplicationFrequencyInMinutes)"{
$sitelink.ReplicationFrequencyInMinutes| Should be $lookupSiteLinks.$($sitelink.Name).ReplicationFrequencyInMinutes
$pesterFile = "$($this.exportFolder)\ADInfra.tests.ps1"
$sbValidation.ToString() | out-file -FilePath $pesterFile -Force
$testADInfra = @(
Path = $pesterFile
Parameters = @{
Source = $src
Target = $tgt
$this.ValidationResults += [PSCustomObject]@{
ValidationDate = $(Get-Date)
Results = Invoke-Pester -Path $testADInfra -PassThru -Tag $tag

Classes will definitely enhance your end-user’s experience…

Hope it’s worth something to you…




Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s