Windows PowerShellPlanning to Break

Don Jones

Contents

By All Means, Check First
Or Just Deal with It
Better in Version 2

Recently, I wrote a four-part series in this column that focused on writing a user-provisioning script using Windows PowerShell. One reader, Jim, wrote to ask a pretty detailed question about how to improve the script.

Essentially, Jim has written similar scripts in the past and struggled with ways to handle the errors that occur when you're trying to create a username that already exists. His solution has been to write a "pre-test loop," where he checks to see whether the directory objects already exist before proceeding, but he wanted to know how I'd deal with it in my provisioning script's Provision() function.

It's a great question—and one that I'll answer in this column. Bear in mind, though, that the techniques I'll discuss are pretty universal; you might use similar approaches to deal with other situations where you know that an error can occur. I call my approach "planning to break," and it's how I write scripts that handle potential error conditions a bit more gracefully.

By All Means, Check First

I definitely agree with Jim's approach as a preferred technique. Jim said, "I hate writing error handling," and I tend to agree. Windows PowerShell v1's error handling is a bit primitive for my tastes, so if I can avoid the error entirely, I'll do so.

Let me say first that there are probably an infinite number of variations on my approach, but a finite number of pages for me to work with here, so I'll take just one variation. My basic goal is to detect any user accounts that already exist, and to log them rather than trying to create them a second time. To refresh your memory, the basic Provision() function might look like this:

Function Provision {
  PROCESS {
    CreateUser $_
    CreateHomeFolder $_
    AddToGroups $_
    UpdateAttributes $_
  }
}

The Provision function is expecting pipeline input from a second function whose job it is to import user data from someplace (such as a database or .CSV file) and construct a standardized hash table containing the new user's attributes. So $_['samaccountname'], for example, would contain the user's log-on name. Given that username—which is about the only guaranteed-unique information that I have for a user—I can try to see whether the account already exists. The easiest way is to utilize the Get-QADUser cmdlet (available in the cmdlet package at quest.com/powershell), so I'll modify the Provision function as shown in Figure 1.

Figure 1 Modifying the Provision Function

Function Provision {
  PROCESS {
    $count = Get-QADUser –samAccountName $_['samaccountname'] | Measure-Object
    if ($count.count –gt 0) {
      $_['samaccountname'] | Out-File c:\not-created.txt -append
    } else {

      CreateUser $_
      CreateHomeFolder $_
      AddToGroups $_
      UpdateAttributes $_
  }
  }
}

The modification attempts to retrieve the specified user from the directory and then measure the number of objects retrieved. The measurement object goes into the $count variable, which offers a Count property ($count.count). If the count is more than zero, then the user exists and I use Out-File to pass the username to a log file. If the user doesn't exist, however, then $count will be populated with nothing, and its Count property will not be more than zero—so I proceed with the provisioning.

This approach—checking one user at a time—is the only feasible one given the way the Provision function is built. That's because its PROCESS script block only gets a single user at a time; there's no way to do a "pre-loop" to check all the users first. There's really no need, though, because checking one at a time doesn't significantly alter the performance.

An advantage to this approach over the next one I'll present is that the error-checking—seeing whether the user already exists—is done up-front. In fact, that's the only really viable approach here because the Provision function calls on other functions after attempting to create the user. If I added error-checking to only the Create­User function, the other three—CreateHomeFolder, AddToGroups and UpdateAttributes—might still try to execute.

Or Just Deal with It

But, in general, another valid approach is to attempt to trap the error. Error-trapping in Windows PowerShell v1 is a bit tricky, but it works something like this:

  • You must tell the cmdlet that might run into an error to generate an actual exception, not just print a red error message
  • You must define a trap, where you'll deal with the consequences of the error

I can actually make this sophisticated enough to work with the Provision function. The work starts, however, in the CreateUser function. Somewhere in there is a cmdlet that attempts to create a new user—either New-QADUser, or the New-Mailbox cmdlet if you're using Exchange Server 2007 to create users and mailboxes all at once. In either case, you add the –ErrorAction (or –EA) parameter to tell the cmdlet to change its default error behavior.

You also have to define a trap, which is where the shell will go when an exception actually occurs. The changes to the CreateUser function might look something like this:

Function CreateUser {
  Param ($userinfo)
  Trap {
    $userinfo['samaccountname'] | Out-File c:\errors.txt -append
    break
  }
  New-Mailbox . . . -EA Stop
}

I've omitted the rest of the syntax for New-Mailbox just to make this illustration clearer. Essentially, if New-Mailbox fails, the –EA parameter tells it to stop and throws an exception. This forces the shell to look for an already-defined trap and execute it. In that trap, I log the username to a file and then execute a break, which exits the current function and passes the original error back to whatever called this function. In other words, the Provision function will end up with the error.

To keep Provision from executing the other three functions, I need to modify the way it executes them (see Figure 2).

Figure 2 Modifying the Way Provision Executes

Function Provision {
  PROCESS {
    $go = $true
    trap {
      $go = $false
      continue
    }
    CreateUser $_
    If ($go) {
      CreateHomeFolder $_
      AddToGroups $_
      UpdateAttributes $_
    }
  }
}

As you can see, I've defined another trap. If CreateUser ends with an error, then the trap here in Provision will execute. It simply sets a variable, $go, to the Boolean value False, and instructs the shell to continue—meaning to continue execution on the line following the one that caused the error. That line is an "If" statement, which checks to see if $go contains $True or not. If it does, then the remaining three functions execute; if $go contains $False, those three functions do not execute. I reset that value of $go at the top of the PROCESS scriptblock so that the next user will be attempted.

The reason I used this particular approach (versus any number of others I might have chosen) is that it allows me to have additional code that executes even if the user creation failed (see Figure 3).

Figure 3 Adding a Call to OutputToHTML

Function Provision {
  PROCESS {
    $go = $true
    trap {
      $go = $false
      continue
    }
    CreateUser $_
    If ($go) {
      CreateHomeFolder $_
      AddToGroups $_
      UpdateAttributes $_
    }
    OutputToHTML $_
  }
}

In the variation shown, I've added a call to a function named OutputToHTML. This will execute even if the current user already existed and CreateHomeFolder, AddToGroups and UpdateAttributes did not execute.

Better in Version 2

Windows PowerShell v2, which will ship for the first time in Windows 7 (and which will eventually be available for older versions of Windows), adds new error-handling functionality that's a bit easier to work with: Try…Catch blocks. A full discussion is beyond the scope of this article, so I'll just say that this new feature offers more structured error handling that gives you more options for a bit less complexity.

Don Jones is one of the nation's most experienced Windows PowerShell trainers and writers. He blogs weekly Windows PowerShell tips at ConcentratedTech.com; you may also contact him or ask him questions there.