Figure 1
Introduction
Products implemented over multiple servers in an Active Directory domain often use the AD to store data. Typically, the data is divided in some fashion and stored within specific Organization Units (OUs) on the AD. As OUs may themselves contain OUs, the OU structure of a domain may become complex. Often, the best way to visualize a nested OU structure is to observe a graphical representation.
It is possible to maintain the OU structure of a single domain using the Users and Computers snap-in. But what does one do if the OU structure must be reproduced on many domains? There may be arrays of domains serving as test beds for the product, VMware domains on laptops, performance and capacity domains, and finally, the actual production domain. As the number of domains and the complexity of the OU structure grow, manual maintenance becomes impossibly difficult.
The CSVDE or LDIFDE utilities may be used to create, import, or export an OU structure. However, these utilities have their drawbacks.
- They produce or input a linear list of OUs. OU nesting is not apparent. One is not able to visualize the structure.
- Each OU is identified by its Distinguished Name (DN). In a DN, the domain name is appended to the OU name. One is forced to repeatedly append something like DC=array6,DC=msgtst,DC=doug,DC=org to each OU's DN. The domain name is not germane to the problem of importing or exporting OU structures among multiple domains.
- These general utilities can do many things on an AD. But the number of command line arguments is daunting. One may be left wondering what the utility is doing or even why it works.
- Ordering is not guaranteed or important. The same command on a different OS may change the order of the results.
A small C# script dedicated to OU export or import offers a simpler solution for maintaining OU structures without these drawbacks.
Background
Active directory has a confusing jargon. Fortunately only a few terms are needed to understand OUTransport.
- NC
- Naming Context, data objects in an AD are partitioned or segregated into NCs. The NCs have different functions and replication scopes.
OUTransport
is concerned with the Domain NC.
- DN
- Distinguished Name, a unique name used to locate a data object on an AD. Data on an AD is organized in a hierarchical manner which is reflected in an object's DN. For example the DN for the Domain Controllers OU in domain doug.org is, OU=Domain Controllers,DC=doug,DC=org.
- OU
- Organizational Unit, a container used to hold data objects. OUs may have children which are themselves OUs. An example of a DN of a nested OU is OU=level2,OU=level1,DC=doug,DC=org.
- path
- ADsPath, a way to uniquely reference a data object on an AD according LDAP syntax standards. C# AD classes and methods often have a path property or parameter. For OUTransport, the path is the concatenation of "LDAP://" and a DN. For example, the path for the Domain Controllers OU is LDAP://OU=Domain Controllers,DC=doug,DC=org.
- OU Import File Format
- A text file of OUs defined following rules allowing OUTransport to import the OUs into a domain. The output of an OUTransport export adheres to the OU Import File Format. Thus the output of an OUTransport export may be used as the input for an import. Outransport can digest its own output.
Using the code
OUTransport.exe, a NET 1.1 console executable, is provided in the demo download. It may be run on any server in a domain under the Domain Administrator account. To try OUTransport, open up a command shell and cd to the demo folder. OUTransport determines whether to export or import the OU structure by looking for its first and only command line argument. If the first argument exists, it is assumed to be the name of a text file containing an OU structure to be imported into the domain. When the argument is missing, OUTransport exports the AD's current OU structure to the console.
- View the domain's current OU structure on the console.
$OUTransport
- Export the current OU structure to a text file.
$OUTransport >filename.txt
- Import the OU structure from a file.
$OUTransport filename.txt
Figure 1 shows the screen OUTransport generates to display the current OU structure. Tabs are used to indicate OU nesting. Only the name of the OU is displayed, rather than its full DN. The character '#' in column 1 indicates a comment line. One may redirect this output to a file, edit it with a text editor if desired, and import the file with OUTransport on another domain to reproduce the OU structure. The output of an OUTransport export can serve as its input during an import. This type of output is referred to as the OU Import File Format in this article.
Building from Source
All source for OUTransport resides in a single file, OUTransport.cs. To build the utility, cd a command shell to the source directory and invoke the C# compiler over the source...
$ csc OUTransport.cs
This will produce the executable for the utility,
OUTransport.exe.
OU Import File Format
To define the nesting of an OU the path of all parent OUs, from the first level down to the parent that actually contains the OU, must appear in the file before the OU being defined. Tabs proceed each OU name to indicate an OU's parent (nesting level). Any leading spaces entered by a manually editing the import file are ignored. Spaces should never appear before an OU name as they may lead to an incorrect visualization of the OU structure. OUTransport counts the number of TABs preceding an OU name to determine its nesting level. The nesting level may only be increased by 1 on each consecutive line. However, it may be reduced by any amount.
#An illegal import file showing 3 nested OUs
domain controllers
level1OUa
level3OUa #Illegal, parent OU must proceed it, level jumps by 2
level2OUa
level1OUb
#Legal import file showing 3 nested OUs
domain controllers
level1OUa
level2OUa
level3OUa #Decrease nesting level by 2
level1OUb
What about errors? Import wrong file????
Classes
OUTransport
is composed of three static classes in the file OUTransport.cs. The classes are...
MainProg
- examines command line argument and calls a method for import or export.
ADSearch
- contains all AD logic. Public methods for export and import. OU search and creation methods. Find domain name method. Interprets lines from OU import file into nesting level and OU name. ADSearch
is the only nontrivial class. See the discussion below.
OUFile
- reads an import file into an array list. Provides methods to read the next line or go back to the previous line. The class has no knowledge of the meaning of the lines or how they will be processed.
Exporting an OU structure
Before the ADSearch
class can do any work, it must be initialized by a call to the public method InitAD()
. This method determines the DN of the Domain naming context (NC) the AD is authoritative for and stores it in ADSearch
'ss only public variable, DomainDN
. Determining the Domain NC is a common feature in any code involving the AD.
1 public static void InitAD() {
22 DirectoryEntry DomainDE = new DirectoryEntry("LDAP://rootDSE");
3 DomainDN = (DomainDE.Properties["defaultNamingContext"])[0].ToString();
4 }
The code above creates a DirectoryEntry
object passing the constructor the path "LDAP://rootDSE". This path is a pointer to the very top or root of the hierarchical data maintained on every AD in the domain. One of the things kept here is a list of the partitions or NCs supported by all the ADs in the domain. Constructing the DirectoryEntry
does not actually do anything with the AD. Any bogus path will be accepted without throwing an exception. Note the path does not contain the machine name of a specific AD. This is typical as most AD requests do not care which AD in the domain responds. The first responding AD will be used.
Communication with the AD begins on the second line of code in InitAD()
. This line uses the Directory entry of the rootDSE to lookup one of its properties, the defaultNamingContext. This is the naming context that holds domain specific data like OUs, Computers and Users. There are other naming contexts for other types of data. For example, schema, configuration, application data, and DNS. The default naming context is returned as a DN identifying the top of the naming context. For example, "DC=Doug,DC=org". Every object on an AD has a unique DN that may be used to locate it.
InitAD()
provided the DN to the top of the Domain NC, the starting point to search for all OUs in the domain. However, to preserve the OU structure the search must be limited to a single level. Otherwise one would end up with a list of OUs from all nesting levels in no apparent structure. The method SearchOneLevel()
returns only direct descendent child OUs of the passed parent.
1 private static SearchResultCollection SearchOneLevel( string path ) {
2 DirectoryEntry entry = new DirectoryEntry(path );
3 DirectorySearcher mySearcher = new DirectorySearcher(entry);
4 mySearcher.Filter = ("(objectClass=organizationalUnit)");
5 mySearcher.SearchScope = SearchScope.OneLevel;
6 return mySearcher.FindAll();
7 }
The path, "LDAP://" + DN, the starting point of the search for OUs is passed as a parameter to the SearchOneLevel()
method. Typically SearchOneLevel()
's path points to a parent OU to be searched for direct descendent child OUs. On line 2, the path is passed as a parameter to a constructor to create a DirectoryEntry
object. This associates or logically binds a DirectoryEntry
to the AD object path points to. The created DirectoryEntry
is then passed as a parameter to a constructor to create a DirectorySearcher
object on line 3. This sets up for an AD search beginning at path. The DirectorySearchers
' properties are modified in lines 4 and 5 to refine the search. The Filter
property is set to constrain the search to return only objects which are OUs. Likewise the SearchScope
property is set to limit the search to direct descendents. The search is triggered by invoking the DirectorySearcher's Findall()
method, line 6. The results of the search are returned in a SearchResultCollection
object.
If a SearchOneLevel()
is started at the top of the OU structure below, it will return a SearchResultCollection
containing 2 OUs, level1a and level 1b. Similarly, if SearchOneLevel()
is started at the OU level1a it will return a single OU, level2a. These examples should clarify what SearchOneLevel()
returns.
topOU
level1a
level2a
level1b
level2b
Given a SearchResult
returned by SearchOneLevel()
, one can proceed to search down the OU structure tree one level deeper by invoking SearchOneLevel()
on each OU in the SearchResult
. This behavior forms the basis of recursive code to export the entire OU structure. All that is needed to kickoff the process is the initial SearchResult
from the top of the Domain NC.
1 private static void OUExport(SearchResultCollection
srcCollection, int level) {
2 foreach(SearchResult resEnt in srcCollection) {
3 for (int i=level; i>0; i--) Console.Write("\t");
4
5 Console.WriteLine(
resEnt.GetDirectoryEntry().Name.ToString().Remove(0,3));
6 OUExport( SearchOneLevel(
resEnt.GetDirectoryEntry().Path), level+1);
7 }
8 }
9
10 public static void KickoffOUExport() {
11 OUExport( SearchOneLevel( "LDAP://" + DomainDN), 0);
12 }
The method KickoffOUExport()
on line 10 of the above code is used to jump start the OU export process. The method invokes SearchOneLevel()
at the top of the Domain NC to get the initial SearchResultCollection
. This is passed down to OUExport()
along with a zero to indicate the nesting level of the collection.
The recursive method OUExport()
begins on line 1. srcCollection
and level
the two parameters initially provided by KickoffOUExport()
are all that are needed to implement a recursive process to output the OU structure. The foreach on line 2 sequentially processes every OU in the passed SearchResultCollection
. The processing consists of outputting the OU name to the console in a manner displaying its place in the OU structure. This is performed on lines 3 and 5. The passed argument level
is used to write a TAB character level times. Following the TABs, the name of the OU without the leading 'OU=' is written to complete the output line. The recursive call occurs on line 6. The path to the OU just written is passed to SearchOneLevel()
which returns a SearchResultCollection
for any child OUs having the current OU as a parent. This SearchResultCollection
and the level bumped by one are used as the parameters to recursively call OUExport()
.
Importing an OU structure
During an Import the OU structure specified in an import text file is compared with the existing OU structure on a domain's AD. Any OUs present in the import file and missing on the AD are created. Similar to an export, importing an OU structure is also implemented with a recursive method. However, as the import file is processed line by line, it is sometimes necessary to backup to the previous line before continuing processing. The class OUFILE
is responsible for providing the next or previous line of the import file. It reads the import file into an arraylist of file lines and tracks the current line number. The public methods NextLine()
and PrevLine()
returns the correct line from the arraylist as a string to the caller. The class ADSearch
, which is responsible for the import, maintains the line returned by OUFile
in the private property curLine
. The curLine
property is not accessed directly as it contains both the nesting level and OU's name. These are respectively returned by the private methods GetLineLevel()
and GetLineName()
.
Every OU has a parent container. OUs are created by accessing the parent and adding a child OU to the parent. The method OUCreate()
, shown below, creates any required OUs when an OU structure is imported. OUCreate
is the workhorse used to import an OU structure.
1 private static void OUCreate ( string p_curLvlPath, string OUName) {
2
3 if (DirectoryEntry.Exists("LDAP://OU=" + OUName +
"," + p_curLvlPath) )
4 return;
5
6 if (!DirectoryEntry.Exists("LDAP://" + p_curLvlPath)) {
7 Console.WriteLine( "ERROR - parent container" +
" for new OU does not exist.");
8 Console.WriteLine( " parent: " + p_curLvlPath);
9 System.Environment.Exit(1);
10 }
11 DirectoryEntry curDE = new DirectoryEntry( "LDAP://" +
p_curLvlPath);
12 DirectoryEntries children = curDE.Children;
13 DirectoryEntry OUDE = children.Add( "OU="+OUName,
"organizationalUnit");
14 OUDE.CommitChanges();
15 Console.WriteLine( OUName + "created.");
16 }
OUCreate()
is passed the path of a parent container and the name of an OU to create in the container. On lines 2-4, OUCreate()
constructs a path to the child OU using the passed OU name and the parent's path. The child's path is passed to the static DirectoryEntry
method Exists()
to determine if the OU already exists. If the OU exists, the method returns as no work is required. If the OU does not exist, it must be created. However, before any OU is created, the existence of the parent container is verified is in lines 6-10. If the parent container does not exist the utility is terminated. This implies OUCreate()
can not create OUs in random order. The parent container must be created before any nested child OUs are created. This is precisely the condition specified by the OU Import file format. So a file created during an OU export may be used to import the OU structure. The output produced by an export satisfies the OU creation requirements of an import.
After an OU's parent is verified to exist, it may be used to create a child OU. On line 11 the path to the parent is used to construct a DirectoryEntry
object bound to the parent's container. Line 12 extracts the Children
property from the parent container's DirectoryEntry
and places it in a DirectoryEntries
collection. The collection's Add()
method is called on line 13 to create an OU. The name of the OU and its schema class are passed as parameters to the Add()
method which returns a DirectoryEntry
to the new OU it creates. However, the created OU only resides in AD cache. It has to be written back to the AD's database to become permanent. Line 14 uses the DirectoryEntry
of the created OU returned by the Add()
method to invoke its CommitChanges()
method. Invoking CommitChanges()
makes the created OU persistent. The Operator is informed whenever the import creates an OU on line 15.
OUImport()
is the recursive method that imports an OU structure into the domain. It accomplishes its task by invoking OUCreate()
to create OUs or modifying its parameters and recalling itself. OUImport()
has 2 arguments, the nesting level of a parent to create OUs in and the path to the parent. For example, it is kicked off by passing it 0 for the nesting level and the path to the Domain NC.
1
public static void OUImport( int p_curLvl, string p_curLvlPath) {
2 int lvl;
3 while ( (curLine = OUFile.NextLine()) != null ) {
4 lvl = GetLineLevel();
5 if (lvl == p_curLvl) {
6 OUCreate( p_curLvlPath, GetLineName() );
7 }
8 else if (lvl > p_curLvl) {
9 curLine = OUFile.PrevLine();
10 string newPath = "OU=" + GetLineName() + "," +
p_curLvlPath;
11 OUImport( p_curLvl+1, newPath);
12 }
13 else {
14 int commaIndex = p_curLvlPath.IndexOf(",");
15 string newPath = p_curLvlPath.Remove(0,commaIndex+1);
16 curLine = OUFile.PrevLine();
17 OUImport( p_curLvl-1, newPath);
18 }
19 }
20 }
OUImport()
processes each line in the import file. Each line contains the nesting level and name of an OU. The current nesting level is kept in parameter p_curLvl
. As each line is read, there are 3 choices for the OU nesting level. It can be equal to the current level, in which case OUCreate()
is called to create the OU, see lines 4-7. Or the OU can be at a different nesting level. Recall the nesting level in an import file can be incremented by one between lines or decremented by any number.
Lines 8-12 handle the case where the nesting level of the import file line is greater than the current nesting level. As the nesting level can only increment by 1, the previous import line must be the OU of the parent container for the OU described by the current file line. The PrevLine()
method is invoked to back up to the parent on line 9. Line 10 creates the path to the parent container. Given the path of the parent and the observation that it is nested one level deeper, OUImport is recursively called with the proper parameters to process the import line.
A different approach is used when the OU nesting level from the import file is less than current nesting level in p_curLvl
. The code is shown on lines 13-18. Here the difference in nesting levels may be greater than 1. The path to the right parent container must be calculated. This is done by removing a container from the beginning of the current path in p_curLvlPath
, decrementing the nesting level by one and calling OUImport
recursively. Note the PrevLine()
method is invoked to insure the line from the import file is processed again. This code is repeated until the nesting level indicates the correct parent container has been found.
Points of Interest
Using recursion for both the export and import of an OU structure was interesting to think about. Writing code that can eat its own output to accomplish another task makes a utility more useful. Especially if it is kept simple with a minimum of command line switches.
History
This is the initial release of OUTransport version 0.0.