OU 传输器






4.40/5 (5投票s)
2006年11月1日
15分钟阅读

35126

292
一个简单的控制台实用工具,用于从一个域导出 OU 结构,并将其导入另一个域。它也可用于指定在任何域中创建任意 OU 结构。
图 1
引言
在 Active Directory 域中跨多个服务器实现的產品通常使用 AD 来存储数据。通常,数据会以某种方式进行划分并存储在 AD 的特定组织单位 (OU) 中。由于 OU 本身可能包含 OU,域的 OU 结构可能会变得复杂。通常,可视化嵌套 OU 结构的最佳方法是观察其图形表示。
可以使用“用户和计算机”管理单元来维护单个域的 OU 结构。但是,如果需要在多个域中复制 OU 结构,该怎么办?可能存在用于产品测试的域阵列、笔记本电脑上的 VMware 域、性能和容量域,最后是实际的生产域。随着域的数量和 OU 结构复杂性的增长,手动维护变得异常困难。
CSVDE 或 LDIFDE 实用工具可用于创建、导入或导出 OU 结构。然而,这些实用工具也有其缺点。
- 它们生成或输入 OU 的线性列表。OU 的嵌套关系不明显。无法可视化结构。
- 每个 OU 都由其区分名 (DN) 标识。在 DN 中,域名的后缀会附加到 OU 名称。被迫为每个 OU 的 DN 重复附加类似 DC=array6,DC=msgtst,DC=doug,DC=org 的内容。域名与在多个域之间导入或导出 OU 结构的问题无关。
- 这些通用实用工具可以在 AD 上执行许多操作。但命令行参数的数量令人望而生畏。人们可能会不确定该实用工具正在做什么,甚至为什么它会起作用。
- 顺序不保证也无关紧要。在不同操作系统上执行相同的命令可能会更改结果的顺序。
背景
Active Directory 有一个令人困惑的术语。幸运的是,理解 OUTransport 只需要少数几个术语。
- NC
- 命名上下文 (Naming Context),AD 中的数据对象被分区或隔离到 NC 中。NC 具有不同的功能和复制范围。
OUTransport
关注的是域 NC。 - DN
- 区分名 (Distinguished Name),用于在 AD 中定位数据对象的唯一名称。AD 上的数据以分层方式组织,这反映在对象的 DN 中。例如,在域 doug.org 中,域控制器 OU 的 DN 是 OU=Domain Controllers,DC=doug,DC=org。
- OU
- 组织单位 (Organizational Unit),一个用于容纳数据对象的容器。OU 可以包含子 OU。嵌套 OU 的 DN 示例是 OU=level2,OU=level1,DC=doug,DC=org。
- 路径
- ADsPath,一种使用 LDAP 语法标准在 AD 上唯一引用数据对象的方式。C# AD 类和方法通常具有 path 属性或参数。对于 OUTransport,路径是“LDAP://”和 DN 的连接。例如,域控制器 OU 的路径是 LDAP://OU=Domain Controllers,DC=doug,DC=org。
- OU 导入文件格式
- 一个文本文件,其中包含遵循规则定义的 OU,允许 OUTransport 将 OU 导入域。OUTransport 导出的输出符合 OU 导入文件格式。因此,OUTransport 导出的输出可用作导入的输入。Outransport 可以消化自己的输出。
使用代码
OUTransport.exe
,一个 NET 1.1 控制台可执行文件,在演示下载中提供。它可以在域中任何服务器上以域管理员帐户运行。要尝试 OUTransport,请打开命令提示符并 `cd` 到演示文件夹。OUTransport 通过查看其第一个也是唯一的命令行参数来确定是导出还是导入 OU 结构。如果第一个参数存在,则假定它是包含要导入域的 OU 结构的文本文件的名称。当参数缺失时,OUTransport 将 AD 的当前 OU 结构导出到控制台。- 在控制台上查看域的当前 OU 结构。
$OUTransport
- 将当前 OU 结构导出到文本文件。
$OUTransport >filename.txt
- 从文件导入 OU 结构。
$OUTransport filename.txt
图 1 显示了 OUTransport 生成的用于显示当前 OU 结构的屏幕。使用制表符表示 OU 嵌套。只显示 OU 的名称,而不是其完整的 DN。第一列中的字符 '#' 表示注释行。可以将此输出重定向到文件,如果需要,可以使用文本编辑器进行编辑,然后在另一个域上使用 OUTransport 导入该文件以重现 OU 结构。OUTransport 导出的输出可以在导入时用作其输入。这种类型的输出在本文中被称为 OU 导入文件格式。
从源代码构建
OUTransport 的所有源代码都位于一个文件中,即 OUTransport.cs。要构建该实用工具,请在命令提示符下 `cd` 到源目录,然后调用 C# 编译器处理源文件……
$ csc OUTransport.cs这将生成该实用工具的可执行文件 OUTransport.exe。
OU 导入文件格式
要定义 OU 的嵌套,必须在文件中的 OU 本身之前出现从第一级到实际包含该 OU 的父级的路径。制表符出现在每个 OU 名称之前,以指示 OU 的父级(嵌套级别)。手动编辑导入文件中输入的任何前导空格都会被忽略。OU 名称前不应出现空格,因为它们可能导致 OU 结构的可视化不正确。OUTransport 计算 OU 名称前制表符的数量来确定其嵌套级别。嵌套级别在连续的行之间只能增加 1。但是,它可以减少任意量。
#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
那错误呢?导入错误文件????
类
OUTransport
由文件 OUTransport.cs 中的三个静态类组成。这些类是……
MainProg
- 检查命令行参数并调用导入或导出方法。ADSearch
- 包含所有 AD 逻辑。用于导出和导入的公共方法。OU 搜索和创建方法。查找域名的函数。将 OU 导入文件中的行解释为嵌套级别和 OU 名称。ADSearch
是唯一不平凡的类。请参阅下面的讨论。OUFile
- 将导入文件读入 ArrayList。提供读取下一行或返回上一行的函数。该类不了解行的含义或它们将如何被处理。
导出 OU 结构
在 ADSearch
类可以执行任何工作之前,必须通过调用公共方法 InitAD()
来对其进行初始化。此方法确定 AD 权威域的域命名上下文 (NC) 的 DN,并将其存储在 ADSearch
唯一的公共变量 DomainDN
中。确定域 NC 是任何涉及 AD 的代码中的一个常见功能。
1 public static void InitAD() {
22 DirectoryEntry DomainDE = new DirectoryEntry("LDAP://rootDSE");
3 DomainDN = (DomainDE.Properties["defaultNamingContext"])[0].ToString();
4 } // InitAD()
上面的代码创建一个 DirectoryEntry
对象,将路径“LDAP://rootDSE”传递给构造函数。此路径指向域中每个 AD 上维护的分层数据的最顶层或根。这里存储的内容之一是域中所有 AD 支持的分区或 NC 列表。构造 DirectoryEntry
实际上并没有对 AD 进行任何操作。任何无效路径都会被接受而不会引发异常。请注意,路径不包含特定 AD 的机器名。这是典型的,因为大多数 AD 请求都不关心域中的哪个 AD 响应。第一个响应的 AD 将被使用。
与 AD 的通信从 InitAD()
中第二行代码开始。此行使用 rootDSE 的目录条目查找其属性之一,即 defaultNamingContext。这是包含 OU、计算机和用户等域特定数据的命名上下文。还有其他命名上下文用于其他类型的数据。例如,架构、配置、应用程序数据和 DNS。默认命名上下文作为标识命名上下文顶部的 DN 返回。例如,“DC=Doug,DC=org”。AD 上的每个对象都有一个唯一的 DN,可用于定位它。
InitAD()
提供了域 NC 顶部的 DN,这是搜索域中所有 OU 的起点。但是,为了保留 OU 结构,搜索必须限制在单个级别。否则,最终将得到来自所有嵌套级别的 OU 列表,而没有明显的结构。方法 SearchOneLevel()
只返回传递的父对象的直接后代子 OU。
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()
路径“LDAP://” + DN,是搜索 OU 的起点,作为参数传递给 SearchOneLevel()
方法。通常 SearchOneLevel()
的路径指向要搜索其直接后代子 OU 的父 OU。在第 2 行,路径作为参数传递给构造函数以创建 DirectoryEntry
对象。这会将 DirectoryEntry
与指向的 AD 对象路径关联或逻辑绑定。然后将创建的 DirectoryEntry
作为参数传递给构造函数以创建 DirectorySearcher
对象(第 3 行)。这会为从路径开始的 AD 搜索做好准备。DirectorySearchers
的属性在第 4 行和第 5 行进行了修改以优化搜索。Filter
属性设置为仅返回 OU 对象的约束。同样,SearchScope
属性设置为仅限于直接后代。通过调用 DirectorySearcher 的 Findall()
方法(第 6 行)触发搜索。搜索结果在 SearchResultCollection
对象中返回。
如果在 OU 结构下方的顶层启动 SearchOneLevel()
,它将返回一个包含 2 个 OU(level1a 和 level 1b)的 SearchResultCollection
。类似地,如果在 OU level1a 启动 SearchOneLevel()
,它将返回一个 OU,level2a。这些示例应该能清楚地说明 SearchOneLevel()
返回的内容。
topOU
level1a
level2a
level1b
level2b
给定由 SearchOneLevel()
返回的 SearchResult
,可以通过在 SearchResult
中的每个 OU 上调用 SearchOneLevel()
来继续向下搜索 OU 结构树一级。这种行为构成了递归导出整个 OU 结构代码的基础。要启动此过程,只需要来自域 NC 顶层的初始 SearchResult
。
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()
上面代码的第 10 行的 KickoffOUExport()
方法用于启动 OU 导出过程。该方法在域 NC 的顶部调用 SearchOneLevel()
以获取初始 SearchResultCollection
。这 junto um zero (along with a zero) 被传递给 OUExport()
,以指示集合的嵌套级别。
递归方法 OUExport()
在第 1 行开始。srcCollection
和 level
是由 KickoffOUExport()
最初提供的两个参数,它们足以实现一个递归过程来输出 OU 结构。第 2 行的 foreach 循环顺序处理传递的 SearchResultCollection
中的每个 OU。处理过程是将 OU 名称以显示其在 OU 结构中的位置的方式输出到控制台。这在第 3 行和第 5 行完成。使用的参数 level
用于写入制表符,次数等于 level。在制表符之后,写出 OU 的名称(去掉前面的 'OU=')以完成输出行。递归调用发生在第 6 行。刚刚写入的 OU 的路径被传递给 SearchOneLevel()
,它返回一个 SearchResultCollection
,其中包含作为当前 OU 的父级的任何子 OU。这个 SearchResultCollection
和增加一级的 level 被用作参数来递归调用 OUExport()
。
导入 OU 结构
在导入期间,将导入文本文件中指定的 OU 结构与域 AD 上现有的 OU 结构进行比较。导入文件中存在的 OU 并且在 AD 上缺失的 OU 将被创建。与导出类似,导入 OU 结构也使用递归方法实现。但是,由于导入文件是逐行处理的,有时在继续处理之前有必要回溯到上一行。OUFILE
类负责提供导入文件的下一行或上一行。它将导入文件读入文件行列表,并跟踪当前行号。公共方法 NextLine()
和 PrevLine()
将列表中的正确行作为字符串返回给调用者。负责导入的 ADSearch
类维护 OUFile
返回的行,存储在私有属性 curLine
中。curLine
属性不直接访问,因为它同时包含嵌套级别和 OU 的名称。这些分别由私有方法 GetLineLevel()
和 GetLineName()
返回。
每个 OU 都有一个父容器。通过访问父容器并向父容器添加子 OU 来创建 OU。下面显示的 OUCreate()
方法在导入 OU 结构时创建任何必需的 OU。OUCreate
是用于导入 OU 结构的主力。
// 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()
接收父容器的路径和要在容器中创建的 OU 的名称作为参数。在第 2-4 行,OUCreate()
使用传递的 OU 名称和父路径构建子 OU 的路径。然后将子路径传递给静态 DirectoryEntry
方法 Exists()
,以确定 OU 是否已存在。如果 OU 存在,则方法返回,无需进行任何操作。如果 OU 不存在,则必须创建它。但是,在创建任何 OU 之前,会验证父容器是否存在(第 6-10 行)。如果父容器不存在,则终止实用工具。这意味着 OUCreate()
无法按任意顺序创建 OU。父容器必须在任何嵌套的子 OU 创建之前创建。这正是 OU 导入文件格式指定的条件。因此,在 OU 导出过程中创建的文件可用于导入 OU 结构。导出产生的输出满足导入的 OU 创建要求。
在验证 OU 的父级存在后,它可用于创建子 OU。在第 11 行,父路径用于构造绑定到父容器的 DirectoryEntry
对象。第 12 行从父容器的 DirectoryEntry
中提取 Children
属性,并将其放入 DirectoryEntries
集合中。在第 13 行调用集合的 Add()
方法来创建 OU。将 OU 的名称及其架构类作为参数传递给 Add()
方法,该方法返回一个 DirectoryEntry
到它创建的新 OU。但是,创建的 OU 仅存在于 AD 缓存中。它必须写回 AD 的数据库才能永久生效。第 14 行使用 Add()
方法返回的创建的 OU 的 DirectoryEntry
来调用其 CommitChanges()
方法。调用 CommitChanges()
使创建的 OU 永久化。导入在 AD 上创建 OU 时(第 15 行),操作员会收到通知。
OUImport()
是将 OU 结构导入域的递归方法。它通过调用 OUCreate()
来创建 OU 或修改其参数并再次调用自身来完成任务。OUImport()
有 2 个参数:用于创建 OU 的父项的嵌套级别和指向父项的路径。例如,通过传递 0 作为嵌套级别和域 NC 的路径来启动它。
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()
处理导入文件中的每一行。每一行都包含 OU 的嵌套级别和名称。当前嵌套级别存储在参数 p_curLvl
中。在读取每一行时,OU 嵌套级别有 3 种选择。它可以等于当前级别,在这种情况下,调用 OUCreate()
来创建 OU(参见第 4-7 行)。或者 OU 可以处于不同的嵌套级别。回想一下,导入文件中的嵌套级别可以在行之间增加 1,或者减少任意数量。
第 8-12 行处理导入文件行的嵌套级别大于当前嵌套级别的情况。由于嵌套级别只能增加 1,因此上一条导入行必须是当前文件行所描述的 OU 的父容器的 OU。调用 PrevLine()
方法回溯到第 9 行的父级。第 10 行创建父容器的路径。给定父路径以及嵌套级别加深一级的观察,递归调用 OUImport 并使用正确的参数来处理导入行。
当导入文件中的 OU 嵌套级别小于 p_curLvl
中的当前嵌套级别时,使用不同的方法。代码显示在第 13-18 行。这里的嵌套级别差异可能大于 1。必须计算到正确父容器的路径。这是通过从 p_curLvlPath
的当前路径的开头移除一个容器,将嵌套级别减一,然后递归调用 OUImport
来完成的。请注意,调用 PrevLine()
方法以确保导入文件中的行再次被处理。这段代码会重复执行,直到嵌套级别指示找到正确的父容器为止。
关注点
使用递归来导出和导入 OU 结构很有意思。编写能够消化自身输出以完成另一项任务的代码使实用工具更有用。尤其是如果它保持简单,并且命令行开关最少。
历史
这是 OUTransport 版本 0.0 的初始发布。