Click here to Skip to main content
15,867,704 members
Articles / Programming Languages / C#
Article

OU Transporter

Rate me:
Please Sign up or sign in to vote.
4.40/5 (5 votes)
1 Nov 200615 min read 33.9K   292   19   7
A simple console utility to export the OU structure from one domain, and import it into another. It may also be used to specify an arbitrary OU structure to be created in any domain.

OUTransport Export

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.

  1. They produce or input a linear list of OUs. OU nesting is not apparent. One is not able to visualize the structure.
  2. 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.
  3. 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.
  4. 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.
  1. View the domain's current OU structure on the console.
    $OUTransport
  2. Export the current OU structure to a text file.
    $OUTransport >filename.txt
  3. 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...

  1. MainProg - examines command line argument and calls a method for import or export.
  2. 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.
  3. 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.

C#
1    public static void InitAD() {
22        DirectoryEntry DomainDE = new DirectoryEntry("LDAP://rootDSE");
3        DomainDN = (DomainDE.Properties["defaultNamingContext"])[0].ToString();
4    } // InitAD()

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.

C#
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; //enum to restrict search
6        return mySearcher.FindAll();
7    } // SearchOneLevel()

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.

C#
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        // get rid of leading "OU=" in Name,
         // just want only the name output.
5        Console.WriteLine(
           resEnt.GetDirectoryEntry().Name.ToString().Remove(0,3));
6        OUExport( SearchOneLevel(
                    resEnt.GetDirectoryEntry().Path), level+1);
7        }
8    } // OuExport()
9
10    public static void KickoffOUExport() {
11        OUExport( SearchOneLevel( "LDAP://" + DomainDN), 0);
12    } // KickoffOUExport()

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.

C#
    // Creates an OU if it does not already exist
1    private static void OUCreate ( string p_curLvlPath, string OUName) {
2        // Do nothing if OU already exists.
3        if (DirectoryEntry.Exists("LDAP://OU=" + OUName + 
                                   "," + p_curLvlPath) )
4            return; // outta here.
5        //Verify that parent container exists so you can create a child
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        } //parent does not exist
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

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.

C#
1    //p_curLvl is level of parent, p_curLvlPath is parent's path
    //started at (0, domain) so there is no real parent when started.
    public static void OUImport( int p_curLvl, string p_curLvlPath) {
2        int lvl;
3        while ( (curLine = OUFile.NextLine()) != null ) {
4        lvl = GetLineLevel();    // The level for the new OU
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        } //while NextLine()
20    } // OUImport()

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Amir Mohammad Nasrollahi8-Aug-13 20:10
professionalAmir Mohammad Nasrollahi8-Aug-13 20:10 
QuestionNice work Pin
Jim Lovejoy23-Oct-12 22:38
Jim Lovejoy23-Oct-12 22:38 
QuestionGreat piece of code Pin
cacoleman10-Feb-12 21:47
cacoleman10-Feb-12 21:47 
GeneralNice Work.. Pin
S Gnana Prakash28-May-09 23:09
S Gnana Prakash28-May-09 23:09 
Nice Work..Thumbs Up | :thumbsup: ..
GeneralGreat Pin
Albatros345-Sep-07 4:14
Albatros345-Sep-07 4:14 
GeneralFantastic Pin
DALAS_<><>14-Jun-07 23:19
DALAS_<><>14-Jun-07 23:19 
GeneralThanks Pin
Matware6-Dec-06 21:24
Matware6-Dec-06 21:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.