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:

classadinfra

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… 😉

RunValidation()

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.

desribesourcetarget

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…

pester-poll

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.GetCurrentConfig()
$ADInfra.snapshotDate = $snapShotDate
$ADInfra.ExportADSnapshot()
$ADInfra.ImportADSnapshot()
$ADInfra.ADSnapshot
$ADInfra.RunValidation($ADInfra,$ADInfra.ADSnapshot,@('Forest','Domain'))
$ADInfra.RunValidation($ADInfra.ADSnapshot,$ADInfra,@('Forest','Domain'))
$ADInfra.ActionHistory | Select-Object -Property TimeGenerated,Tags,MessageData
#endregion

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.

currentconfigtest

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 = @(
$primaryDC
)
}
Domain = @{
DistinguishedName = 'DC=pshirwin,DC=local'
InfrastructureMaster = $primaryDC
PDCEmulator = $primaryDC
RIDMaster = $primaryDC
}
Sites = @(
[PSCustomObject]@{
Name = 'Default-First-Site-Name'
}
[PSCustomObject]@{
Name = 'Branch01'
}
)
SiteLinks = @(
[PSCustomObject]@{
Name = 'DEFAULTIPSITELINK'
Cost = 100
ReplicationFrequencyInMinutes = 180
}
)
Subnets = @(
[PSCustomObject]@{
Name = '192.168.0.0/24'
Site = 'CN=Branch01,CN=Sites,CN=Configuration,DC=pshirwin,DC=local'
Location = $null
}
)
DomainControllers = @(
[PSCustomObject]@{
Name = 'DC-DSC-01'
Enabled = $true
IsGlobalCatalog = $true
IsReadOnly = $false
IPv4Address = '10.15.75.250'
}
[PSCustomObject]@{
Name = 'DC-DSC-02'
Enabled = $true
IsGlobalCatalog = $true
IsReadOnly = $false
IPv4Address = '10.15.75.251'
}
)
}
$ADVerifyInfra = [ADInfrastructure]::New(
$ADVerifyConfig.Forest,
$ADVerifyConfig.Domain,
$ADVerifyConfig.Sites,
$ADVerifyConfig.Subnets,
$ADVerifyConfig.SiteLinks,
$ADVerifyConfig.DomainControllers
)
$ADVerifyInfra.snapshotDate = $snapShotDate
$ADVerifyInfra.ImportADSnapshot()
$ADVerifyInfra.RunValidation($ADVerifyInfra,$ADVerifyInfra.ADSnapshot,@('Forest','Domain'))
$ADVerifyInfra.RunValidation($ADVerifyInfra.ADSnapshot,$ADVerifyInfra,@('Forest','Domain'))
$ADVerifyInfra.RunValidation($ADVerifyInfra.ADSnapshot,$ADVerifyInfra,@('DomainControllers'))
$ADVerifyInfra.RunValidation($ADVerifyInfra,$ADVerifyInfra.ADSnapshot,@('DomainControllers'))
$ADVerifyInfra.RunValidation($ADVerifyInfra.ADSnapshot,$ADVerifyInfra,@())
$ADVerifyInfra.ActionHistory | Select-Object -Property TimeGenerated,Tags,MessageData
#endregion

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.

$ADVerifyInfra.RunValidation($ADVerifyInfra.ADSnapshot,$ADVerifyInfra,@('DomainControllers'))

mansnapshotvssource

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

$ADVerifyInfra.RunValidation($ADVerifyInfra,$ADVerifyInfra.ADSnapshot,@('DomainControllers'))

mansourcevssnapshot

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

ActionHistory

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… 😉

actionhistory

ValidationResults

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{
[PSObject]$Forest
[PSObject]$Domain
[PSObject]$Sites
[PSObject]$Subnets
[PSObject]$Sitelinks
[PSObject]$DomainControllers
[String]$snapshotDate
[String]$exportFolder='C:\scripts\export\dsa'
[PSObject]$ADSnapshot
[PSObject[]]$validationResults
[PSObject[]]$ActionHistory
#Default Constructor
ADInfrastructure(){}
#Constructor
ADInfrastructure($frs,$dom,$sit,$sub,$stl,$dcs){
$this.Forest = $frs
$this.Domain = $dom
$this.Sites = $sit
$this.Sitelinks = $stl
$this.Subnets = $sub
$this.DomainControllers = $dcs
}
GetCurrentConfig(){
$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 *)
}
ImportADSnapshot(){
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 *
}
Else{
$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 *
}
}
ExportADSnapshot(){
$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
}
RunValidation($src,$tgt,$tag){
#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 = {
Param($Source,$Target)
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" {
$Source.Forest.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 |
ForEach-Object{
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 |
ForEach-Object{
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…

Ttyl,

Urv

Advertisement

Leave a Reply

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

WordPress.com Logo

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

Facebook photo

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

Connecting to %s