Don Jones

Contents

Setting Goals
Building the Initial Structure
Getting Input
Expansion
Coming Up Next

With this column, I am kicking off a four-part series on creating a practical, real-world, user-provisioning script in Windows PowerShell. In this first part, I'll focus on creating the structure for the script. Next month, I'll look at actually creating a new, mailbox-enabled user in Active Directory and Exchange Server 2007. The following month, I'll take care of creating home directories and applying the proper Access Control Lists (ACLs) to them. And in the final installment, I'll place the new user into groups and take care of populating additional Active Directory attributes.

Video (no longer available)

Don Jones kicks off a four part series looking at how you can use Windows PowerShell to create a user provisioning script. Watch Don demonstrate these techniques in this video.

Setting Goals

The trickiest part of user provisioning is deciding from where the initial data will come. How will I know what users to create? And where will I get their information, such as names, e-mail addresses, mailing address, department, and other details?

Nearly every environment will have a different answer, and so I will create a script that's capable of accepting input from a variety of places. That will make the script slightly more complicated, but it'll be worth the effort in the long run because the script will be more flexible. We also want to build a script capable of creating a hundred new users as easily as one—this is about automation, after all!

Building the Initial Structure

I do a decent amount of consulting with various companies, and a lot of the focus is on scripting and automation. Not surprisingly, I see a lot of scripts, and I've noticed that a common approach is for an administrator to just build a single provisioning script.

Over time, the administrator will modify the script to handle more and more tasks. That's not a bad approach, but by taking time up front to create a somewhat more modularized structure, you can create a script that's actually more flexible and easier to maintain. And this doesn't require much extra work.

I'm going to start with a central function named Provision. It's not actually going to do very much; it will accept a hashtable (or associative array) that contains the new user information, and then it will call a series of sub-functions that actually handle all of the provisioning tasks. Because this is Windows PowerShell, I want all of this to work on the pipeline. So I'm going to have it accept one hashtable for each new user, and I will enable it to accept a whole pipeline full of hashtables to create multiple users.

The basic function template looks like this:

Function Provision { PROCESS { } }

Not very impressive, is it? With Windows PowerShell, it doesn't need to be. That PROCESS script block will execute once for each object that I pipe into the function. And within the PROCESS script block, I will use the $_ variable to access the current pipeline object.

Windows PowerShell Q&A

Q I looked up the Win32_OperatingSystem Windows Management Instrumentation (WMI) class on the MSDN Web site. It says the class contains service pack version information. When I run Get-WmiObject Win32_OperatingSystem in Windows PowerShell, no service pack information is shown. Why?

A The Windows PowerShell formatting subsystem selects a default subset of the properties in that class for display. One way to force it to show all of the properties is to use a formatting cmdlet to override those defaults:

Get-WmiObject Win32_OperatingSystem | Format-List *

Or, if you just want specific properties:

Get-WmiObject Win32_OperatingSystem | Format-List BuildNumber,CSName,ServicePackMajorVersion

Getting Input

The reason I don't have the Provision function actually get any data on its own is that I want more flexibility, and I don't want to go back and change the function every time I want to adopt a new form of input. Instead, I will create two functions that get my new-user information—one from a CSV file and the other from a database. For now, I'll just work on the CSV-related function since CSV files are easy to create with Notepad or another text editor (or even Microsoft Excel and most database applications).

One trick about CSVs—especially if provided by someone less technically inclined—is that you can't rely on the file's column headers being the proper Active Directory attributes. That is, instead of column names such as sn and samAccountName, you're more likely to get column names such as Last Name and Logon Name. That's OK—Windows PowerShell can do the translation. I'll start by assuming a CSV file contains the following columns:

  • First Name
  • Last Name
  • City
  • Department
  • Job Title
  • Logon Name
  • Password

You can, of course, expand that list—I'll show you how it works in a moment. Using this list, a sample CSV file might look something like this:

First Name,Last Name,City,Department,Job Title,Logon Name,Password Don,Jones,Las Vegas,IT,Writer,donj,P@ssw0rd Greg,Shields,Denver,IT,Administrator,gregs, P@ssw0rd

I'll create a function named ProvisionInputCSV, have it accept a filename as an input parameter, and have it simply read the CSV file:

Function ProvisionInputCSV { Param ([string]$filename) Import-CSV $filename }

I can run that function by itself, like so, just to make sure it's reading the CSV file:

ProvisionInputCSV c:\files\myinput.csv

Now I want to have the function translate each line of the CSV into a hashtable. And, rather than retaining the column header names from the CSV file, I want to translate those column headers into more Active Directory-acceptable attribute names. There are a number of ways to do that in Windows PowerShell, but I'm going to stick with an approach that's relatively simple: I use a foreach loop to process each line of the CSV file, one at a time. For each line of the file, I'll create a hashtable and write it out to the pipeline.

Take a look at my function, shown in Figure 1. There are a few points worth noting:

  • Within the foreach block, the $user variable contains a single user. The foreach construct runs through each line in the $users variable and automatically populates $user with the next one.
  • Because some of the column header names contain spaces, it's necessary that I enclose those names in quotation marks.
  • I've added another attribute, displayName, which is constructed from two of the CSV columns: First Name and Last Name.
  • Write-Output is used to write each hashtable to the pipeline.

Figure 1 Function using a foreach loop

Function ProvisionInputCSV { Param ([string]$filename) $users = Import-CSV $filename foreach ($user in $users) { $ht = @{'givenName'=$user."First Name"; 'sn'= $user."Last Name"; 'title'= $user."Job Title"; 'department'= $user.Department; 'displayName'= $user."First Name" + " " + $user."Last Name"; 'city'= $user.City; 'password'= $user.Password; 'samAccountName'= $user."Logon Name" } Write-Output $ht } }

The practical upshot of all this is that I can use one function to read the CSV file and transform its data into hashtables. The hashtables can then be piped to my provisioning function:

ProvisionInputCSV c:\data\myinput.csv | Provision

Since my Provision function accepts a standardized hashtable, I can create several different input-generation functions—ProvisionInputDatabase, Pro­visionInputSpreadsheet, and so on.

As long as those functions output my standardized hashtable full of user data, the main Provision function will operate correctly. This approach means I can, in the future, use entirely new input sources for new user data—without having to modify the core Provision function.

Expansion

You can easily add more columns to your version of the CSV file to populate things such as phone numbers or whatever other data you like. You just need to make sure that you also expand the hashtable to contain that information. For example, let's say I want to add Description and Office to my CSV file. I'd simply expand the hashtable by adding some lines, as shown in Figure 2. In other words, you can use the basic structure I've provided to accommodate whatever requirements your environment has for the directory attributes of new user accounts.

Figure 2 Revised function

Function ProvisionInputCSV { Param ([string]$filename) $users = Import-CSV $filename foreach ($user in $users) { $ht = @{'givenName'=$user."First Name"; 'sn'= $user."Last Name"; 'title'= $user."Job Title"; 'department'= $user.Department; 'displayName'= $user."First Name" + " " + $user."Last Name"; 'city'= $user.City; 'password'= $user.Password; 'samAccountName'= $user."Logon Name"; 'office' = $user.office; 'description' = $user.description } Write-Output $ht } }

Coming Up Next

Next month, I'll start populating the main Provision function by creating a sub-function that creates new user accounts. I'll give you two versions of that sub-function—one for folks who have Exchange Server 2007 (which is Windows PowerShell enabled) and another for folks who don't. At that point, you'll actually have a pretty usable script: It'll just create user accounts. But since that's one of the more time-consuming parts of dealing with new users, this function could help you to start saving time immediately.

Don Jones is a cofounder of Concentrated Technology where he blogs weekly about Windows PowerShell, SQL Server, App-V, and other topics. Contact him through his Web site.