Use PowerShell to Clean Up Active Directory After a Stalled Migration

Use PowerShell to Clean Up Active Directory After a Stalled Migration

  • Comments 4
  • Likes

Summary: Use Windows PowerShell to clean up extended user attributes following a stalled Active Directory migration.

 

Microsoft Scripting Guy Ed Wilson here. Today I am proud to announce the return of Microsoft MVP Nicolas Blank to the Hey, Scripting Guy! Blog.

Photo of Nicolas Blank

Nicolas has more than 14 years’ experience on various versions of Exchange and is a messaging architect specializing in consulting and building on-premise and cloud-based Exchange-based messaging systems as well as  the interoperations with various vendor ecosystems.

Nicolas currently holds the status of Microsoft Certified Master Exchange 2010 and Microsoft Most Valuable Professional for Microsoft Exchange since March 2007.

Nicolas writes regularly about Exchange and messaging topics on blankmanblog.com, and he provides content for the IT Pro Africa project, which contributes toward building IT in Africa.

Now, without further ado, here is Nicolas to talk to us about writing self-executing code in Windows PowerShell.

 

For this post, I’m using the free (I like free) Active Directory cmdlets from Quest, simply because for the scenario I am addressing I am processing extended attributes on user and group objects, which aren’t necessarily mail or mailbox enabled. As a rule, if your users and groups are mail or mailbox enabled, the Exchange Management Shell cmdlets could also be used, albeit with different syntax.

I am also leveraging another feature of Windows PowerShell: the ability to execute strings as code. You can do this with a cmdlet known as Invoke-Expression, but before I give too much away, let me state my case.

So here’s my premise. Active Directory Domain Services has been populated extensively during a migration, which had stalled and needed to be restarted. This means that I have a boatload of populated extended attributes that I need to preserve; however, I have others that I need to remove. This means it isn’t as simple as setting the value of every attribute to $null. I need to examine every attribute value in turn, and then decide if I should remove it or preserve it.

So simply stated, I need to examine the 15 extended attributes on the user object for one of two possible values. If the values existed and they matched a predetermined pattern, set the attribute to $null. If it did have a value, but the value “made sense” in business terms, leave it alone.

So why write code that writes code? I am glad you asked!

I will admit it—I am lazy and I do not tend to like doing the same thing more than once, if I can avoid it. In the past, I would have been content with writing large amounts of code, but in this case, because the language I am coding with is Windows PowerShell, I am able to write Windows PowerShell that writes and executes Windows PowerShell, as it is needed. The other reason is the ease of debugging one line of code instead of 15 lines of code. I will paste the complete code below, but I will discuss the pieces we need to. There is a ton of potential debugging to be done here, so I have built in output and debugging as I have gone along.

We start off by populating $Objects with the complete set of objects we want to process, which is all users and groups in the domain. Note that I am only choosing to return the attributes I need in order to be as efficient as possible; otherwise, the query may take a while and be expensive in Active Directory terms.

Note that $sizelimit is defined as a variable, so I can quickly crank up or down the numbers of objects as I am testing:

#if Sizelimit = 0 then no limits apply, otherwise limit  the number of objects to build the AD Query

$sizelimit = 0

#Only return user AND group objects without discriminating if they are mail enabled or not

$Objects = get-qadobject  -LdapFilter "(|(&(objectCategory=person)(objectclass=user))(objectclass=group))"  -sizelimit $sizelimit -includedproperties userprincipalname, ExtensionAttribute1, ExtensionAttribute2,ExtensionAttribute3, ExtensionAttribute4,ExtensionAttribute5,ExtensionAttribute6,ExtensionAttribute7,ExtensionAttribute8,ExtensionAttribute9,ExtensionAttribute10,ExtensionAttribute11,ExtensionAttribute12,ExtensionAttribute13,ExtensionAttribute14,ExtensionAttribute15 

Now that $Objects is populated, let’s generate our first bit of self-executing code. The first thing I want to do is see if there is a value to act upon. We start a loop-based counter called I$ and give it a value of one, which we increment later:

   $i = 1

    do { #this loop builds the code to form the required 15 Extention attributes and executes it

    #build the string to form the extention attribute

       $str="$"+"object.ExtensionAttribute$i";

       $AttribValue = Invoke-Expression $str

We build the string called $str:

 $str="$"+"object.ExtensionAttribute$i";

We then execute the line using Invoke-Expression and pass the value back to $AttribValue. As the loop iterates this line becomes:

$object.ExtensionAttribute1,

$object.ExtensionAttribute2,

$object.ExtensionAttribute13 ….. and so forth, up to $object.ExtensionAttribute15.

Each of these execute and populate $AttribValue in the next line:

$AttribValue = Invoke-Expression $str

This allows us to move to the next line, which effectively checks if there is a value to act upon or not:

if ($AttribValue) #If there IS a value to consider

       { Do Stuff}

Moving on—and speaking of free—this is where using the Windows PowerShell ISE comes into its own as a free editor. I am pasting a screenshot of a line of code that shows where strings start and end, which was quite critical in writing this line of code.

$Exec= "(("+$str+".StartsWith("+'"ID:"'+")) -or ("+$str+" -match("+'"^[0-9A-F]{32}$"'+'))) ';

           #$exec

        #if the next line returns true, then a match for one of the conditions if found      

          if  (Invoke-Expression $Exec)

{ Do Stuff}

 

Remembering that $str contains $object.ExtensionAttribute1,2,3, we get 15 iterations of this:

(($object.ExtensionAttribute1.StartsWith(“ID”))-or($object.ExtensionAttribute1  –match ("^[0-9A-F]{32}$")))

That is quite a mouthful. In a nutshell, look for a string that starts with “ID” or matches a regular expression pattern of uppercase letters or numbers with a string length of 32. Things get quite tricky with needing to build a string that has quotes in it. This is where we use a single quotation mark to frame a double quotation mark (in other words, ‘ ” ’), which returns one double quotation mark.

Then, iterate that for extension attribute 1,2,3,4,5,6,7…up to 15.

Again you can see how the color-coding in ISE helps to see at a glance what is quoted, and what is code vs. what is commented.

Finally, we reach the line of code that wipes the attribute. I had a challenge here of wanting to suppress errors and write the output to a log at a later date, so I used the following syntax to build a string and execute it:

$Killstring = "get-QADObject "+'"'+$object+'"'+" | set-qadobject -ObjectAttributes @{ExtensionAttribute$i="+"$"+"null}"

invoke-expression $Killstring

 

It builds code that looks like this:

get-QADObject $object | set-qadobject -ObjectAttributes @{ExtensionAttribute=$null}

I could have done two things here: write 15 sets of code with 15 points of error and debug potentially, or make an investment in code that executes itself and modifies itself according to the context of the loop. I chose the latter.

Full code follows:

#Add the Active Roles Snapin

Add-PSSnapin Quest.ActiveRoles.ADManagement

 

#if Sizelimit = 0 then no limits apply, otherwise limit the number of objects to build the AD Query

$sizelimit = 0

 

#Only return user AND group objects without discriminating if they are mail enabled or not

$Objects = get-qadobject  -LdapFilter "(|(&(objectCategory=person)(objectclass=user))(objectclass=group))"  -sizelimit $sizelimit -includedproperties userprincipalname, ExtensionAttribute1, ExtensionAttribute2,ExtensionAttribute3, ExtensionAttribute4,ExtensionAttribute5,ExtensionAttribute6,ExtensionAttribute7,ExtensionAttribute8,ExtensionAttribute9,ExtensionAttribute10,ExtensionAttribute11,ExtensionAttribute12,ExtensionAttribute13,ExtensionAttribute14,ExtensionAttribute15 

 

 

Write-host "Found this many objects: "$objects.count

 

 

#Loop through all the users and evaluate all 15 Extention attributes

$count=1 #the first attribute starts at one so start the loop at 1

Foreach ($object in $Objects)

{

$complete = (($count / $Objects.count)  * 100) # Grap a quick and dirty percentage count and output via the progress bar

Write-Progress -activity "Evaluating Objects" -status "Percent Complete: $complete" -percentComplete $complete

$count++

 

#I like to build strings like this to output the status as it's only a step away to add it to a log output

$tmpstr = "Evaluating Object Type: " + $object.classname + " Object: " + $object.ntaccountname

write-host  $tmpstr

 

    $i = 1

    do { #this loop builds the code to form the required 15 Extention attributes and executes it

    #build the string to form the extention attribute

       $str="$"+"object.ExtensionAttribute$i";

       $AttribValue = Invoke-Expression $str

       #execute it and examine it for value. it it's $null there's nothing to do and the condition returns $false

       if ($AttribValue) #If there IS a value to consider continue

       {

        #$exec contains the string which compares for two possible values, a string starting with ID: or a regex for a 32 char GUID        

           $Exec= "(("+$str+".StartsWith("+'"ID:"'+")) -or ("+$str+" -match("+'"^[0-9A-F]{32}$"'+'))) ';

           #uncomment the next line to see what string is build, for debug purposes

           #$exec

        #if the next line returns true, then a match for one of the conditions if found      

          if  (Invoke-Expression $Exec) #Check if the extention attrbute has a qualifying value, i.e. it's not null

          {

              $tmpstr= $object.NTAccountName+" Found Qualifying value in Attribute number "+$i

              write-host $tmpstr

             

              $tmpstr=  "Removing Value:" + $AttribValue

              write-host $tmpstr

             

              #build the command the wipe the affected attribute and execute it

              $Killstring = "get-QADObject "+'"'+$object+'"'+" | set-qadobject -ObjectAttributes @{ExtensionAttribute$i="+"$"+"null}"

            

              #uncomment the next line to see what string is build, for debug purposes

              #$Killstring

             

              #Execute the line of code we build above.

              invoke-expression $Killstring

          }

       } 

      

        $i++}

        while ($i -le 15)

    }   

     

$dt = Get-Date -format "ddMMyyyy_hhmm"

$tmpstr  = "Script Finished " + $dt

write-host $logfilename

 

I want to thank Nicolas for this great article. I always love seeing how IT pros use Windows PowerShell in the field to solve real-world problems.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy

 

 

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • Hi Nicolas,

    thanks for this interesting blog entry!

    I sometimes forget about Invoke-Expression and this brings it back to the surface of my old brain!

    It is definitely of great help if you want to iterate through a collection and want to construct expresions

    based on a common pattern. The execution of generated code is a great feature!

    I love to follow you and I know that it works very well.

    But it is hard to debug and avoiding errors is an art of its own.

    So it is a two-sided sword ... we may use it and we probably should avoid it ... depending on your level of braveness :-)

    Klaus.

  • You're right Klaus, it can be a double sided sword, but I look at each instance on it's own merrit. If the ultimate saving is worth using Invoke-Expression, then I'll be willing to take the extra pain the debuging can cause, if not, then I don't.

    Thanks for a great observation and thank you for your kind comment.

    Nic

  • Thanks A lot you really help me

  • thanks