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.

 

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:

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

Hope it’s worth something to you…

Ttyl,

Urv

RoboCopy class

‘Sup PSHomies,

It all started a year ago… Always wanting to learn anything PowerShell related, classes caught my eye ever since it was introduced in v5.  I wanted to try my hand at classes with a real life application… So I got on twitter for some tips…

powershell-class-tweet-2

powershell-class-tweet-3

Doug was kind enough to reach out and point me in the right direction, for which I owe him a great debt! Appreciate it Doug!!!

Like I said, I wanted to try my hand at classes with a real life application… If you’ve read my blogs then you’ll know that I’m a fan of robocopy, seriously, huge fan! . Did I mention how awesome robocopy is? 😛 I think I found my real life application 😉

When I started out with my Robocopy class, it was just about logging initially, but it could be so much more! Classes are native to v5. Now that v5 is mainstream I decided to finish the class. Richard Siddaway’s article  was just the spark I needed to get me going (again)!

Here’s what the Robocopy class looks like:

robocopy-class

Here a quick rundown on the properties:

The source/destination properties of the class are self explanatory (if you’re familiar with robocopy). The property logdir and JobID will be used to define a valid logfile name (with extension .log). Robocopy has quite a bit of options. I wanted to keep it as generic as possible. The property $this.Options is still a work in progress. The property $this.WildCards  is where you’ll define what will be filtered.  I’ll get back to rcCMDs and rcExitCodes later on…

These are the methods I came up with (so far, still a work in progress)

  • Mirror(). Mirrors $this.Source to $this.Destination with some default options
  • Move(). Moves this.Source to $this.Destination with some default options
  • RollBack(). Rollback $this.Destination to $this.Source with some default options
  • Sync(). Sync will synchronize the delta’s from $this.Source to $this.Destination using any additional $this.Options defined (at least that’s the idea). I’ve added a few options by default, mostly to exclude files and folders, think recycle.bin “System Volume Information” and the likes.
  • VerifyPaths(). This let’s you know if the $this.Source, $this.Destination and $this.LogDir are valid.
  • GetlistSource(). This will list the content of the $this.Source
  • GetListDestionation(). This will list the content of $this.Destination
  • GetLogSummary. This will return a summary of the log file (Hehe). The method is static so that you don’t have to instantiate the class in order to use it. (Thanks again Doug!)

The two methods: StartRCProcess and ExecuteRCCMD are actually helper methods. I just haven’t figured out how that works in classes. Ideally I’d like to have them hidden or as a function if that even makes sense. So here’s where they come in. At first I just executed robocopy with the necessary arguments. If you’re not interested in the exitcode then using ExecuteRCCMD is what you need. I wrote a blog about enumerating RoboCopy Exitcodes. Using $LastExitCode isn’t going to cut it if you decide to run robocopy jobs parallel. That’s where StartRCProcess comes in.Using Start-Process comes with an overhead of say 20 MB, which could add up in the long run. You do need to wait until the process has finished to retrieve the exitcode. If you really need the exitcode then StartRCProcess is what you need. The property $this.rcExitCodes will only be populated if StartRCProcess is used. Both will populate the $this.rcCMDs property.

Ok I think I’ve covered the basics, time to show some code! 😉

Here what’s happening in the List methods:

methods-lists

GetListSource() is using $this.StartRCProcess to generate a list of $this.Source using some default option. While writing I noticed that I forgot to add the wildcards to the parameter. All I had to do was add it!. I added it at the beginning so it lines up accordingly… Robocopy is fickle like that…  GetListDestination does the same only it uses ExecuteRCCMD instead.

Here’s what’s going on in StartRCProcess and ExcuteRCCMD

startexecuterc

Both StartRCProcess and ExcuteRCCMD will save the robocopy command using Write-Information. I’m loving Write-Information more and more! StartRCProcess saves the exitcode with some extra information. Here’s where the robocopy exitcode enumeration came in handy! ExecuteRCCMD will run robocopy with the specified arguments. Truth be told I’m more partial to the ExecuteRCCMD method. I added the StartRCProcess more for demo purposes and finally getting to use my Robocopy exitcode enumeration!

For Mirror(),Move() and RollBack(), I omitted the Wildcards. These methods all or nothing in my opinion. If omitted, . will be the default.

Sync() had me going for a while. I still have some issues with Options. For now Sync() uses some default switches. Like I said work in progress…

Quite a bit of code, so does it work? Here’s some code to play with. be sure to edit the source,destination and logdir to your liking. Just remember that robocopy is unforgiving so make sure not to use it of production folders!

#region Main
$rc = [RoboCopy]::New('C:\scripts\move','C:\temp\move','rc-0001','c:\scripts\log',@('*.*'))

#Run RoboCopy methods
$rc.Sync()
$rc.GetListSource()
$rc.GetListDestination()

#Get RoboCopy LogFile Summary
[RoboCopy]::GetLogSummary("$env:HOMEDRIVE\scripts\log\listSRC-rc-0001.log")
[RoboCopy]::GetLogSummary("$env:HOMEDRIVE\scripts\log\listDES-rc-0001.log")
[RoboCopy]::GetLogSummary("$env:HOMEDRIVE\scripts\log\sync-rc-0001.log")

#Get RoboCopy executed CMDs
$rc.rcCMDs
$rc.rcExitCodes
#endregion

First I instantiate the class with arguments. I then run the methods Sync(),GetListSource() and GetListDestination(). Up next is retrieve the LogSummaries from the methods. Here’s a screenshot of the Sync LogSummary

synclogfile

I did a select of $rc.rcCMDs to give you an idea what is being stored

rc-rccmds

Only want ListDES?

$rc.rcCMDs |
Where-Object{$_.Tags -contains 'ListDes'} |
Select-Object -Property Time*,Tag*,Mess*

rc-rccmdswhereobject

The information stream is quite handy! The tags will definitely come in handy when you need to filter on action verb or job ID.

The methods GetListSource() & Mirror() both make use of StartRCProcess(), so let’s see what $rc.rcExitcode has shall we?

rcexitcodes

Nice!

This is by far my longest blog, if you made this far then… Congratulations! There’s still so much to discover when it comes to classes.

Classes definitely  makes your code look and feel more like a developer 😉 . I feel more comfortable giving a colleague this class than a bunch of scripts. In Richard’s article he’s using both classes and modules. There are sure to be some gotcha’s… Do you go all in with classes or only partial?

I’m hoping that the community can shed some light on the subject. I’d love to hear from you guys on how to improve on this… Let’s make this year, a year of PowerShell Classes! 😛

Hope it’s worth something to you…

Ttyl,

Urv

 

A simple logger using PowerShell Class

‘Sup PSHomies,

At our last DuPSUG meeting, Jaap Brassers did a short Demo on the latest addition to PowerShell Streams, Information. How did I miss this?

So what’s so great about the new Information stream? The Information stream solves the Write-Host issue of not sending output to a stream like: error;verbose;warning etc. As such we were told that Write-Host is evil and every time you use it a puppy dies… Who wants that on their conscious right?

Well with the Information stream Write-Host is allowed… no more puppy genocide!

Jaap showed us what the stream output looks like and how to write to the information stream. Here’s what the output looks like

informationstream

Here are a few properties we can play with: TimeGenerated,Source,User,Computer,Tags and MessageData. You get all of this by writing/redirecting to the information stream. The TimeGenerated property caught my eye, imagine creating a timeline of sorts. While googling (don’t act like it’s just me…) I came across a great article by Jeff Hicks on the subject (Seriously, how did I miss this?) In it he also talked about generating a timeline view… 😉

After I had some time to think about how to make use of this, it dawned on me… You know what else is native to PowerShell v5? Classes! When Classes were introduced in v5, they were another way to write DSC Resources. I’ve always thought that there’s more to classes than meets the eye.

If you’re interested in classes, Richard Siddaway has a great article taking you through each step, worth trying out.

Jaap’s demo gave me the idea to try my hand at a simple logger using classes. Here’s what I came up with…

Here’s a quick rundown:

construtors

The idea is to gather output from the Information stream. Constructors are a way of initiating an object for use (Shout out to Doug Finke for breaking this down for me!)

writeloginformation

I “borrowed” Jeff Hicks idea of validating the preference first. This way you can toggle between showing Write-Information and/or Write-Host data. You can use Write-Host for a more colorful experience. Did I already mention that Write-Host is cool again? :-P.  I also added the possibility to show data in the console if you choose to, a verbose action of sorts…

exportlogger

I also added two methods of exporting: text and xml. That’s where LogDirectory and FileBaseName come in. For the text export, I selected TimeGenerated and MessageData for a timeline view (I know I know Jeff Hicks did it first :-P). Export to xml saves InfoMessages in raw format.

Here’s some code to play with.

function Get-PowerShellProcess{
   [cmdletbinding()]
   param()

   Write-Verbose "InformationPreference = $InformationPreference"
   $Logger.Preference = $InformationPreference
   $Logger.WriteInformation("Starting $($MyInvocation.MyCommand)")

   Get-Process -Name *PowerShell*
   $Logger.WriteInformation("Finishing $($MyInvocation.MyCommand)")
}

#region Main
#Initiate variable
$Logger = [WriteLog]::New('psProcess','C:\scripts\temp')

#Run with InformationAction. Information will be saved and not shown
Get-PowerShellProcess -InformationAction Continue -Verbose

#Run without InformationAction with Console set to $true
$Logger.Console = $true
Get-PowerShellProcess -Verbose

#Run without InformationAction with Console set to $false
$Logger.Console = $false
Get-PowerShellProcess -Verbose
#endregion

Here are some screenshots of the output

I initiated the object a FileBaseName value ‘psProcess’ and LogDirectory ‘C:\scripts\temp’. Default Preference is ‘SilentContinue’ and no ouput to the console

initiate

First up run with action set to continue

loggerinfoaction

$Logger.InfoMessages has redirected data.

loggerinfomessages

When console is set to true we’ll see the Information message without explicitly setting InformationAction to ‘continue’. Using InformationAction will save the Information data to InfoMessages property.

infostreamconsole

Setting the console preference back to $false stops displaying output to the console

infostreamconsolefalse

I hope that you were able to follow it all. 2016 was all about Operation Validation for me. I think classes are going to be mainstream in 2017! PowerShell + Classes will definitely give you an edge and not to mention make you feel more like a developer! 😉

The line between Dev and Ops is being blurred…

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.

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
& .\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

AD Security Group matrix

‘Sup PSHomies,

So last blog was about adding members to a security group efficiently. Which got me thinking, can I reverse this process? If given the security groups with specified members, can I recreate the csv? I do love me a challenge!

So to pick up where we left off, I’ll be using the $addADGroupMembers to repopulate the csv…

#Security Matrix
$Groups = $addADGroupMembers.Keys
function Convert-ArrayToHash($a){
    begin { $hash = @{} }
    process { $hash[$_] = $null }
    end { return $hash }
}

$template = [PSCustomObject]([Ordered]@{UserID=$null} + $($Groups | Convert-ArrayToHash))

$addADGroupMembers has all the group names we need. I’m converting the group names into an empty hashtable. The $template variable is a custom object I’ll be using to move things  along, I’ll explain as we go…

matrix-template

Now for the tricky part…

$arrMatrix = @()

$Groups |
ForEach-Object{
   $GroupName = $_
   if($addADGroupMembers.$_){
      $addADGroupMembers.$_ |
      ForEach-Object{
         if($arrMatrix.Count -eq 0) {
            $newItem = $template.PSObject.Copy()
            $newItem.UserID = $_
            $newItem.$GroupName = '1'
            $arrMatrix += $newItem
         }
         else{
            if($arrMatrix.UserID.contains($($_))){
               $index = [array]::IndexOf($arrMatrix.UserID, $_)
               $arrMatrix[$index].$GroupName = '1'
            }
            else{
               $newItem = $template.PSObject.Copy()
               $newItem.UserID = $_
               $newItem.$GroupName = '1'
               $arrMatrix += $newItem
            }
         }
      }
   }
}

First we have an array to save the results. We only need to worry about groups with members. The $template has been initialized with $null. The first time it runs, $arrMatrix.Count will be zero, so just add this group to get things started. Here’s where it gets interesting, in order to add a newItem to the array I have to clone it first. Adding and saving this way makes sure I have a row with a unique UserID. Truth be told I had to google to figure this one out. Modifying $template and then assigning it to $newItem will only assign the reference. Change the value once more and every item in the array changes! I read it somewhere, it was fun to stumble on this… The more you know…

The next trick was to find the index of a UserID already saved. Google to the rescue! I found this neat trick of using [array]::IndexOf(). This will give you the first index with that value. Lucky for me, my UserIDs are unique 😉
Once I have my index I can add a value of ‘1’ to the group if the UserID is a member. If I can’t find a UserID then a new unique UserID is added to the $arrMatrix

Ok enough chit-chat here’s some code to play with

This should be your endresult

securitymatrixresults

No too shabby eh? 😛

Ok Urv that’s all good and well but when am I going to use this?

Why thank you for asking!

If you’ve ever been in charge of implementing Role Based Access Control then you could appreciate this. A security matrix like this is where I’d start, only now you don’t have to start from scratch… 😉

Here’s how it works…

adsecuritymatrix

I created a security group Rol-Consultant for RBAC purposes. This group is a member of all the APP-* groups giving any member access by way of group nesting. Users who are a member of Rol-Consultant don’t have to be a direct member for access. The down side of RBAC is it’s all or nothing, exceptions are real deal breakers…

I did a blog about reporting a user’s nested group membership. Let take user ‘dlbouchlaghmi’. This is what his effective user group membership looks like in list form

nestusergroupmembership

The security matrix makes it a bit more visual. Granted, it takes some getting use to but the information is there. Now you can ‘fix’ any issues and reapply the way you see fit! 😉

This has been on my radar for quite some time. Processing security groups this way, makes scripting, I wouldn’t say easier, but more easier to manipulate if you catch my drift…

Ok here’s the code to get the ADSecuirtyMatrix. Do be careful with groups with large memberships. I tried my hand at a group with more than 3800 member, took a couple of minutes, but it worked.

I got one more use to go… Some Operation validation! Stay tuned…

Hope it’s worth something to you…

Ttyl,

Urv

Add Members to Group – a different approach

‘Sup PSHomies,

My Project Manager is slowly becoming a  true PowerShell believer! Of course he doesn’t have to do the actual scripting, that’s where I come in… 😉

So in walks the PM…

PM: “Say Urv, if I gave you an excel worksheet with the user/group relationship, think you’d be able to script it?”

Me:”Gee, this is so sudden, let me think about it and get back to you asap… (Grinning)”

Here’s an impression of the worksheet:

excel-groupmembers

I’ve done this in past by just going through each user adding each group. I remember reading a post by Mike F. Robbins on how this could be done more efficiently! I very much like this approach. I’ve added a lil’ extra to the mix. Let’s dig in!

Well for starters I’ll just get the source directly from the excel file using D. Finke’s ImportExcel Module. No need to convert to CSV first.

$xlsxADGroupMembers = Import-Excel .\source\xlsx\$xlsxFile -WorkSheetname $WorkSheet

The other thing was retrieving the GroupNames from PSCustomObject by selecting MemberType ‘NoteProperty’, I got this as a tip on my own blog by Dirk. This way you’re not depending on the position of where the group names start in the header.

#Select Group names from Object
$Header = $xlsxADGroupMembers |
   Get-Member -MemberType NoteProperty |
   Where-Object{$_.Name -ne  'UserID'} |
   Select-Object -ExpandProperty Name

This makes it just a bit resilient.

I also decided to use hashtable to store the results first before processing the group membership. This way I can also export the results for future reference (Always keep a log)

#Create empty hashtables
$addADGroupMembers = @{}
$delADGroupMembers = @{}

#Get Group membership
$Header |
ForEach-Object{
   $Group = $_
   $addADGroupMembers.$Group  = $xlsxADGroupMembers.Where{$_.$Group -eq '1'} | Select-Object -ExpandProperty 'UserID'
   $delADGroupMembers.$Group = $xlsxADGroupMembers.Where{$_.$Group -ne '1'} | Select-Object -ExpandProperty 'UserID'
}

Now it’s time to add the members to specified groups

$Header |
ForEach-Object{
   if($addADGroupMembers.$_){
      try{
         Add-ADGroupMember -Identity $_ -Members $addADGroupMembers.$_
      }
      catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException]{
         Write-Warning "AD Object $($Error[0].CategoryInfo.TargetName) not found"
      }
   }

   if($delADGroupMembers.$_){
      try{
         Remove-ADGroupMember -Identity $_ -Members $delADGroupMembers.$_ -Confirm:$false
      }
      catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException]{
         Write-Warning "AD Object $($Error[0].CategoryInfo.TargetName) not found"
      }
   }
}

I’m using Try/Catch to catch any errors on AD Objects not existing. Without it you’d get quite a few errors if the AD objects don’t exists.

Here’s the full script:

I like the fact that you can use the list to add, but also delete if it isn’t necessary. Of course if you fill it in wrong, then there is no fixing that. So be warned!

Another thing is that members can be added manually or another process, so don’t be surprised, when you’re evaulating the group membership….

Tip: Save the  GroupMembership just to be sure…

#Get current Group memberships
$SnapshotADGroupMembers = @{}

$Header |
ForEach-Object{
   $SnapshotADGroupMembers.$($_) = Get-ADGroupMember -Identity $_ | Select-Object -ExpandProperty SamAccountName
}

Next time I’ll blog about how we could to this in reverse… Stay tuned!

Hope it’s worth something to you,

Ttyl,

Urv

Get DSA object creation date

‘Sup PSHomies,

The project manager is on to me! 😛

PM: “PowerShell is awesome!!!”

Me: “You know it!!!”

PM: “Glad you agree! So here’s what I need, We need a weekly update of all users, groups in the source domain based on their creation date…”

Me: “Wait, what just happened?

PM: “You were agreeing that PowerShell is awesome? (Grinning)”

Me: “Well played… I ain’t even mad mad at ya…”

And so begins another PowerShell journey 🙂

The idea here is to be proactive in our migration. Instead of asking for a list of newly created users/groups we’ll gather the information ourselves. All the customer has to do now is validate our list as to who is relevant. Which got me thinking, why stop at user/groups?

Remember I needed to recreate the DFS Structure? The data that I used was from February. What if there were newly created DFS links in the mean time? Turns out my hunch paid off!

Now curiosity got the better of me… “What about accounts that were deleted?” We’re migrating in batches. My first action is always to verify that the accounts in the batch exist. Having a list of deleted objects helps verifying it wasn’t a typo and that this object isn’t relevant anymore…

Here’s what I came up with:

I added Contact and PrintQueues as a bonus 😉

For reporting it’s ImportExcel to the rescue!

dsaobjects

At first I added a extra property in order to lookup the creation date using yyyyMMdd date format. Turns out it wasn’t necessary. You can just a easily use a filter on Created

dsaobjects-filter

The PM is quite pleased… PowerShell is Awesome!!! But you already knew that… 😉

Hope it’s worth something to you

Ttyl,

Urv