<< Azure DevOps CI Ansible LAMP Deployment | Azure DevOps CI Ansible VPN Deployment Between Virtual Network Gateway and Cisco ASA >> |
Earlier this year I had to migrate users and groups from two disconnected domains. Also, I needed to do this without administrative access to the source domain. To do this, I programmed a small utility that automatically created the Active Directory PowerShell module commands required to recreate the users and groups in the destination domain. The program works by parsing the results of two PowerShell "Get" commands that can be run on any source domain workstation by a regular user without administrative privileges.
In this blog I will first give an overall description on how the program works, and then I will go into detail of some aspects of the program that helps ensure the Users and Groups are created in the destination domain without errors, such as handling the empty user properties and creating the Organizational Units (OU) that contain them.
The source code for the utility and the PowerShell commands that create the reports can be found on this public GitHub repository:
Better-Computing-Consulting/ad-users-groups-migrate-commands-generator: Program to generate all the necessary Active Directory PowerShell Module commands to migrate Users, Groups and Organizational Units between disconnected Domain Controllers. The program creates the commands by parsing the reports generated by Get-ADUser and Get-ADGroup commands on the source domain. (github.com)
There is also a YouTube video that demonstrates the utility:
The utility works by reading the two Users and Groups reports, and from each line in the report it creates custom ADUser and ADGroup objects. These objects have a property that when called outputs the command to create the user or the group as a string. The program, after reading and processing the reports, loops through the list of objects it created to write the PowerShell commands to a text file. After the program finishes, the commands can be copied to the destination domain to create the users and groups.
The utility will output these commands in order:
First, New-ADOrganizationalUnit to make sure every user or group has its appropriate container.
Then, New-ADUser followed by Set-ADUser to create and then assign the old domain email address to the new user as a secondary ProxyAddresse in case email delivery will be migrated to the new organization as well.
Last, New-ADGroup followed by Add-ADGroupMember to create and add users to the same groups they used to be in the old organization. The type and scope of the groups are maintained during migration.
The two PowerShell commands that create the reports can be found in the Reports.ps1 file included in the repository. The commands create the reports as Tab delimited files, as you can see in lines 4 and 12-13 of Reports.ps1:
4
|
Export-Csv -Path C:\temp\users.txt -Delimiter "`t" -NoTypeInformation
|
12 13 |
[string]::Format("{0}`t{1}`t{2}`t{3}`t{4}",$user, $g.Name, $g.DistinguishedName, $g.GroupCategory, $g.GroupScope) | `
Out-File c:\temp\groups.txt -Append}
|
The program splits each line of the reports using the Tab character as the delimiter between fields in the "New" Sub of the ADGroup and ADUser objects. See lines 129-130, 172-173 of Module.vb.
129 130 |
Public Sub New(GroupLine As String, newdomain As String)
Dim values As String() = GroupLine.Split(vbTab)
|
172 173 |
Public Sub New(UserLine As String, newdomain As String, reportheader As List(Of String))
Dim values As String() = UserLine.Split(vbTab)
|
To output the command needed to create the User or Group each custom object in the program has a property that returns the command as a string when called. For example, this is the property in the ADGroup custom object:
138 139 140 141 142 143 |
ReadOnly Property NewADGroupCMD As String
Get
Dim cmd As String = "New-ADGroup -Name " & qt & Name & qt & " -SamAccountName " & qt & Name & qt & " -GroupCategory "
& GroupCategory & " -GroupScope " & GroupScope & " -DisplayName " & qt & Name & qt & " -Path " & Path
Return cmd
End Get
End Property
|
And this is how Main calls the property within a loop to write the resulting file with the commands:
24 25 26 27 |
For Each g As ADGroup In ADGroups
cmds.WriteLine(g.NewADGroupCMD)
cmds.WriteLine(g.AddGroupMemeberCMDs)
Next
|
The PowerShell command that creates a new AD User will fail if it contains any blank properties. For example, the command fails if it includes the "-Initials" option, but the option is followed by just blank space. The program, when splitting the UserLine using the Tab delimiter, will recognize that the value of a field is an empty string, and it assigns the empty string to the object's property. Further, the custom ADUser object contains a list of another custom object called ADUserProperty for the user. This is a very small custom object that only contains two variables, Name and Value, and a property IsSet, which returns False if the value of the property is an empty string. See lines 244-257 of Module.vb.
244 245 246 247 248 249 250 251 252 253 254 255 256 257 |
Class ADUserProperty
Public Name As String
Public Value As String
Private ReadOnly qt As String = Chr(34)
Public Sub New(inName As String, inValue As String)
Name = " -" & inName.Trim(CChar(qt)) & " "
Value = inValue
End Sub
ReadOnly Property IsSet As Boolean
Get
If Value.Trim.Length > 0 Then Return True Else Return False
End Get
End Property
End Class
|
Then, when the ADUser object creates the New-ADUser command, it loops through the properties and only adds those which have a value by checking the IsSet property.
201 202 203 204 205 206 |
ReadOnly Property NewSetADUserCMDs As String
Get
Dim cmd As String = "New-ADUser"
For Each p As ADUserProperty In Properties
If p.IsSet Then cmd &= p.Name & p.Value
Next
|
For the new Users and Groups to be created successfully, the OUs that contain them need to be created ahead of time. Also, to avoid errors during the OU creation, there is only one New-ADOrganizationalUnit issued per OU, even when multiple Users or Groups share the same OU. Last, the OUs closer to the root of the domain, those that only contain other OUs, not Users or Groups, must be created before the other OUs. In other words, the OUs that contain other OUs must be created before the OUs they contain.
The OUs are created based on the DistinguishedName property of the Users and Groups. First, the program uses the DistinguishedName property to create the Path property that is needed for the New-ADUser and New-ADGroup commands. To create the Path property the program removes the CN element from the property and replaces the DC elements with those of the new domain. For example, to obtain the Path property for the users, the program takes the middle of the DistinguishedName string from the first occurrence of the string "OU=" up to the first occurrence of the string "DC=", and to this string it appends the new DN of the new domain (newdn). This happens on the custom ADUser object's "New" sub, lines 184-186 of Module.vb.
184 185 186 |
Case "DistinguishedName"
Path = qt & Mid(values(i), values(i).IndexOf("OU=") + 1, values(i).IndexOf("DC=") - values(i).IndexOf("OU=")) & newdn & qt
Properties.Add(New ADUserProperty("Path", Path))
|
Then, when the program finishes creating the lists of ADUser and ADGroup objects, the program passes these lists to the GetADOrganizationalUnits function, which creates the list of custom ADOrganizationalUnit objects. To make sure the that there only one New-ADOrganizationalUnit command for each OU, the function makes a list of all the required paths, only adding the first occurrence of each path to the list. This is done on lines 70-75 of the function.
70 71 72 73 74 75 |
For Each u As ADUser In ADUsers
If Not OUs.Contains(u.Path) Then OUs.Add(u.Path)
Next
For Each g As ADGroup In ADGroups
If Not OUs.Contains(g.Path) Then OUs.Add(g.Path)
Next
|
Next, to get the list of custom ADOrganizationalUnit objects the program loops through this list paths and for each Path, it loops again through each element of the Path, assigning the first element to the Name of a new ADOrganizationalUnit object, and the remaining elements to the Path of the object. Each time the loop goes through the Path it removes the first element from it until the there is nothing left in the Path, in which case the new object only gets the new domain DN as its path. And for each new object the loop creates it only adds the first occurrence of the object to the list, ensuring the uniqueness of each OU object. This is done on lines 80-89 of the GetADOrganizationalUnits function.
80 81 82 83 84 85 86 87 88 89 |
Do
Dim ouname As String = oupath(0).Replace("OU=", "")
oupath.RemoveAt(0)
Dim newpath As String = ""
For Each ou As String In oupath
newpath &= ou & ","
Next
Dim anOU As New ADOrganizationalUnit(ouname, newpath & newdn.TrimStart(CChar(",")))
If Not ADOrganizationalUnits.Contains(anOU) Then ADOrganizationalUnits.Add(anOU)
Loop While oupath.Count > 0
|
Lastly, to make sure that the OUs closer to the root of the domain are created first, the GetADOrganizationalUnits sorts the list of OU objects before returning it to Main, as seen on lines 91-92 of the function.
91 92 |
ADOrganizationalUnits.Sort(Function(x, y) x.PathLengh.CompareTo(y.PathLengh))
Return ADOrganizationalUnits
|
The Sort function uses the PathLengh property of the custom ADOrganizationalUnit object to sort the OUs. This property is calculated based on the number of elements the Path of the OU object contains.
109 110 111 112 113 |
ReadOnly Property PathLengh As Integer
Get
Return Path.Split(",").Count
End Get
End Property
|
Thus, by the time the list of OUs is returned to Main, the list of OUs is composed of unique elements sorted in ascending order. Main then first loops through the list of OUs to get their New-ADOrganizationalUnit commands to make sure these commands come before the New-ADUser and New-ADGroup commands.
Thank you for reading.
IT Consultant
Better Computing Consulting
<< Azure DevOps CI Ansible LAMP Deployment | Azure DevOps CI Ansible VPN Deployment Between Virtual Network Gateway and Cisco ASA >> |
F: (310) 935-0341
Mon -Fri 9AM - 6PM Pacific Time