The Surly Admin

Father, husband, IT Pro, cancer survivor

Functions – When and how?

Recently I was reworking a script for someone at Spiceworks, you can read about it here.  But some interesting things came up and I wanted to talk about it here.  It’s about functions, and when and how you should use them.  Read on to hear my take on the subject.

Passwords Expiring Report

For backstory, the OP of this thread requested a report to alert him when users were getting close to having their password expire.  I had just completed the Password Expiration Report so I posted that, since it would have all the information he needed in it.  It did, but he wanted the report to be a bit better focused which gave rise to the Passwords Expiring Report, and this post about functions.

I have two general rule about functions and when I’ll use them.  If I need to use the same bit of code in two seperate areas, and if that code doesn’t do the same thing.  Now, that may sound a bit contradictory and as I’ve written it, it really is, so let me explain.  The first rule, if the code appears in two seperate areas, should be pretty obvious.  You never want to write the same thing twice if you can help it, so make a function out of it.  This not only saves you from writing more code but when you inevitably need to change the code you only have to change it in one place instead of remembering that it appears twice and do it somewhere else.  This is really helpful when you have to change a script years later and have completely forgotten it–hell, I run across scripts I forgot I even wrote!

So what do I mean by the function shouldn’t do the same thing?  Is the function doing the same generic lookup?  If so you might be looking at a performance issue.  Let me explain with an example.

M. Ali wrote a wonderful function for doing password age lookups, and I used it in my Password Expiration Report. This made creating this report a snap as I was able to setup my user loop, call the function and create the report.  Whole thing came together in less then an hour.  But if you look closely, the function does a generic lookup everytime you run it, and in my case that’s for every user in my Active Directory.  Here it is:

$maxPasswordAgeTimeSpan = $null
$dfl = (get-addomain).DomainMode
if ($dfl -ge 3) {
   ## Greater than Windows2008 domain functional level
   $accountFGPP = Get-ADUserResultantPasswordPolicy $accountObj
   if ($accountFGPP -ne $null) {
      $maxPasswordAgeTimeSpan = $accountFGPP.MaxPasswordAge
   } else {
      $maxPasswordAgeTimeSpan = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge
   }
} else {
   $maxPasswordAgeTimeSpan = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge
}

He also uses some scriptblock spacing that I hate (read here for what I mean), but I cleaned that up for my script.  What this section of code is doing is using the Get-ADDomain cmdlet to find out what level of your AD is running out.  Anything greater then a 3 in Domain Mode means a 2008 functional level or higher.  That means it’s possible to have granular level password settings, so he then uses the Get-ADUserResultantPasswordPolicy to determine that.  If that comes up blank then we get the information from the domain, otherwise we get it from the Policy cmdlet.  So that part is user specific, but when getting it from the domain, and when querying the domain on what functional level it’s at we’re doing a generic lookup.  Everytime the function is run.

For the Password Expiring Report, I decided to change things up.  First off, when looking at this report we discover that this function breaks both rules for when to write a function!  We only use this code once in the script, and it’s doing a generic lookup within the function.  So I broke it up.  First we need to determine the domain level and get the Maximum Password Age set for the domain.

$maxPasswordAgeTimeSpan = $null
$dfl = (get-addomain).DomainMode
$maxPasswordAgeTimeSpan = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge
If ($maxPasswordAgeTimeSpan -eq $null -or $maxPasswordAgeTimeSpan.TotalMilliseconds -eq 0)
{  Write-Host "MaxPasswordAge is not set for the domain or is set to zero!"
   Write-Host "So no password expiration's possible."
   Exit
}

Now we’ve only done this task once, and saved the results in variables.  Much more efficient.  Then within the main program loop we have to look at the domain mode ($dfl) and do our Policy check.

If ($dfl -ge 3)
{  ## Greater than Windows2008 domain functional level
   $accountFGPP = $null
   $accountFGPP = Get-ADUserResultantPasswordPolicy $User
   If ($accountFGPP -ne $null)
   {   $ResultPasswordAgeTimeSpan = $accountFGPP.MaxPasswordAge
   }
   Else
   {   $ResultPasswordAgeTimeSpan = $maxPasswordAgeTimeSpan
   }
}
Else
{  $ResultPasswordAgeTimeSpan = $maxPasswordAgeTimeSpan
}

First check if the $dfl is greater then 3, if not just set a new variable, $ResultPasswordAgeTimeSpan, to the value we discovered at the beginning of the script.  If it is greater then we can query the Policy and get the specific password policy for that user and again set $ResultPasswordAgeTimeSpan.  After that it’s a simple matter of using New-Timespan to determine if the expired passwords hit the age range we’re looking for and report on it.

Best Practice

As with all best practices, it changes as soon as you have a requirement that needs to break the rule.  But these are the rules I tend to follow.

  1. Use a function if you need to write the same code more then once
  2. Do not use a function if you need to do the same generic lookup

The second rule is much harder to follow if you’re writing a function that you intend to publish for other people to use.  M. Ali’s function is a great example of this.  He was not writing a script for a specific report, but publishing his Function for others to use.  This puts a different spin on the requirements because now you need your function to be as self contained as possible, which means the generic lookup that he requires must be within the function.  And by doing so he saved me much time in the Password Expiration Report.  But for the Password Expiring Report I wanted to bump the performance up, and I did.  The Expiration report takes a few minutes to run, while the Expiring report takes just a few seconds (and since my domain is running at a 2003 functional level I don’t have to do individual Policy lookups on the users which also speeds things up greatly).

January 3, 2013 - Posted by | Powershell - Best Practices | ,

3 Comments »

  1. […] I’ve talked about before, Functions are great for doing the same task over and over again within a script but another thing […]

    Pingback by Functions That Use the Pipeline « The Surly Admin | January 12, 2013 | Reply

  2. […] how do we repeat the same code in a script while only writing it once?  Sounds an awful lot like a function, don’t you think?  So I wrap the entire script in a […]

    Pingback by Using Powershell as a Telnet Client « The Surly Admin | April 4, 2013 | Reply

  3. […] I’ve talked about before, Functions are great for doing the same task over and over again within a script but another thing […]

    Pingback by Functions That Use the Pipeline « The Surly Admin | October 15, 2015 | Reply


Leave a comment