Modest Refinement — Evolving Ad Hoc Scripts

modest-refinement-gp

Recently, I wrote of how to work with Active Directory Group Policy via Windows PowerShell and presented a modification of a Windows PowerShell script which was the result of a quick hack lobotomy (um, translation) from a VBscript example.

Admittedly, as a software engineer (not full time anymore, remember I teach too now) while I acknowledge, use, and even write ad hoc scripts, something deep inside me yearns to optimize them, modularize them, and treat them nicely. Let’s look at a couple of things we could do with the aforementioned ad hoc script. I’m not going to start here by reposting the original, but by recanting snippets and eventually composing a new whole. If you want the original, please read “This is Your Brain on Active Directory-based Group Policy.”

At the end of the GPOPolicyVer.ps1 script, we have a Group Policy Object (GPO) in a variable $oGPO. The script displays several version number properties of that object.

"The user version number from the Active Directory = " + $oGPO.UserDSVersionNumber "The computer version number from the Active Directory = " + $oGPO.ComputerDSVersionNumber "The user version number from the Sysvol = " + $oGPO.UserSysvolVersionNumber "The computer version number from the Sysvol = " + $oGPO.ComputerSysvolVersionNumber

 

This type of redundant verbosity is common in script, application software, and seems rampant in the industry. So much of the same text and variable references are repeated again and again. Actually, this tiny example isn’t anywhere near as bad as a lot of code I’ve seen and worked with. But consider the following “adjustment.”

$tags = @{ DS="Active Directory"; Sysvol="SysVol" } "DS","Sysvol" |%{ $x = $_; "User","Computer" |%{ "The {0} version number from {1} = {2}" -f $_, $tags[$x], $oGPO.$("{0}{1}VersionNumber" -f $_, $x ) } }

 

Yes, there are surely tighter, denser ways to express this, but let’s not get our PowerShell egos in a knot. Some people into PowerShell seem to be obsessed with making everything a “one-liner” – an addiction I got over years ago. The above snippet was written as two, but I’ve transcribed it here as four for “readability,” which could easily be further expanded for better clarity. Anyway, before I digress, let’s focus on the task at hand – what on earth does this do? And why might we want to use this sort of technique in other scripts?

Let’s start with philosophy. Instead of writing out some text including some data value, and then doing it again, and again, and again, let’s step back for a moment a think about what we want to do. We want several values, in this case four. And actually, there are two aspects of two kinds of values we want. Therefore, we iterate through the first two options, then inside that loop we iterate through the next two, each time emitting one value, and as two times two is four, what we have is this nest iteration yielding four values.

Enough philosophy. Let’s dissect this lovely code snippet and walk through it.

First we set up an array with the nice human-readable names of the directory service and the system volume. This isn’t absolutely essential, and it could have been embedded in the next line instead of assigning it to a variable, but in a later refinement we’ll see that this is brought outside of another loop, therefore in this version it’s defined before the loops. The variable $tags is assigned with the associative array with two entries. The @{} notation delimits the associative array (hash table) with the different values separated with a semicolon. Each value consists of a tag (DS or Sysvol), an equal sign, and the string used as the value. This will be used to display the name instead of the abbreviation later on. In this example we have the tag and the value for Sysvol pretty much the same, but we could have used Sysvol=”System Volume” or Sysvol=”SYSVOL share” or another variation. The point is to use a generic technique.

The next part of this code has a list of two string which are passed to a pipeline which runs a ForEach-Object loop on them. “DS”,”Sysvol” is the specification of the strings, the vertical bar (|) sends those values down the pipeline, and the percent sign (%) is an alias for the ForEach-Object cmdlet. The code block for this loop begins with the curly brace right after the percent sign and ends at the end of the code snippet with the closing curly brace.

We do two things inside that outer loop. First, we save away the value of the iteration variable $_ into a variable $x. The first time through the loop, $x will have the value “DS” and the second time $x will be “Sysvol” by virtue of the nature of ForEach-Object assigning the special variable $_ to one of the values in the list piped into it at a time.

Once we’ve saved that value away, we begin another pipeline and ForEach-Object structure. This time we have the two string “User”,”Computer” |%{ … } to loop through the values “User” and “Computer” using ForEach-Object. This inner loop  is where the real work resides. We just use the format operator (-f) to display a string. This has three or five parts, depending on how you count. On the left we have the format string “The {0} version number from {1} = {2}” and then the format operator (-f) itself. Two down, values to go. The third part is a list of three values, thus if we wanted to treat this comma separated list of values as the 3rd, 4th, and 5th parts of the expression, that wouldn’t be outlandish but more a matter of ambiguous opinion.

Let’s look at these three data values to be injected into the format string before the result is output (displayed) for the user. The first is easy: $_ is the iteration variable for the inner loop and will thus have either the value “User” or “Computer” depending on which trip we’re at through the loop. According to the format string, argument number zero (the first one in the list on the right-hand side of the -f operator) will be inserted after the word “The” in place of {0}, therefore we’ll first get: “The User version number…” and then “The Computer version number…” the 1st and 2nd times through the inner loop, respectively.

The second value injected in the format string in place of {1} will be $tags[$x] where $x has the value “DS” or “Sysvol” for the 1st and 2nd times through the outer loop. This results in the value “Active Directory” or “SysVol” being put into the resultant output string.

But we’re not done yet, and the third value injected in place of {2} is the most fun. This value is an attribute (a.k.a. property) of the object in the variable $oGPO. But which one? Let’s remember why we’re here. We wanted to avoid literally repeating much of the same text and data specifications. There are humongous motivations for doing so which I’m not going to state here and which are not blatantly obvious from this tiny example of four values. But here’s the gist. Each time through the loop, we want either the UserDSVersionNumber, ComputerDSVersionNumber, UserSysvolVersionNumber, or ComputerSysvolVersionNumber. How do we pick one? Shall we use a switch statement? Cascading series of if/else? Think again. We have all of the pieces we need in front of us to identify which attribute to use, yet those pieces may need to be… well, pieced together. There are numerous ways to do this, and if we gathered 100 PowerShell-fluent people, I’m sure they’d come up with lots of different ways. We could use various string concatenation techniques and take the catenated value and presto, we’d be set. But we’ve already used a similar technique in this code snippet which we can use again – the format operator! Behold, magic: “{0}{1}VersionNumber” -f $_,$x and we’re golden.

Almost. We still need to use the results of that format operator which generates the proper attribute name and access that attribute on the $oGPO variable. This is done by substituting the name back into the command expression with a dollar sign and parentheses around the expression. Thus, the whole third parameter for the first -f operator is:

 

$oGPO.$(“{0}{1}VersionNumber” -f $_, $x )

The first time through the outer and inner loops this would reference $o.GPO.UserDSVersionNumber, and the other attributes on the subsequent trips through the loops.

That’s it for collapsing down the easy to read “string = value” type output into a condensed double-loop structure. Now let’s move onto something else we can do with the original example script. Based on a translation from the VBscript version which pulled the results of the first item of a group policy object search using: set oGPO = oGPSearchResults.Item(1), we simply used $oGPO = $oGPSearchResults[0] for a similar behavior. Following that was the display of the version numbers which we’ve already transmogrified above.

What if we got back a number of GPOs from the search? We’ll make that more possible in a moment, but first, let’s replace that crazy assumption that we only want element zero of the array (yes, $oGPSearchResults was forced/casted as an array earlier in the original translation using @(…), but that’s another story. Let’s use the foreach construct of PowerShell, which is notably distinct from the ForEach-Object cmdlet (although that cmdlet is aliased as both % and foreach) as I pointed out in the course materials written for Microsoft’s course 6434. We don’t need to know the distinctions here; just now to follow an example.

foreach( $oGPO in $oGPSearchResults ){ … }

This foreach loop should be a nice replacement for the $oGPO = $oGPSearchResults[0], and we’d put the display snippet we munged earlier in the body instead of that evil ephemeral ellipsis.

But wait. How does a search with searchOpEquals match more than one GPO? Well, I’ll leave that as an exercise for the reader, and in the mean-time let’s switcheroo that operator to searchOpContains which will match substrings in the search. Because multiple GPOs could be coming out in the results and we may not be sure what their exact names are, we’ll add another line like “`n— GPO {0} —” -f $oGPO.DisplayName so we know what we’re getting resultant version number for. Also, as promised earlier, we’ll rotate the $tags assignment out of the foreach loop. So far, our revised script would look like this.

# Script to identify the Group Policy version number # PowerShell version, retromutated Mach 1 $USE_THIS_DC = 0 $strPolicyName = "TEST" $strDC = "delta.hq.local" $strDomainName = "hq.local" # Create objects for searching the domain $oGPM = New-Object -ComObject "GPMGMT.GPM" $oGPConst = $oGPM.GetConstants() $oGPSearch = $oGPM.CreateSearchCriteria() $oDom = $oGPM.GetDomain( $strDomainName, $strDC, ` $USE_THIS_DC ) $oGPSearch.Add( $oGPConst.searchPropertyGPODisplayName, $oGPConst.searchOpContains, $strPolicyName ) $oGPSearchResults = @($oDom.SearchGPOs( $oGPSearch )) # Verify we have found a GPO. If not quit. if( $oGPSearchResults.Count -le 0 ){ "The Group Policy object " + $strPolicyName + " was not found`non Domain Controller " + $strDC return } "Got {0} GPOs back..." -f $oGPSearchResults.Count # If found policy then print out version numbers $tags = @{ DS="Active Directory"; Sysvol="SysVol" } foreach( $oGPO in $oGPSearchResults ){ "`n---GPO {0} ---" -f $oGPO.DisplayName "DS","Sysvol" |%{ $x = $_; "User","Computer" |%{ "The {0} version number from {1} = {2}" -f $_, $tags[$x], $oGPO.$("{0}{1}VersionNumber" -f $_, $x ) } } }

 

But we’re not done yet. Let’s take this one step further. Having a script which gets GPOs could be really handy, but why should it be coupled with the display of version numbers? A bit of well-placed modularity could be sprinkled on this prototype. Then this version number printing could depend on a some generic fetching GPO code.

Let’s usher in this next stage of the transformation with a variation of the above script with influences from the three forms of a Get-GPO function I included in the course 6434 “Automating Windows Server 2008 Administration with Windows PowerShell” supplementary materials. Let’s use a form similar to my Get-GPO function with the name search functionality, keeping the contains rather than strict equals matching, yet not complicate it here with the abilities of matching backup GPOs nor starter GPOs. Let me know if you want a more flexible version.

function Get-GPO( $name = "", $domain = "nanoware.net" ){ $gpm = new-object -com gpmgmt.gpm $gpmConstants = $gpm.getConstants() $dom = $gpm.getdomain( $domain, "", $gpmConstants.UseAnyDC ) # get all GPOs (unless a name is given) $sc = $gpm.CreateSearchCriteria() if( $name -ne "" ){ $sc.Add( $gpmConstants.SearchPropertyGPODisplayName, $gpmConstants.SearchOpContains, # or Equals $name ) } $all = @($dom.SearchGPOs( $sc )) return $all }

 

Such a function could be defined in a script that’s run before doing Group Policy work, or in a script run as a part of a Windows shortcut, calling script, or profile so that’s available when you need it.

How would we use this generic Get-GPO function to simplify the earlier script? Consider the following function.

# Function to identify the Group Policy version number # PowerShell version, retromutated Mach 2 function Get-GPOVersions( $name = "" ){ $sr = Get-GPO $name if( $sr.Count -le 0 ){ "The Group Policy object {0} was not found." -f $name return } "Got {0} GPOs..." -f $sr.Count # If found policy then print out version numbers $tags = @{ DS="Active Directory"; Sysvol="SysVol" } foreach( $oGPO in $sr ){ "`n---GPO {0} ---" -f $oGPO.DisplayName "DS","Sysvol" |%{ $x = $_; "User","Computer" |%{ "The {0} version number from {1} = {2}" -f $_, $tags[$x], $oGPO.$("{0}{1}VersionNumber" -f $_, $x ) } } } }

 

How could we take this one step further? By taking out the Get-GPO invocation from Get-GPOVersions, changing the Get-GPOVersions into a filter or function which accepts pipeline input, and requiring that the caller just pipeline the output of Get-GPO into Get-GPOVersions.

get-gpo marketing | get-gpoversions # if modified as described

 

An alternative would be to have them use Get-GPO piped to Format-Table with the version numbers selected.

get-gpo marketing | FT displayName,*versionNumber

 

In the end, the key to scripting is often to keep things simple. An innocent-looking asterisk can sometimes save a whole lot of code. Many ad hoc scripts can be virtually optimized out of existence.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.