‘Sup PSHomies,
Last blog I demonstrated how to create a HTML report from the Active Directory configuration snapshot. Here’s yet another way to get more use out of the Active Directory configuration snapshot.
I started out with the intention of reporting, then it hit me, why not use the snapshot for Operation readiness? Let’s dive in, I’ll explain as we go along…
Before we get started, you’ll need to have your Active Directory specification at hand. Modify $ADConfiguration according to your specifications.
#region Active Directory configuration as you expect it to be. Modify to reflect your Active Directory $ADConfiguration = @{ Forest = @{ FQDN = 'pshirwin.local' ForestMode = 'Windows2012R2Forest' GlobalCatalogs = @( 'DC-DSC-01.pshirwin.local' ) SchemaMaster = 'DC-DSC-01.pshirwin.local' DomainNamingMaster = 'DC-DSC-01.pshirwin.local' } Domain = @{ NetBIOSName = 'PSHIRWIN' DomainMode = 'Windows2012R2Domain' RIDMaster = 'DC-DSC-01.pshirwin.local' PDCEmulator = 'DC-DSC-01.pshirwin.local' InfrastructureMaster = 'DC-DSC-01.pshirwin.local' DistinguishedName = 'DC=pshirwin,DC=local' DNSRoot = 'pshirwin.local' DomainControllers = @( 'DC-DSC-01' ) } PasswordPolicy = @{ PasswordHistoryCount = 24 LockoutThreshold = 0 LockoutDuration = '00:30:00' LockoutObservationWindow = '00:30:00' MaxPasswordAge = '42.00:00:00' MinPasswordAge = '1.00:00:00' MinPasswordLength = 8 ComplexityEnabled = $true } Sites = @('Default-First-Site-Name') SiteLinks = @( @{ Name = 'DEFAULTIPSITELINK' Cost = 100 ReplicationFrequencyInMinutes = 180 } ) SubNets = @() } #endregion
Quick sidestep, we’re in the middle of implementing a new Infrastructure for a customer. Some post configuration had to be done, FSMO roles rearranged, Global catalogs etc. etc., you know the drill. I got my hand on the Active Directory specifications and filled it in. I did a AD configuration snapshot and was now ready to compare. My colleagues were in the middle of post configuring Active Directory. I noticed that the FSMO roles weren’t as expected. I was missing a Domain Controller and some Sites, subnets and sitelinks. I did a AD snapshot the next day, ran my operation readiness test and surprise, everything was as expected! It wasn’t my intention to supervise my colleagues, but I could give them the good news that the Active Directory is configured as specified.
To give you an idea of what to expect, I did the operation readiness test on my lab. Here’s the script:
<# | |
Author: I.C.A. Strachan | |
Version: 1.1 | |
Version History: | |
08-04-2016 1.0 - First Release | |
12-05-2016 1.1 - Fixed issues with Sitelinks & Subnets. | |
Purpose: Pester script to validate Active Directory configuration. | |
#> | |
[CmdletBinding()] | |
Param( | |
$xmlFile = 'ADReport-12052016.xml' | |
) | |
#region Active Directory configuration as you expect it to be. Modify to reflect your AD | |
$ADConfiguration = @{ | |
Forest = @{ | |
FQDN = 'pshirwin.local' | |
ForestMode = 'Windows2012R2Forest' | |
GlobalCatalogs = @( | |
'DC-DSC-01.pshirwin.local' | |
) | |
SchemaMaster = 'DC-DSC-01.pshirwin.local' | |
DomainNamingMaster = 'DC-DSC-01.pshirwin.local' | |
} | |
Domain = @{ | |
NetBIOSName = 'PSHIRWIN' | |
DomainMode = 'Windows2012R2Domain' | |
RIDMaster = 'DC-DSC-01.pshirwin.local' | |
PDCEmulator = 'DC-DSC-01.pshirwin.local' | |
InfrastructureMaster = 'DC-DSC-01.pshirwin.local' | |
DistinguishedName = 'DC=pshirwin,DC=local' | |
DNSRoot = 'pshirwin.local' | |
DomainControllers = @('DC-DSC-01') | |
} | |
PasswordPolicy = @{ | |
PasswordHistoryCount = 24 | |
LockoutThreshold = 0 | |
LockoutDuration = '00:30:00' | |
LockoutObservationWindow = '00:30:00' | |
MaxPasswordAge = '42.00:00:00' | |
MinPasswordAge = '1.00:00:00' | |
MinPasswordLength = 8 | |
ComplexityEnabled = $true | |
} | |
Sites = @('Default-First-Site-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' | |
} | |
) | |
} | |
#endregion | |
#Import saved AD snapshot | |
$SavedADReport = Import-Clixml .\export\dsa\$xmlFile | |
Describe 'Active Directory configuration operational readiness' { | |
Context 'Verifying Forest Configuration'{ | |
it "Forest FQDN $($ADConfiguration.Forest.FQDN)" { | |
$ADConfiguration.Forest.FQDN | | |
Should be $SavedADReport.ForestInformation.RootDomain | |
} | |
it "ForestMode $($ADConfiguration.Forest.ForestMode)"{ | |
$ADConfiguration.Forest.ForestMode | | |
Should be $SavedADReport.ForestInformation.ForestMode.ToString() | |
} | |
} | |
Context 'Verifying GlobalCatalogs'{ | |
$ADConfiguration.Forest.GlobalCatalogs | | |
ForEach-Object{ | |
it "Server $($_) is a GlobalCatalog"{ | |
$SavedADReport.ForestInformation.GlobalCatalogs.Contains($_) | | |
Should be $true | |
} | |
} | |
} | |
Context 'Verifying Domain Configuration'{ | |
it "Total Domain Controllers $($ADConfiguration.Domain.DomainControllers.Count)" { | |
$ADConfiguration.Domain.DomainControllers.Count | | |
Should be @($SavedADReport.DomainControllers).Count | |
} | |
$ADConfiguration.Domain.DomainControllers | | |
ForEach-Object{ | |
it "DomainController $($_) exists"{ | |
$SavedADReport.DomainControllers.Name.Contains($_) | | |
Should be $true | |
} | |
} | |
it "DNSRoot $($ADConfiguration.Domain.DNSRoot)"{ | |
$ADConfiguration.Domain.DNSRoot | | |
Should be $SavedADReport.DomainInformation.DNSRoot | |
} | |
it "NetBIOSName $($ADConfiguration.Domain.NetBIOSName)"{ | |
$ADConfiguration.Domain.NetBIOSName | | |
Should be $SavedADReport.DomainInformation.NetBIOSName | |
} | |
it "DomainMode $($ADConfiguration.Domain.DomainMode)"{ | |
$ADConfiguration.Domain.DomainMode | | |
Should be $SavedADReport.DomainInformation.DomainMode.ToString() | |
} | |
it "DistinguishedName $($ADConfiguration.Domain.DistinguishedName)"{ | |
$ADConfiguration.Domain.DistinguishedName | | |
Should be $SavedADReport.DomainInformation.DistinguishedName | |
} | |
it "Server $($ADConfiguration.Domain.RIDMaster) is RIDMaster"{ | |
$ADConfiguration.Domain.RIDMaster | | |
Should be $SavedADReport.DomainInformation.RIDMaster | |
} | |
it "Server $($ADConfiguration.Domain.PDCEmulator) is PDCEmulator"{ | |
$ADConfiguration.Domain.PDCEmulator | | |
Should be $SavedADReport.DomainInformation.PDCEmulator | |
} | |
it "Server $($ADConfiguration.Domain.InfrastructureMaster) is InfrastructureMaster"{ | |
$ADConfiguration.Domain.InfrastructureMaster | | |
Should be $SavedADReport.DomainInformation.InfrastructureMaster | |
} | |
} | |
Context 'Verifying Default Password Policy'{ | |
it 'ComplexityEnabled'{ | |
$ADConfiguration.PasswordPolicy.ComplexityEnabled | | |
Should be $SavedADReport.DefaultPassWordPoLicy.ComplexityEnabled | |
} | |
it 'Password History count'{ | |
$ADConfiguration.PasswordPolicy.PasswordHistoryCount | | |
Should be $SavedADReport.DefaultPassWordPoLicy.PasswordHistoryCount | |
} | |
it "Lockout Threshold equals $($ADConfiguration.PasswordPolicy.LockoutThreshold)"{ | |
$ADConfiguration.PasswordPolicy.LockoutThreshold | | |
Should be $SavedADReport.DefaultPassWordPoLicy.LockoutThreshold | |
} | |
it "Lockout duration equals $($ADConfiguration.PasswordPolicy.LockoutDuration)"{ | |
$ADConfiguration.PasswordPolicy.LockoutDuration | | |
Should be $SavedADReport.DefaultPassWordPoLicy.LockoutDuration.ToString() | |
} | |
it "Lockout observation window equals $($ADConfiguration.PasswordPolicy.LockoutObservationWindow)"{ | |
$ADConfiguration.PasswordPolicy.LockoutObservationWindow | | |
Should be $SavedADReport.DefaultPassWordPoLicy.LockoutObservationWindow.ToString() | |
} | |
it "Min password age equals $($ADConfiguration.PasswordPolicy.MinPasswordAge)"{ | |
$ADConfiguration.PasswordPolicy.MinPasswordAge | | |
Should be $SavedADReport.DefaultPassWordPoLicy.MinPasswordAge.ToString() | |
} | |
it "Max password age equals $($ADConfiguration.PasswordPolicy.MaxPasswordAge)"{ | |
$ADConfiguration.PasswordPolicy.MaxPasswordAge | | |
Should be $SavedADReport.DefaultPassWordPoLicy.MaxPasswordAge.ToString() | |
} | |
} | |
Context 'Verifying Active Directory Sites'{ | |
$ADConfiguration.Sites | | |
ForEach-Object{ | |
it "Site $($_)" { | |
$SavedADReport.Sites.Name.Contains($_) | | |
Should be $true | |
} | |
} | |
} | |
Context 'Verifying Active Directory Sitelinks'{ | |
$lookupSiteLinks = $SavedADReport.Sitelinks | Group-Object -AsHashTable -Property Name | |
$ADConfiguration.Sitelinks | | |
ForEach-Object{ | |
it "Sitelink $($_.Name)" { | |
$_.Name | | |
Should be $($lookupSiteLinks.$($_.Name).Name) | |
} | |
it "Sitelink $($_.Name) costs $($_.Cost)" { | |
$_.Cost | | |
Should be $lookupSiteLinks.$($_.Name).Cost | |
} | |
it "Sitelink $($_.Name) replication interval $($_.ReplicationFrequencyInMinutes)" { | |
$_.ReplicationFrequencyInMinutes | | |
Should be $lookupSiteLinks.$($_.Name).ReplicationFrequencyInMinutes | |
} | |
} | |
} | |
Context 'Verifying Active Directory Subnets'{ | |
$lookupSubnets = $SavedADReport.SubNets | Group-Object -AsHashTable -Property Name | |
$ADConfiguration.Subnets | | |
ForEach-Object{ | |
it "Subnet $($_.Name)" { | |
$_.Name | | |
Should be $lookupSubnets.$($_.Name).Name | |
} | |
it "Site $($_.Site)" { | |
$_.Site | | |
Should be $lookupSubnets.$($_.Name).Site | |
} | |
} | |
} | |
} |
And here’s the result:
My testlab is quite simple.
Validating operation readiness will definitely help you keep things in check! No second guessing: “Did I configure server x as a Global catalog? With the AD Configuration snapshot you can be certain how you left things! “I know for a fact I configured the server as a Global catalog last week.” Compare your past snapshot to what you’re expecting. Create a new snaphot and compare again. If it’s different… Well… Sometimes colleagues forget to communicate changes that have been made… At least you don’t have to second guess yourself 😉 As an OPS guys Operation readiness has my vote!
Hope it’s worth something to you
Ttyl,
Urv
Hey Irwin – thanks for the great post. One of my listeners over on RunAs Radio mentioned (show 469) it as a clever example of Configuration-as-Code and I agree! I read his comment on episode 472 and I’ll include a link in the show notes. Thanks!
LikeLiked by 1 person
Wow!!! I’m honored!
LikeLike
Pingback: Some Pester Tests for SQL Defaults – SQL DBA with A Beard
Pingback: Continuously Testing your Infrastructure with OVF and Microsoft Operations Management Suite | PowerShell, Programming and DevOps
Pingback: Active Directory Operational Testing « The Surly Admin
Pingback: AD Operation Validation class | pshirwin
Pingback: Episode 319 - PowerScripting Podcast - MVPs Don Jones and Adam Bertram on Pester
Pingback: Episode 319 - PowerScripting Podcast - MVPs Don Jones and Adam Bertram on Pester | PowerShell.org