Category Archives: Pester

Taking ADUser validation a step further…

β€˜Sup PSHomies,

This is me every single time I sit down to use Pester…

PesterOVF.PNG

Hehe…

While I do enjoy using Pester for operational validation, what do you do with the ones that fail? Most of the time you’re the one doing the validation in the chain of process and everything goes right! Because you’re that good… πŸ˜‰ Hehe…Β  I’ve been recently asked to update some users where some attributes didn’t get populated during a migration… Are you thinking what I’m thinking? πŸ˜‰

The first thing I did was export the attributes they needed, easy enough. Next was to change the necessary attributes to the correct value. The ones that differ will fail (Of course they will Urv, not a total retard here… get on with it… )

Here’s where having proper descriptions can go a long way.

Breadcrumbing

This is what a Test ErrorRecord looks like when you capture the results:PesterError

At first I thought of using regex on the ErrorRecord. Some of theΒ  attributes aren’t set which gave me some issues, so I decided to breadcrumb the name. First do a split on the colon ‘:’ grab the last part and do a split again using ‘/’ to get the attribute name and value. Don’t forget to trim() πŸ™‚

There were some other Attributes that weren’t part of the default parameter set of the Set-ADUser cmdlet. To change those attributes you need to use the DisplayName and the Replace Operator. For the attributes “not set” that need to be cleared, use the Clear operator. Just don’t use both the parameter and the DisplayName! I had EmailAddress and mail in the CSV file, one passed and the other failed… I got rid of mail…

Ok here’s the code to get things done:

First get failed Pester tests.

<#
Author: I. Strachan
Version:
Version History:
Purpose: Validate AD User attributes
#>
[CmdletBinding(SupportsShouldProcess = $True)]
Param(
$csvFile = 'd:\scripts\ps1\source\csv\SetUser.csv'
)
#region Import Csv file
$csvUsers = Import-Csv -Path $csvFile -Delimiter "`t" -Encoding UTF8
$userProperties = ($csvUsers | Get-Member -MemberType NoteProperty).Name |
Where-Object {$_ -ne 'OU'}
#endregion
#region Main
$csvUsers |
Foreach-Object {
$Expected = $_
Describe "Processing User: $($Expected.SamAccountName)" {
Context "Verifying AD User properties for $($Expected.DisplayName)" {
#Get AD user properties
$Actual = Get-ADUser -Identity $Expected.SamAccountName -Properties $userProperties
ForEach( $property in $userProperties){
if (([string]::isNullOrEmpty($Expected.$property))) {
$Expected.$property = $null
$lableExpected = '<not set>'
}
else{
$lableExpected = $Expected.$property
}
it "Verifying user property: $($property) / $($lableExpected)"{
$Actual.$property | Should be $Expected.$property
}
}
}
}
}
#endregion
#Run PesterTest and save results
$resultsTest = Invoke-Pester D:\scripts\ps1\dsa\ADUser.Properties.Tests.ps1 -PassThru
#Get All failed tests
$failedTests = $resultsTest.TestResult.where{$_.Passed -eq $false}
$failedTests |
ForEach-Object{
$result = $_.Name.Split(':')[-1]
$arrResult = $result.Split('/')
[PSCustomObject]@{
SamAccountName = ($_.Describe.split(':').Trim())[-1]
Property = $arrResult[0].Trim()
Expected = $arrResult[1].Trim()
}
} -OutVariable failedObjects
#Export Failed objects
$failedObjects |
Export-Csv -Path D:\Scripts\ps1\source\csv\FailedTests.csv -Delimiter "`t" -NoTypeInformation -Encoding UTF8

This will give me a csv file with the following Columns, SamAccountName, Property & Expected (Value).

The set failed Pester tests will either set or clear the attribute depending on it’s value. If it’s $null it will be cleared.

<#
Author: I. Strachan
Version:
Version History:
Purpose: Set ADUser attributes of failed tests
#>
[CmdletBinding(SupportsShouldProcess = $True)]
Param(
$csvFile = 'D:\Scripts\ps1\source\csv\FailedTests.csv'
)
$FailedTests = Import-Csv -Path $csvFile -Delimiter "`t" -Encoding UTF8
#Get Set-ADUser Parameters
$setADUserParameters = (Get-Command Set-ADUser).ParameterSets.Parameters.Where{ $_.IsDynamic -eq $true} | Select-Object -ExpandProperty Name
#Get User Property
$FailedTests |
Foreach-object{
#Set Expected to null if <not set>
$Expected = @{$true = $null; $false = $_.Expected}[$_.Expected -eq '<not set>']
If ($setADUserParameters.Contains($_.Property)){
$paramSetADUser = @{
Identity = $_.SamAccountName
$_.Property = $Expected
}
Set-ADUser @paramSetADUser
}
else{
if($Expected){
Set-ADUser -Identity $($_.SamAccountName) -Replace @{$_.Property = $Expected}
}
else{
Set-ADUser -Identity $($_.SamAccountName) -Clear $($_.Property)
}
}
}

Here are some screenshots to give you an idea what to expect.

Ideally you’d only have a few failed tests. I wouldn’t use this to reset entire User attributes. Moving and/or renaming an object isn’t supported… yet! πŸ˜‰

So there you have it, taking failed Pester tests values and setting them accordingly!

Hope it’s worth something to you,

Ttyl,

Urv

 

 

PSConfAsia 2017

I recently had the pleasure of presenting at PSConf.asiaΒ in Singapore. This was my very first conference as a speaker! As a first time speaker a great way to get started.

Jaap Brasser approached me at PSConf.EuΒ about doing a session (or two) at PSConf.asia. At first I was hesitant…Β  me, a speaker… Hmmm… I need to think about… Let me get back to you on that… Jaap was clever enough to appease my ego, now how could I say no?

I decided to get out of my comfort zone and said yes, much to Jaap’s delight! Alright! Another PSConf, this time in Singapore, how cool is that!

So after I said yes I did some googling… Let’s see Singapore… Wait what? 12 Hours??? Oh boy… Flying isn’t really my favorite passtime…

ba plane

Hehe… Fortunately I had night flights so…

Singapore has my kinda weather, tropical! I came well prepared. Poor Rob Sewell was melting in Singapore. Transportation is awesome in Singapore. From and to the conference was about 1 SGD.

The organizers did an awesome job! Great location! I met up with old acquaintances and made some new ones!

Finally got to interact with David das Neves. I wanted to attend his session but I opted for Steve Hosking session on Graph. In case you missed it Graph is going to interconnect everything. Definitely have a look at it. I liked David’s approach on having a Class for different types of log format. So I asked him “Hey noΒ  love for Robocopy?” Next thing I knew it was in it! You can read all about here.Β Raimund AndrΓ©eΒ was also there. I told him how NTFSSecurity module saved me on quite a few occasions. He has some serious plans for the module on PowerShell core, so stay tuned!

Rick Taylor won a book on Windows PowerShell for Developers by Doug Finke! Rick was like: “Wait I know this guy!” So I said we need to take a pic featuring Doug’s book! :-). Max Trinidad is a great guy! Been in IT for some time and has loads and loads of skills and it definitely shows!

The keynote is always the highlight of the Conference. Angel Calvo delivered the keynote like a boss! How can I do the key note justice? It’s all about Digital Transformation.

Digital Transformation

Digital tranformation

This is something we all need to consider. It’s not a question of if but when and how. If you’re content with where you’re at right now then you will be obsolete in the near future. What I really appreciated, was the fact that Angel acknowledged that this change can be overwhelming, but it isn’t an all-or-nothing situation. Start where you can, with what you can and take it from there. This digital transformation may take you places you never thought possible. I spoke to Amanda DeblerΒ , her transformation has her working more with kurbernetes, how cool is that?

Azure is about adding value to your business

If you’re still under the impression that Azure is just for offloading servers to the cloud, you’re sorely mistaken. We got some excellent demos by Micheal Greene andΒ Ravi Kiran Chintalapudi on Azure Management Services. Azure is about delivering value to your business. And what has value? Data! Lots and lots of it! Having your servers, application, services just to name a few in Azure, will give you the chance to transform metric and monitoring data into added value for your business. As an OPS guy I always thought of monitoring as a necessary evil. Ravi showed us how to manipulate Azure data that is at your deposition and make it valuable! That one server that isn’t patched adequately or that server that’s missing a configuration, the data is there, it’s up to you to turn that into valuable information.

ChatOps

Michael Greene’s demo on ChatOps blew my mind!

ChatOps

I’ve seen chatbots in action but this will take things to the next level! Here are a few links Michael shared to give you an idea where they’re heading:

PowerShell and the future

When Jeffrey Snover said that PowerShell is finished, I died a little inside. Fortunately I was better prepared this time around when Angel said the same πŸ™‚ . PowerShell has gained critical mass. Moving forward, PowerShell Core is where all attention will be placed. Steve Lee had some interesting charts and number he pulled from GitHub (It’s all about data). Mark Kraus is the nr 1 contributor on GitHub! Michael Greene’s go-to PowerShell version is Core! Joey even had a demo on cross-platform administration starting a session on a Windows and Linux. Unfortunately Joey forget about the Timezone difference, so his servers were down.

Keynote conclusion

Conclusion

Pretty much sums it up, no need to reiterate… πŸ™‚

Networking event

Conferences are a great wayΒ  to interact with the Microsoft Team and delegates. It was awesome meeting Michael Greene and Steve lee in the flesh! I decided on a different approach when it came to mingling. I wanted to connect on a personal level. I can always reach out when I’m working on something. So here I am sitting in an Irish pub in Singapore knocking over a few with Michael, Amanda, Jason and Max! I asked MichaelΒ  what his thoughts were on running Windows for Workgroup 3.11 in Azure… Hypothetically that is :-P. What followed was a lively discussion! Jason had some real great stories to tell! Max told us about his first job in IT back in ’78 when debugging was literally ‘debugging’. Amanda was like: “I wasn’t even born yet…”

Benjamin had Joey finish his demo in the pub. Joey’s demo didn’t go through because of timezone difference (His Azure Servers shutdown automatically after-hours), but Benjamin wasn’t haven’t that. Joey complied and did the demo in the pub, with all attending cheering him on like proper hooligans! For outsiders we may as well have been watching a football game.

I even have my own fan club!

Suresh

Suresh follows my blog and was pretty excited to meet me! Lil’ ol’ me! Suresh made the 12 hour flight worth it! I enjoy meeting my PowerShell heroes in the flesh, I just never considered myself special… Thanks for the support!!! Appreciate it!

Slides & Code

Before I forget here’s the GitHubΒ link to all my presentation slides and code. So Fabian Dibot was at my Infrastructure session and I almost didn’t recognize him! Both him and Mathias JessenΒ had fun sending twitter messages during my session πŸ˜› . Tip: make sure you disable browser notification before starting you presentation… I know, a rookie mistake, you caught me guys… πŸ˜›

Bartek Bielawski was at my Dependencies session! That was a big honor for me! Bartek is the guy you go to when you’re really stuck! While doing my session I was trying to get a read on Bartek’s facial expression. At some point I could see him scanning the code… was that a smile? Ah! no comment whew! I couldn’t wait to ask him for feedback. Getting a compliment from Bartek on presentation preparationΒ  & code definitely gave me a confidence boost! IMHO I enjoyed presenting this session the most. I did this as a flash session at one of our DuPSUG gatherings. I took a different route using AzureAD instead of plain ol’ AD and came across some fun stuff. AD and AzureAD have different parametersets πŸ˜‰

I’m really glad I did the PSConf.asia sessions. Milton GohΒ was hinting on a surprise next year? πŸ™‚ If I’m fortunate to be asked next year, I won’t hesitate!

Thanks for a wonderful experience PSConf.asia! I can’t wait to see where your Digital Transformation takes you!

PSConfAsia

Ttyl,

Urv

 

 

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

Verify GroupMembership with Pester

β€˜Sup PSHomies,

Here’s another advantage of addingΒ members with a different approach, Pester validation!

This makes for an easy way to process validation of Β each member of a group.

Quick update: I’ve added Β some extra code (at the end of the previous blog code)Β toΒ export added- and revoked members.

#region Export for futher processing
$GroupMembers =@{
  Groups  = $Header
  Added   = $addADGroupMembers
  Revoked = $delADGroupMembers
}
$GroupMembers |
Export-Clixml .\export\dsa\ADGroupMembers-$exportDate.xml -Encoding UTF8
#endregion

Quick rundown, first we’ll import the saved object and used that to get a snapshot of the current group members. Then it’s time to vaildate who has been added or revoked.

<#
Author: I. Strachan
Version:
Version History:
Purpose: Validate group membership being added or revoked
#>
[CmdletBinding()]
param(
$xmlFile = 'ADGroupMembers-12102016.xml'
)
#Get saved group members from xmlFile
$SavedGroupMembers = Import-Clixml .\export\dsa\$xmlFile
#region Get current Group memberships
$SnapshotADGroupMembers = @{}
$SavedGroupMembers.Groups |
ForEach-Object{
$SnapshotADGroupMembers.$($_.SamAccountName) = Get-ADGroupMember -Identity $_.SamAccountName| Select-Object -ExpandProperty SamAccountName
}
#endregion
#region Verify members being revoked.
$SavedGroupMembers.Revoked.Keys |
ForEach-Object{
$GroupName = $_
if($SnapshotADGroupMembers.$GroupName){
Describe "AD GroupMembership revoked operational readiness for $GroupName" -Tags Revoked{
Context "Verifying users whose membership has been revoked from $GroupName."{
$SavedGroupMembers.Revoked.$GroupName |
ForEach-Object{
It "User $($_) is not a member of $($GroupName)"{
!($SnapshotADGroupMembers.$GroupName.Contains($_)) | Should be $true
}
}
}
}
}
}
#endregion
#region Verify members being added.
$SavedGroupMembers.Added.Keys |
ForEach-Object{
$GroupName = $_
if($SnapshotADGroupMembers.$GroupName){
Describe "AD GroupMembership added operational readiness for $GroupName" -Tags Added{
Context "Verifying users who are members of $GroupName."{
$SavedGroupMembers.Added.$GroupName |
ForEach-Object{
It "User $($_) is a member of $($GroupName)"{
($SnapshotADGroupMembers.$GroupName.Contains($_)) | Should be $true
}
}
}
}
}
}
#endregion
#region Save Current membership for future reference
$SnapshotADGroupMembers |
Export-Clixml .\export\dsa\SnapshotADGroupMembers-$exportDate.xml -Encoding UTF8
#endregion

adgroupmemberresults

In this case I wanted to generate different Describe blocks. This makes for a better distributionΒ in the HTML report.

adgroupmembernunithtml

Here’s the code to generate the HTML report using reportunit.exe

#region
$exportDate = Get-Date -Format ddMMyyyy
#endregion

#region Main
$pesterGroupMembers = Invoke-Pester .\ps1\dsa\ADGroupMembers*  -OutputFile .\export\dsa\ADGroupMembers.NUnit.xml -OutputFormat NUnitXml -PassThru

#run reportunit against ADgroupMembers.NUnit.xml and display result in browser
&amp; .\tools\ReportUnit\reportunit.exe .\export\dsa\ADGroupMembers.NUnit.xml
Invoke-Item .\export\dsa\ADGroupMembers.NUnit.html

#Export Pester results to xml
$pesterGroupMembers | Export-Clixml .\export\dsa\PesterResults-GroupMembers-$($exportDate).xml -Encoding UTF8
#endregion

Making sure a user is a member can be tricky at times especially when the members list is a few hundred.

As always, snapshots are your friend! When I exported the groups the first time I did it without validating if they existed. I recently ran into a situation where AD Objects were being deleted and recreated using the same SamAccountName! So having a little more information than just the SamAccountName can help when troubleshooting now and in the future.

When my project manager asked for logs and I handed him the HTML generated report of the group members… You should have seen the glee on his face!

So there you have it, verfying group membership using Pester!

Hope it’s worth something to you…

Ttyl,

Urv

SMB shares Operational readiness

Sup’ PSHomies!

In migration mode! It’s been awhileΒ since I’ve done post-configuration of File Servers. I needed to recreate a DFSn structure in our Datacenter. A great opportunity to try out the new dfsn en smb cmdlets! πŸ˜‰

The SMB & DFSn cmdlet work with CIMSessions. Β Before I can create the DFS structure I need to create the shares… Here’s an impression of what the csv file looks like:

Server	ShareName	Path	FullAccess	ChangeAccess	ReadAccess
AZR-FS-01	FS_ARC001$	E:\FS_ARC001	NT AUTHORITY\Authenticated Users
AZR-FS-01	FS_ARC002$	E:\FS_ARC002	NT AUTHORITY\Authenticated Users
AZR-FS-01	FS_ARC003$	E:\FS_ARC003	NT AUTHORITY\Authenticated Users
AZR-FS-01	FS_ARC004$	E:\FS_ARC004	NT AUTHORITY\Authenticated Users
AZR-FS-01	FS_ARC005$	E:\FS_ARC005	NT AUTHORITY\Authenticated Users
AZR-FS-01	FS_ARC006$	E:\FS_ARC006	NT AUTHORITY\Authenticated Users

I’ll be using the servername to retain the cimsession amongst other things. Instead of giving everyone FullAccess to the share, we’re giving ‘Authenticates users’ FullAccess. To complete the list I’ve added Change- and ReadAccess as well even though I won’t be using them.

<#
Author: I. Strachan
Version:
Version History:
Purpose: Create new SMB shares
#>
#region import saved credential and csv File
$Hash = Import-CliXml -Path "${env:\userprofile}\Hash.Cred"
$cred = $Hash.'admstrachan'
$exportDate = Get-Date -Format ddMMyyyy
$csvShares = Import-Csv .\sources\csv\DFSnShares.csv -Delimiter "`t" -Encoding UTF8
#endregion
#region Main
$csvShares |
Foreach-object{
#Get CIMSession
$CIMSession = Get-CimSession -Name $_.Server
#If it doesn't exist create one
if(!$CIMSession){
$CIMSession = New-CimSession -ComputerName $_.Server -Name $_.Server -Credential $cred
}
#Test if Share is available
$Share = Get-SmbShare -Name $_.ShareName -CimSession $CIMSession -ErrorAction SilentlyContinue
if($Share){
#Test if Share path is equal to CSV Share Path
if($_.Path -eq $share.Path){
[PSCustomObject]@{
Server = $_.Server
ShareName = $_.ShareName
Path = $_.Path
Exists = $true
PathCorrect = $true
}
}
else{
[PSCustomObject]@{
Server = $_.Server
ShareName = $_.ShareName
Path = $_.Path
Exists = $true
PathCorrect = $false
}
}
}
else{
#Create folder via UNCPath
$UNCPath = '\\{0}\{1}' -f $_.Server,($_.Path).Replace(':','$')
New-Item -Path $UNCPath -ItemType Directory -Force
New-SmbShare -CimSession $CIMSession -FullAccess $_.FullAccess -Name $_.ShareName -Path $_.Path
[PSCustomObject]@{
Server = $_.Server
ShareName = $_.ShareName
Path = $_.Path
Exists = 'Created'
PathCorrect = $true
}
}
} |
Export-Csv .\export\dfsn\DFSnShares-Created-$($exportDate).csv -Encoding UTF8 -Delimiter "`t" -NoTypeInformation
#endregion
#Remove all CimSessions
Get-CimSession | Remove-CimSession

Here’s a quick rundown of the script.Β I’ve created quite a few checkpoints. First, see if the share exists then verify if the paths are identical. You never know if the share already exists and is pointing to another folder, I’ve learned that from past migration. If it doesn’t exist create it!

The reason for this setup is that I want to be able to re-run this script in the future. Without the check we’d end up with quite some errors… Incidentally, this is how I did my validation pre Pester… πŸ˜‰

Ok now for the fun part!

<#
Author: I. Strachan
Version:
Version History:
Purpose: OVF for SMBShares
#>
[CmdletBinding()]
Param(
$csvFile = 'DFSnShares.csv'
)
#region Import CSV and saved Credentials
$csvDFSnShares = Import-Csv .\sources\csv\$csvFile -Encoding UTF8 -Delimiter "`t"
$savedCreds = Import-CliXml -Path "${env:\userprofile}\Hash.Cred"
$cred = $savedCreds.'admstrachan'
#endregion
#region
$csvDFSnShares |
ForEach-Object{
#If CIMSession doesn't exist create one
if(!$( Get-CimSession -Name $_.Server -ErrorAction SilentlyContinue)){
$CIMSession = New-CimSession -ComputerName $_.Server -Name $_.Server -Credential $cred
}
else{
#Get CIMSession
$CIMSession = Get-CimSession -Name $_.Server
}
#Test if Share is available
$Share = Get-SmbShare -Name $_.ShareName -CimSession $CIMSession -ErrorAction SilentlyContinue
Describe "SMBShare Operation validation share $($_.ShareName) on $($_.Server)" -Tags 'DFSnShares' {
Context "Verifying share properties on $($_.Server)" {
It "Share Name $($_.ShareName) exists" {
$_.ShareName | Should be $Share.Name
}
It "Share Path $($_.Path) exists"{
$_.Path | Should Be $Share.Path
}
It "$($_.FullAccess) has full Access" {
$FullAccess = Get-SmbShareAccess -CimSession $CIMSession -Name $share.Name | Where-Object{$_.AccessRight -eq 'Full'}
$FullAccess.AccountName.Contains($_.FullAccess) | Should be $true
}
}
}
}
#endregion
#region Remove CimSessions
Get-CimSession | Remove-CimSession
#endregion

This time I didn’t make use of snapshots and went straight to the source! Using the csv file I can verify that the shares have been create using the right path and that ‘Authenticated Users’ have Full access.

Screen Shot 2016-07-26 at 12.40.06

Looking at the creation script you might wonder why bother with an OV script (Operation Validation)? As chance would have it, one of the File Server’s Β volume needed to be recreated. Which meant shares had to be recreated (and validated…) Now I wasn’t part of the recreation process, but I could validate that the shares were available as intended once the recreation took place! πŸ˜‰ It’s all about having peace of mind… I just ran the test before posting this blog, everything is as it should be… πŸ™‚

Hope it’s worth something to you

Ttyl,

Urv

 

 

 

 

 

 

Active Directory ReportUnit Pester results

‘Sup PSHomies,

As excited as I get whenever I see all purple (or is it magenta? πŸ˜› ) and green on my test results, how do you report your results?Β I saw this great article on reporting against Pester resultsΒ by Dirk Bremen. I did see a screenshot of this somewhere on twitter but it didn’t register at that time. So having this article was great! You should definitely read his blog!

When I saved the Active Directory operational readiness, I realized that on screen it looked great, but as a report it was kinda flat. So I went back and looked at the code, I had only on Describe!

I decided to categorize the test by functionality and give the test appropriate Tags. Giving the tests tags helps target a specific test, no need to run the whole test to get a specific test πŸ˜‰

Describe AD

Save the output to a XML file. I saw Flynn Bundy use the -PassThru switch to save the results as an object.

Pester ReportUnit

I also exported the results to XML just for good measure. You can always refer to past results if necessary…

Here’s a screenshot with theΒ Forest operational test expanded:

ReportUnit AD Forest

Nice!

A little Slack notification on the side please!

Flynn also used Slack to send a notification. As luck would have it @pscookiemonster has a Slack Module aptly named, wait for it… PSSlack! Warren’s take on Slack is definitely worth reading if you’re considering using Slack as Β a way to communicate with team members.Β I’m quite notorious for stalking my colleagues with unsolicited notification/reports. Slack is a great way to communicate without feeling pushy about it. Information is there, feel free to read it or don’t, your call.

I’m using the summary count of the Pester results for notification purposes.

SlackNotification

Quick tip on the token: Jaap Brasser has a blog about saving credentials safely. Your Slack token should be treated as careful as a password. I saved the token as a password using the Slack team name as the username for reference.Β This makes retrieving the token simple.

SlackToken-Hash

Here’s what the notification looks like in Slack

SlackNotification Resullts

Warren’s module is one to watch!

Ok here the code all put together:

<#
Author: I. Strachan
Version:
Version History:
Purpose: Get Active Directory Report Unit tests and send notification to Slack
#>
#region import module and saved credentials/tokens
Import-Module PSSlack
#Jaap Brassers blog on saving credentials. Saved slack's token as a password
#http://www.jaapbrasser.com/quickly-and-securely-storing-your-credentials-powershell/
$savedCreds = Import-CliXml -Path "${env:\userprofile}\Hash.Cred"
$token = $savedCreds.'udm-slack'.GetNetworkCredential().Password
$exportDate = Get-Date -Format ddMMyyyy
#endregion
#region Main
$pesterADDS = Invoke-Pester .\ps1\dsa\AD.Operations* -OutputFile .\export\adds\ADConfiguration.NUnit.xml -OutputFormat NUnitXml -PassThru
#run reportunit against DFSnShares.NUnit.xml and display result in browser
& .\tools\ReportUnit\reportunit.exe .\export\adds\ADConfiguration.NUnit.xml
Invoke-Item .\export\adds\ADConfiguration.NUnit.html
#Export Pester results to xml
$pesterADDS | Export-Clixml .\export\adds\PesterResults-ADDS-$($exportDate).xml -Encoding UTF8
#endregion
#region Send Slack notification of Pester results
$iconEmoji = @{$true = ':white_check_mark:';$false=':red_circle:'}[$pesterADDS.FailedCount -eq 0]
$color = @{$true='green';$false='red'}[$pesterADDS.FailedCount -eq 0]
#SlackFields
$Fields = [PSCustomObject]@{
Total = $pesterADDS.TotalCount
Passed = $pesterADDS.PassedCount
Failed = $pesterADDS.FailedCount
Skipped = $pesterADDS.SkippedCount
Pending = $pesterADDS.PendingCount
} | New-SlackField -Short
$slackAttachments = @{
Color = $([System.Drawing.Color]::$color)
PreText = 'Active Directory Pester Results'
AuthorName = '@irwins'
AuthorIcon = 'https://raw.githubusercontent.com/irwins/PowerShell-scripts/master/wrench.png&#39;
Fields = $Fields
Fallback = 'Your client is bad'
Title = 'Pester counts'
TitleLink = 'https://www.youtube.com/watch?v=IAztPZBQrrU&#39;
Text = @{$true='Everything passed';$false='Check failed tests'}[$pesterADDS.FailedCount -eq 0]
}
New-SlackMessageAttachment @slackAttachments |
New-SlackMessage -Channel 'powershell' -IconEmoji $iconEmoji -AsUser -Username '@irwins' |
Send-SlackMessage -Token $token
#endregion

Shout-out to you guys for your contribution to the PowerShell community! Keep it up! This is going to make Pester reporting and notifications way easier and not to mention cool! πŸ˜‰

Hope it’s worth something to you

Ttyl,

Urv

 

 

 

 

Operational Readiness validation gotchas

Sup’ PSHomies,

Back from the PSConfEu 2016 in Hannover! It was awesome!!! It was great meeting so many in person! I highly recommend attending a conference if ever given the chance! Tobias did a great job organizing PSConfEU 2016!

The presentations were top notch! Two presentations I definitely wanted to follow were June Blenders’ Real world Pester TDD tests & Ravikanth Chaganti Operations Validation Framework. I enjoyed Ravikanth’s approach: a simplistic and a comprehensive test. June’s presentationΒ was insightful! There were definitely a few aha moments for me! SoΒ I decided to re-evaluate what I had learned so far now that I’ve seen how it should be done! πŸ˜‰ .

Simplistic tests

Think of simplistic testsΒ as kicking the tires. Here’s where the obvious tests go:

  • Are the sets aligned?
  • Did the object count meet your expectation?

Things of that nature. Here are some tests to help you understand some gotchaΒ moments.

Validating the count seems pretty straight forward right? Well, not always… I’ll explain…

When the sets to validate are identical validation is pretty straight forward. This is the best case scenario. I did one validation with and without Group-Object (I’ll explain later on).

Simplistic Identical sets - Count

Simplistic Indentical sets

By using Group-Object I can get the “real” count of a set. Group-Object will gauge the uniqueness of the set, but I also found a test where that might not always be useful… So my next test was to omit an entry from the Verify set

Simplistic Missing entry sets - Count

Simplistic missing an entry in Verify

The test failed as it should. Now for some fun, let’s add a double entry to the Verify set.

 

Simplistic Double entry sets - CountThis is an odd test, it could be a typo. Imagine having a list where double entries aren’t that obvious, this should catch it. Now ideally you’d use your source code that you used during deployment. I’m assuming you did automate your process eh? πŸ˜‰ Without the Group-Object the count is identical. With Group-Object you only have one entry! This could explain an exception happened during deployment… Accidents will happen…

The last test is a fun one: different sets.

Simplistic Different sets - Count

Different sets

Surprise! Both validation count test passed, but the sets are totally different!

Bonus Test!

Simplistic Different sets double entry - Count

Different set with a double entry

This one almost got away. While doing the comprehensive test, it dawned on me that I should be testing both counts, with and without Group-Object. I updated the screenshot accordingly.

Take away Simplistic test:

Don’t only rely theΒ count of a set. By using Group-Object you can gauge a set’s uniqueness. The bonus test showed that exceptions may happen. It’s totally valid as a starting point, that’s why it’s aΒ simplistic tests. Here’s the code for the Simplistic test for count validation:

$savedADConfig= @{
   GlobalCatalogs = @(
      'DC-DSC-01.pshirwin.local'
      'DC-DSC-02.pshirwin.local'
   )
}

$verifyADConfig= @{
   GlobalCatalogs = @(
      'DC-DSC-01.pshirwin.local'
      'DC-DSC-02.pshirwin.local'
   )
}

#region Example Operational validation Simplistic test
Describe 'Active Directory configuration operational readiness' {
   Context 'Verifying GlobalCatalogs count without Group-Object'{
      it 'Total GlobalCatalogs match' {
         @($savedADConfig.GlobalCatalogs).Count |
         Should be @($verifyADConfig.GlobalCatalogs).Count
      }
   }

   Context 'Verifying GlobalCatalogs count with Group-Object'{
      it 'Total GlobalCatalogs match' {
         @($savedADConfig.GlobalCatalogs  | Group-Object).Count |
         Should be @($verifyADConfig.GlobalCatalogs | Group-Object).Count
      }
   }
}
#endregion

If you’d like to try out the simplistic tests just add/remove entries to the saved-/verifyADConfig sets. Now for the Comprehensive tests!

Comprehensive tests

Here’s where in-depth analysis goes. When I did the AD Operational Readiness test, I had a feeling I was missing something. I saw June using sort-object in one of her validations. That triggered me to re-valuate this test.

Comprehensive Identical sets

Comprehensive Identical sets

Depending on which set you used for your enumeration you could end up with different results. When the sets are identical, all goes well. Next test, omit an entry in Verify set.

Comprehensive Missing an entry

Comprehensive missing an entry

Enumerating from the saved test caught the missing entry, enumerating from verify didn’t. Both found ‘DC-DSC-01.pshirwin.local’. The simplistic test caught this, that is why you need both! Next up: Double entry in Verify.

Comprehensive Double entry

Comprehensive double entry in verify

Enumerating from the saved set caught the double entry. Enumerating from the verify set just enumerated the entry twice. If you’re visually inclined, you might miss this.

Hey all my tests results are green and purple! Yeah…

Last test: Different sets.

Comprehensive Different sets

Different sets

At this point you’re comparing apple with oranges. This should fail.

Take away comprehensive tests:

The set you’re enumerating from matters! To cover validation, best bet is to do both! Here’s the code for the Comprehensive Test:

#region Example Operational validation Comprehensive test
$savedADConfig= @{
   GlobalCatalogs = @(
      'DC-DSC-01.pshirwin.local'
      'DC-DSC-02.pshirwin.local'
   )
}

$verifyADConfig= @{
   GlobalCatalogs = @(
      'DC-DSC-01.pshirwin.local'
      'DC-DSC-02.pshirwin.local'
   )
}

Describe 'Active Directory configuration operational readiness' {
   Context 'Verifying GlobalCatalogs enumerating from saved configuration'{
      $savedADConfig.GlobalCatalogs |
      ForEach-Object{
         it "Server $($_) is a GlobalCatalog"{
            $verifyADConfig.GlobalCatalogs.Contains($_) |
            Should be $true
         }
      }
   }
   Context 'Verifying GlobalCatalogs enumerating from verify configuration'{
      $verifyADConfig.GlobalCatalogs |
      ForEach-Object{
         it "Server $($_) is a GlobalCatalog"{
            $savedADConfig.GlobalCatalogs.Contains($_) |
            Should be $true
         }
      }
   }
}
#endregion

Validation is great, but you need to make sure your validating with the right set in the right order!

Summary

  • Create Simplistic & Comprehensive tests.
  • Simplistic tests should take care of the obvious.
  • Comprehensive tests is where in-depth analysis takes place.
  • Validate by enumerating from both sets!
  • Never trust a test that doesn’t fail πŸ˜‰

I’m glad I visited both presentations! Now it’s time to update my Operational readiness tests accordingly! πŸ™‚

Hope it’s worth something to you

Ttyl,

Urv