GrabDuck

Миграция контроллера домена с SAMBA на ActiveDirectory

:

image
Вот и пришло время рассказать о способе, который методом научного тыка, нескольких умных людей и несколько часов свободного времени помогли мне мигрировать домен, построенный предыдущим админом с SAMBA на ActiveDirectory.

Многие знают и используют возможность использовать SAMBA в качестве домена, но, по моему мнению, это можно использовать только как лабораторный стенд, а в реальный жизни лучше не использовать. Это решает каждый сам, меня не устроило отсутствие групповых политик, постоянная невозможность авторизации, частые зависания и неимоверные утечки памяти (через 30 дней непрерывной работы, иногда, пожираются все 16 ГиБ оперативной памяти и утилизируется весь swap-раздел). Поэтому моему терпению пришёл конец, я попытался собраться с мыслями и несколько раз пытался эмулировать переход в виртуальной среде, но всё не было времени, пока не пришёл сибирский пушистый зверёк и не положил по непонятным причинам всю структуру и зависящий от него bind9, который полностью парализовал работу офиса в 70 человек. Вот тут-то и настало время срочного перехода, 2 дня чтения разных мануалов, 2 дня попыток взлететь на ActiveDirectory ни к чему не привели, пока не натолкнулся на эту статью и она же на TechNet .
Так же били попытки смены ролей домена, т.е. ввести в линуксовый домен Windows Server и сделать его резервным контроллером, а затем повысить его роль до pdc, но, как показал 1 тест в виртуальной среде – это была неудачная идея, т.к. переносились все глюки GPO (их просто не было), а возможно ещё какие-либо недоразумения.

У меня всё же не получилось всё свести к одному нажатию кнопки, поэтому будет использоваться Active Perl (х86), Microsoft Office Excel (делал на 2003 и 2007), Batch, VBS, а так же утилита newsid ( которую на сайте успешно удалили) и дополнительный модуль acctinfo.
Все действия будут происходить в Windows-окружении на рабочей станции, а потом Windows Server (здесь хочется уточнить, что Windows Server был 2003 R2 SP2 по некоторым соображениям, поэтому работа на более современных системах не тестировалась), так же есть один нюанс, Windows Server лучше использовать английской версии, без MUI, пока происходит миграция, потом ничто не мешает его поставить.

Итак, хватит воды, приступим к самому процессу.

Перед дальнейшими работами можно начать ставить систему, но ничего в ней не настраивая и не создавая (я даже драйвера ставить не стал, пока не создам пользователей).

Дня начала нам надо вытащить список всех пользователей и групп, в которых они находятся (у меня была проблема, не все пользователи экспортировались с группами, в которые они входят, надо проверять), для этого используем vbs скрипт, который нашёл здесь, но его пришлось модифицировать, что бы получить более наглядно и нужные параметры (так же стоит упомянуть что это надо делать на ПК, входящим в домен и под доменной учёткой, я делал под учёткой глобального администратора домена).

export AD User Accounts.vbs
On Error Resume Next 
 
strFileName = "Users-Groups-SIDs.xlsx" 
 
Set objShell = CreateObject("Wscript.Shell") 
 
strPath = Wscript.ScriptFullName 
Set objFSO = CreateObject("Scripting.FileSystemObject") 
 
Set objFile = objFSO.GetFile(strPath) 
strFolder = objFSO.GetParentFolderName(objFile) 
 
 
SET objExcelApp = CREATEOBJECT("Excel.Application") 
SET objWB = objExcelApp.Workbooks.Add 
SET objExcel = objWB.Worksheets(1) 
 
objWB.SaveAs(strFolder & "\" & strFileName) 
 
Const ADS_SCOPE_SUBTREE = 2 
 
Set objConnection = CreateObject("ADODB.Connection") 
Set objCommand =   CreateObject("ADODB.Command") 
objConnection.Provider = "ADsDSOObject" 
objConnection.Open "Active Directory Provider" 
Set objCommand.ActiveConnection = objConnection 
 
objCommand.Properties("Page Size") = 1000 
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE  
 
'Set the path of the file to the same folder of the script 
'Open the file and make the workbook visible 
Set objExcel = CreateObject("Excel.Application") 
Set objWorkbook = objExcel.Workbooks.Open(strFolder & "\" & strFileName) 
objExcel.Visible = True 
 
'objExcel.Cells(1, 1).Value = "Name" 
'objExcel.Cells(1, 1).Font.Bold = TRUE 
'objExcel.Columns(1).ColumnWidth = 40 
'objExcel.Cells(1, 2).Value = "Security ID" 
'objExcel.Cells(1, 2).Font.Bold = TRUE 
'objExcel.Columns(2).ColumnWidth = 60 
 
'Starting row of the Excel is 2, since first row are column headings 
y = 2

 
objCommand.CommandText = _ 
    "SELECT * FROM 'LDAP://DC=mvi,DC=srv' WHERE objectCategory='user'"   
Set objRecordSet = objCommand.Execute 
  
objRecordSet.MoveFirst 
   Do Until objRecordSet.EOF 
   strADsPathUser = objRecordSet.Fields("ADsPath").Value 
   'wScript.echo strADsPathUser  
   Set objUser = GetObject(strADsPathUser) 
   
   
z = 1
      objExcel.Cells(y,z) = objUser.sn
      objExcel.Cells(1, z).Value = "sn" 
      'Wscript.Echo objUser.sn
      objExcel.Cells(1, z).Font.Bold = TRUE 
z = z + 1
      objExcel.Cells(y,z) = objUser.givenName
      objExcel.Cells(1, z).Value = "givenName" 
      'Wscript.Echo objUser.givenName
      objExcel.Cells(1, z).Font.Bold = TRUE 
z = z + 1
      objExcel.Cells(y,z) = objUser.initials
      objExcel.Cells(1, z).Value = "initials" 
      'Wscript.Echo objUser.initials
      objExcel.Cells(1, z).Font.Bold = TRUE       
z = z + 1
      objExcel.Cells(y,z) = objUser.description 
      objExcel.Cells(1, z).Value = "description" 
      objExcel.Cells(1, z).Font.Bold = TRUE 
      'Wscript.Echo objUser.description       
z = z + 1
      objExcel.Cells(y,z) = objUser.codePage 
      objExcel.Cells(1, z).Value = "codePage" 
      objExcel.Cells(1, z).Font.Bold = TRUE 
      'Wscript.Echo objUser.codePage        
   
z = z + 1
      objExcel.Cells(y,z) = objUser.sAMAccountName 
      objExcel.Cells(1, z).Value = "sAMAccountName"
      'Wscript.Echo objUser.sAMAccountName  
      objExcel.Cells(1, z).Font.Bold = TRUE
z = z + 1
      objExcel.Cells(y,z) = objUser.codePage 
      objExcel.Cells(1, z).Value = "codePage" 
      objExcel.Cells(1, z).Font.Bold = TRUE 
      'Wscript.Echo objUser.codePage        
z = z + 1      
      objExcel.Cells(y,z) = objUser.mail
      objExcel.Cells(1, z).Value = "mail" 
      'Wscript.Echo objUser.mail
      objExcel.Cells(1, z).Font.Bold = TRUE        

z = z + 1 
      intUserSID = fnGet_HexString(objUser.ObjectSID)            
      objExcel.Cells(y,z) = intUserSID   
      objExcel.Cells(1, z).Value = "ObjectSID"
      'Wscript.Echo objUser.ObjectSID  
      objExcel.Cells(1, z).Font.Bold = TRUE 
 




z = z + 1
      objExcel.Cells(y,z) = objUser.userPrincipalName
      objExcel.Cells(1, z).Value = "userPrincipalName" 
      'Wscript.Echo objUser.userPrincipalName
      objExcel.Cells(1, z).Font.Bold = TRUE 


z = z + 1      
      objExcel.Cells(y,z) = objUser.displayName
      objExcel.Cells(1, z).Value = "displayName" 
      'Wscript.Echo objUser.displayName
      objExcel.Cells(1, z).Font.Bold = TRUE 

     
z = z + 1      
      objExcel.Cells(y,z) = objUser.distinguishedName
      objExcel.Cells(1, z).Value = "distinguishedName" 
      'Wscript.Echo objUser.distinguishedName
      objExcel.Cells(1, z).Font.Bold = TRUE  
z = z + 1 
      intUserSID = stringlist(objUser.memberOf)            
      objExcel.Cells(y,z) = intUserSID   
      objExcel.Cells(1, z).Value = "memberOf"
      'Wscript.Echo objUser.memberOf  
      objExcel.Cells(1, z).Font.Bold = TRUE 
      
      
      
      
           
' нет необходимости      
z = z + 1      
      objExcel.Cells(y,z) = objUser.physicalDeliveryOfficeName
      objExcel.Cells(1, z).Value = "physicalDeliveryOfficeName" 
      'Wscript.Echo objUser.physicalDeliveryOfficeName
      objExcel.Cells(1, z).Font.Bold = TRUE 
z = z + 1      
      objExcel.Cells(y,z) = objUser.telephoneNumber
      objExcel.Cells(1, z).Value = "telephoneNumber" 
      'Wscript.Echo objUser.telephoneNumber
      objExcel.Cells(1, z).Font.Bold = TRUE

z = z + 1      
      objExcel.Cells(y,z) = objUser.profilePath
      objExcel.Cells(1, z).Value = "profilePath" 
      'Wscript.Echo objUser.profilePath
      objExcel.Cells(1, z).Font.Bold = TRUE
z = z + 1      
      objExcel.Cells(y,z) = objUser.scriptPath
      objExcel.Cells(1, z).Value = "scriptPath" 
      'Wscript.Echo objUser.scriptPath
      objExcel.Cells(1, z).Font.Bold = TRUE
z = z + 1      
      objExcel.Cells(y,z) = objUser.homeDirectory
      objExcel.Cells(1, z).Value = "homeDirectory" 
      'Wscript.Echo objUser.homeDirectory
      objExcel.Cells(1, z).Font.Bold = TRUE
z = z + 1      
      objExcel.Cells(y,z) = objUser.homeDrive
      objExcel.Cells(1, z).Value = "homeDrive" 
      'Wscript.Echo objUser.homeDrive
      objExcel.Cells(1, z).Font.Bold = TRUE
z = z + 1      
      objExcel.Cells(y,z) = objUser.title
      objExcel.Cells(1, z).Value = "title" 
      'Wscript.Echo objUser.title
      objExcel.Cells(1, z).Font.Bold = TRUE
z = z + 1      
      objExcel.Cells(y,z) = objUser.department
      objExcel.Cells(1, z).Value = "department" 
      'Wscript.Echo objUser.department
      objExcel.Cells(1, z).Font.Bold = TRUE
z = z + 1      
      objExcel.Cells(y,z) = objUser.company
      objExcel.Cells(1, z).Value = "company" 
      'Wscript.Echo objUser.company
      objExcel.Cells(1, z).Font.Bold = TRUE
z = z + 1      
      objExcel.Cells(y,z) = objUser.manager
      objExcel.Cells(1, z).Value = "manager" 
      'Wscript.Echo objUser.manager
      objExcel.Cells(1, z).Font.Bold = TRUE
z = z + 1      
      objExcel.Cells(y,z) = objUser.homePhone
      objExcel.Cells(1, z).Value = "homePhone" 
      'Wscript.Echo objUser.homePhone
      objExcel.Cells(1, z).Font.Bold = TRUE
z = z + 1      
      objExcel.Cells(y,z) = objUser.pager
      objExcel.Cells(1, z).Value = "pager" 
      'Wscript.Echo objUser.pager
      objExcel.Cells(1, z).Font.Bold = TRUE
z = z + 1      
      objExcel.Cells(y,z) = objUser.mobile
      objExcel.Cells(1, z).Value = "mobile" 
      'Wscript.Echo objUser.mobile
      objExcel.Cells(1, z).Font.Bold = TRUE
z = z + 1      
      objExcel.Cells(y,z) = objUser.facsimileTelephoneNumber
      objExcel.Cells(1, z).Value = "facsimileTelephoneNumber" 
      'Wscript.Echo objUser.facsimileTelephoneNumber
      objExcel.Cells(1, z).Font.Bold = TRUE
z = z + 1      
      objExcel.Cells(y,z) = objUser.ipphone
      objExcel.Cells(1, z).Value = "ipphone" 
      'Wscript.Echo objUser.ipphone
      objExcel.Cells(1, z).Font.Bold = TRUE
z = z + 1      
      objExcel.Cells(y,z) = objUser.info
      objExcel.Cells(1, z).Value = "info" 
      'Wscript.Echo objUser.info
      objExcel.Cells(1, z).Font.Bold = TRUE
z = z + 1      
      objExcel.Cells(y,z) = objUser.streetAddress
      objExcel.Cells(1, z).Value = "streetAddress" 
      'Wscript.Echo objUser.streetAddress
      objExcel.Cells(1, z).Font.Bold = TRUE
z = z + 1      
      objExcel.Cells(y,z) = objUser.postOfficeBox
      objExcel.Cells(1, z).Value = "postOfficeBox" 
      'Wscript.Echo objUser.postOfficeBox
      objExcel.Cells(1, z).Font.Bold = TRUE
z = z + 1      
      objExcel.Cells(y,z) = objUser.l
      objExcel.Cells(1, z).Value = "l" 
      'Wscript.Echo objUser.l
      objExcel.Cells(1, z).Font.Bold = TRUE
z = z + 1      
      objExcel.Cells(y,z) = objUser.st
      objExcel.Cells(1, z).Value = "st" 
      'Wscript.Echo objUser.st
      objExcel.Cells(1, z).Font.Bold = TRUE
z = z + 1      
      objExcel.Cells(y,z) = objUser.c
       objExcel.Cells(1, z).Value = "c" 
      'Wscript.Echo objUser.c
      objExcel.Cells(1, z).Font.Bold = TRUE
z = z + 1      
      objExcel.Cells(y,z) = objUser.wWWHomePage
      objExcel.Cells(1, z).Value = "wWWHomePage" 
      'Wscript.Echo objUser.wWWHomePage
      objExcel.Cells(1, z).Font.Bold = TRUE
      
'''''''''''''''''''''''''''
      y = y + 1
      objRecordSet.MoveNext 
   Loop 
 
objCommand.CommandText = _ 
    "SELECT * FROM 'LDAP://DC=mvi,DC=srv' WHERE objectCategory='group'"   
Set objRecordSet = objCommand.Execute 
 
 
objRecordSet.MoveFirst 
   Do Until objRecordSet.EOF 
 
      strADsPathGroup = objRecordSet.Fields("ADsPath").Value 
      'wScript.echo strADsPathGroup  
      Set objGroup = GetObject(strADsPathGroup) 
      
      'if objGroup.groupType = "-2147483646" then 
      objExcel.Cells(y,1) = objGroup.sAMAccountName 
      'Wscript.Echo objUser.sAMAccountName 
 
      intGroupSID = fnGet_HexString(objGroup.ObjectSID)           
 
      objExcel.Cells(y,2) = intGroupSID   
      'Wscript.Echo intUserSID 
      'End if 
      y = y + 1 
      objRecordSet.MoveNext 
   Loop 
 
objRecordSet.Close 
objConnection.Close 
 
SET objSheet = NOTHING 
SET objWB =  NOTHING 
objExcelApp.Quit() 
SET objExcelApp = NOTHING 
 
Wscript.echo "Script Finished..." 
 


'''
Function stringlist(memberOf)     
  Dim objmemberOf    

' Heart of the script, extract a list of Groups from memberOf
objmemberOf  = objUser.GetEx("memberOf")
For Each objGroup in objmemberOf
   strList = strList & """" & objGroup & """" & " "
Next
stringlist = strUser & strList
'WScript.Echo "Groups for " & strUser & strList
End Function 
'''

 
Function fnGet_HexString(intSID)     
  Dim strRet, i, b     
  strRet = "" 
      
  For i = 0 to Ubound(intSID)         
   b = hex(ascb(midb(intSID,i+1,1)))         
   If( len(b) = 1 ) then b = "0" & b            
    strRet = strRet & b     
  Next         
 
  fnGet_HexString = fnHexStrToDecStr(strRet)     
End Function 
 
Function fnHexStrToDecStr(strSid)   
  Dim arrbytSid, lngTemp, j  
  ReDim arrbytSid(Len(strSid)/2 - 1)  
 
  For j = 0 To UBound(arrbytSid)      
   arrbytSid(j) = CInt("&H" & Mid(strSid, 2*j + 1, 2))  
  Next  
 
  fnHexStrToDecStr = "S-" & arrbytSid(0) & "-" & arrbytSid(1) & "-" & arrbytSid(8)  
  lngTemp = arrbytSid(15)  
  lngTemp = lngTemp * 256 + arrbytSid(14)  
  lngTemp = lngTemp * 256 + arrbytSid(13)  
  lngTemp = lngTemp * 256 + arrbytSid(12)  
 
  fnHexStrToDecStr = fnHexStrToDecStr & "-" & CStr(lngTemp)  
 
  lngTemp = arrbytSid(19)  
  lngTemp = lngTemp * 256 + arrbytSid(18)  
  lngTemp = lngTemp * 256 + arrbytSid(17)  
  lngTemp = lngTemp * 256 + arrbytSid(16)  
 
  fnHexStrToDecStr = fnHexStrToDecStr & "-" & CStr(lngTemp)  
 
  lngTemp = arrbytSid(23)  
  lngTemp = lngTemp * 256 + arrbytSid(22)  
  lngTemp = lngTemp * 256 + arrbytSid(21)  
  lngTemp = lngTemp * 256 + arrbytSid(20)  
 
  fnHexStrToDecStr = fnHexStrToDecStr & "-" & CStr(lngTemp)  
 
  lngTemp = arrbytSid(25)  
  lngTemp = lngTemp * 256 + arrbytSid(24)  
 
  fnHexStrToDecStr = fnHexStrToDecStr & "-" & CStr(lngTemp)  
End Function  

При запуске откроется окно Office Excel, с открытым файлом Users-Groups-SIDs.xlsx и в него будут построчно записываться данные (желательно мышку не трогать, никуда не нажимать пока работа не закончится, иначе могут быть ошибки в получении данных). После окончания работы скрипта получим уведомление, Script Finished... которое означает его завершение, теперь подождём несколько секунд что бы скрипт освободил таблицу, на это мы получим уведомление от офиса, что таблица доступна для записи, соглашаемся и жмём кнопку «сохранить». На выходе у нас получилась таблица скрин таблицы где есть столбцы: sn; givenName; initials; description; codePage; sAMAccountName; codePage; mail; ObjectSID; userPrincipalName; displayName; distinguishedName; memberOf; physicalDeliveryOfficeName; telephoneNumber; profilePath; scriptPath; homeDirectory; homeDrive; title; department; company; manager; homePhone; pager; mobile; facsimileTelephoneNumber; ipphone; info; streetAddress; postOfficeBox; l; st; c; wWWHomePage

Большинство из них нам не нужны (можно поправить в самом скрипте, удалив ненужные параметры, но на первое время пусть будут все).
Из полученных значений сейчас нам понадобятся столбцы sAMAccountName и ObjectSID, сортируем ObjectSID в порядке возрастания (от А до Я) копируем их сохраняя в текстовый файл users.txt, немного изменив их вид, должно поучится так: sAMAccountName,ObjectSID
Т.е. вот так:

dns-gw-sult,S-1-5-21-833212901-2941102506-3986841923-1101
DnsAdmins,S-1-5-21-833212901-2941102506-3986841923-1102
IIS_IUSRS,S-1-5-21-833212901-2941102506-3986841923-1102
DnsUpdateProxy,S-1-5-21-833212901-2941102506-3986841923-1103
ivanov,S-1-5-21-833212901-2941102506-3986841923-1105
ozonov,S-1-5-21-833212901-2941102506-3986841923-1108
elina,S-1-5-21-833212901-2941102506-3986841923-1111
anna,S-1-5-21-833212901-2941102506-3986841923-1113
dash,S-1-5-21-833212901-2941102506-3986841923-1115
denis,S-1-5-21-833212901-2941102506-3986841923-1116
danuev,S-1-5-21-833212901-2941102506-3986841923-1119

Как видим, каждая запись в отдельной строчке, можно увидеть что последние номера идут по порядку, но есть и отсутствующие интервалы (поскольку самба писала пользователей в LDAP с разными RID (т.е. номерами по порядку), а AD будет создавать пользователей подряд, начиная с какого-то определенного RID, нужно создать всех недостающих пользователей и как-то более или менее понятно их обозвать), вот мы их и заполним, используя perl скрипт script-add user.pl:

script-add user.pl
use strict;
use warnings;
use Data::Dumper;

my (%input, %output,$max);

my $input_file = "users.txt";
my $output_file = "output.txt";
my $sambaSID = "S-1-5-21-833212901-2941102506-3986841923-";

open FIN, "<$input_file";
while (<FIN>) {
	 chomp();
	 if (/(.*),$sambaSID(.*)/) {
		 $input{$2}=$1;
		 $max=$2
			if $2 > $max;
	 }	 
}
close FIN;

print Dumper(\%input);
print Dumper($max);

open FOUT, ">$output_file";
for (my $i=1001;$i<=$max;$i++) {
	 if (exists $input{$i}) {
		  print "input: $input{$i} i: $i\n";
		  print FOUT "$input{$i}\n";
     } else {
          print FOUT "user$i\n";
     }
}
close FOUT; 

В изначальной статье есть не очень рабочие моменты, поэтому попросил знакомого исправить ошибки, по крайней мере скрипт заработал как планировалось.
И на выходе получили файл output.txt (в котором пользователи будут начинаться с RID 1001 и далее и иметь имена user1001 и т.д.) с содержанием:

output.txt
user1001
user1002
user1003
user1004
user1005
user1006
user1007
user1008
user1009
user1010
user1011
user1012
user1013
user1014
user1015
user1016
user1017
user1018
user1019
user1020
user1021
user1022
user1023
user1024
user1025
user1026
user1027
user1028
user1029
user1030
user1031
user1032
user1033
user1034
user1035
user1036
user1037
user1038
user1039
user1040
user1041
user1042
user1043
user1044
user1045
user1046
user1047
user1048
user1049
user1050
user1051
user1052
user1053
user1054
user1055
user1056
user1057
user1058
user1059
user1060
user1061
user1062
user1063
user1064
user1065
user1066
user1067
user1068
user1069
user1070
user1071
user1072
user1073
user1074
user1075
user1076
user1077
user1078
user1079
user1080
user1081
user1082
user1083
user1084
user1085
user1086
user1087
user1088
user1089
user1090
user1091
user1092
user1093
user1094
user1095
user1096
user1097
user1098
user1099
user1100
dns-gw-sult
IIS_IUSRS
DnsUpdateProxy
user1104
ivanov
user1106
user1107
ozonov
user1109
user1110
elina
user1112
anna
user1114
dash
denis
user1117
user1118
danuev

Полученный список помещаем в таблицу dsadd-new.xls пришлось немного изменить под свои нужды. В изменённую таблицу помещаем в столбец Login. Начальные SID пользователей в столбец SID и проверяем, у пользователя user-1101 должен быть S-1-5-21-833212901-2941102506-3986841923-1101 у пользователя user-1102 такой S-1-5-21-833212901-2941102506-3986841923-1102 (думаю что логика работы понятна, а SID известных пользователей должен оставаться таким же как и был, у нас есть в файле Users-Groups-SIDs.xlsx). Поместить пользователя в группу, в какой он раньше был, то для этого надо из файла Users-Groups-SIDs.xlsx взять столбец memberOf и поместить каждому пользователю в файл dsadd-new.xls в столбец GROUP. Так же необходимо заполнить все остальные поля, если надо, но обязательно нажо заполнить столбцы Фамилия и Имя, если этого не сделать, то формирование команд на создание будет некорректным. Столбец Login автоматически сформирует имя пользователя для входа в домен, если Вас не устроит смена логина, измените шаблон или же напишите сами необходимый логин (у меня некоторые пользователи имели не такой как принято логин, поэтому у них заменял на необходимый).
Так же прошу обратить внимание на тот факт, что если:
— столбец Отчество не будет заполнено, то при создании пользователей к ним добавится лишний пробел вконце, что может повлечь к проблемам у некоторых программ;
— автоматическое создание группы у меня так и не получилось допилить, поэтому создадим группы при помощи крипта, который создаст их в OU-Users, но это может повлечь к неработоспособности некоторых авторизаций в различных сервисах (Apache, OpenVPN, etc.), т.к. у меня были ещё и OU-Builtin и OU-Groups, но решил пока сложить всё в кучу.

add_group.cmd
rem Создание групп (CN) в CN Users
rem dsadd group "cn=,cn=users,dc=mvi,dc=srv"

for %%A in (
jira-users, jira-administrators, Developers,
jira-developers, mvi-users, berry-dev,
online-developers, marketing-users, Marketing,
ne-users, ne-developers, ne-admin,
marketing-administrators, online-users, bills,QA,
) do dsadd group "cn=%%A,cn=users,dc=mvi,dc=srv"

rem Создание подразделения (OU) в корне
rem dsadd group "cn=,cn=groups,dc=mvi,dc=srv"

dsadd ou "ou=Groups,dc=mvi,dc=srv"
for %%B in (
vpn-users, svn-users, jenkins-adm, jenkins,
PHP_Developers, amazon-users,
) do dsadd group "cn=%%B,ou=groups,dc=mvi,dc=srv"

После выполнения рутинных операций по копипасту и проверке, можно подготовить bat файлы которые помогут создать пользователей и команды по добавлению их в группы. В столбцах: ИТОГО и ИТОГО в группу находятся команды для создания пользователей и команды по добавлению их в группы соответственно, их содержимое и сохраним в bat файлы add_user.cmd, add_group.cmd.
Формирование пользователей и групп готово.

Теперь займёмся Windows Server. На текущий момент система должна быть установлена. Нам понадобится утилита newsid. Возьмём SID из списка пользователей, это S-1-5-21-833212901-2941102506-3986841923 (из полученного SID пользователя удалим последние знаки, вплоть до "-" и получим SID домена), и заменим текущий SID системы на наш. Система начнёт изменения и перезагрузится (после перезагрузки можно ещё раз запустить утилиту и посмотреть что SID изменился). Всё, можно устанавливать службу ActiveDirectory (не знаю кто как, но я сначала ставлю DNS, пропуская его настройки, потом при помощи dcpromo настраиваю саму службу домена), но перед этим надо либо выключить сервер SAMBA и bind, либо выключить эти службы (мне пришлось выключить службы, т.к. это ещё и шлюз в интернет руки бы оторвал такому админу). Не знаю, можно ли указать другое название домена (ничто не мешает), но мне надо было оставить текущее. Производим необходимые настройки и перезагружаем систему (всё как обычно). Теперь необходимо установить модуль для расширения отображаемых свойств объекта acctinfo (по ссылке выше, откуда скачивать, написано как устанавливать), запускаем оснастку «Active Directory — пользователи и компьютеры». Пытаемся создать одного пользователя из первой строки скрипта add_user.cmd

dsadd User "cn=user-1101 user-1101 ,cn=users,dc=mvi,dc=srv" -UPN dns-gw-sult@mvi.srv -samid dns-gw-sult -display "user-1101 user-1101 " -fn user-1101  -ln user-1101 -pwd "p6Jx3Xre" -mustchpwd yes -disabled no -pwdneverexpires yes

Смотрим в «Active Directory — пользователи и компьютеры» какой у него SID в свойствах, во вкладке Additional Account Info. Если имя пользователя соответствует с его окончанием SID, то всё правильно и не требует коррекции (если SID не соответствует, то начинаем с пользователя со следующим номером SID, т.е. к текущему прибавляем 1 ). На этом месте у меня произошёл epic fail с одним пользователем, у него SID содержал 1105, а пользователи у меня начали создаваться с 1106, поэтому пришлось долго мучатся и переносить все данные пользователя, т.к. его идентификатор был неверным.
После выяснения порядка, с которого создаются пользователи необходимо скорректировать создание пользователей, удалив/закомментировав команды, которые уже не соответствуют, т.е. убираем всё, что находится до user-1107 и можно смело запускать скрипт add_user.cmd. После выполнения надо создать группы, иначе ничего не выйдет. Запускаем add_group.cmd проверяем, все ли группы создались (стоит обратить внимание на то, что создаются глобальные группы безопасности, если необходимы другие типы, то надо добавить в скрипт -scope {l | g | u}, читаем мануал по dsadd group).
dsadd group /?
Описание.  Добавление группы в каталог.
Синтаксис: dsadd group <GroupDN> [-secgrp {yes | no}] [-scope {l | g | u}]
           [-samid <SAMName>] [-desc <Description>] [-memberof <Group ...>]
           [-members <Member ...>] [{-s <Server> | -d <Domain>}] [-u <UserName>]
           [-p {<Password> | *}] [-q] [{-uc | -uco | -uci}]
Параметры

Значение                Описание
<DN_группы>             Обязательное или stdin. Различающееся имя (DN) 
                        добавляемой группы. Если конечный объект не указан, 
                        он берется из стандартного ввода (stdin).
-secgrp {yes | no}      Указывает, что группа является (yes) или не является
                        (no) группой безопасности. По умолчанию: yes.
-scope {l | g | u}      Указывает, что область действия группы является
                        локальной (l), глобальной (g) или универсальной (u).
                        Если домен находится в смешанном режиме, универсальная
                        область не поддерживается. По умолчанию: глобальная.
-samid <имя_SAM>        Задает имя учетной записи SAM группы <имя_SAM>
                        (например, "operators").
-desc <описание>        Задает описание группы <описание>.
-memberof <группа...>   Делает группу членом одной или нескольких групп,
                        определяемых разделяемым пробелами списком имен DN 
                        <группа...>.
-members <член...>      Добавляет одного или несколько членов в группу. Члены
                        задаются разделяемым пробелами списком имен <член...>.
{-s <сервер> | -d <домен>}
                        -s <сервер> задает подключение к контроллеру домена
                        (КД) с именем <сервер>.
                        -d <домен> задает подключение к КД в домене.
                        По умолчанию: КД в домене входа.
-u <пользователь>       Подключение с именем <пользователь>. По умолчанию:
                        вошедший пользователь. Формат имени: имя,
                        домен\имя или имя участника-пользователя (UPN).
-p {<пароль> | *}       Пароль пользователя <пользователь>. Если задана *,
                        выводится приглашение ввести пароль.
-q                      Тихий режим: подавление всего вывода для 
                        стандартного вывода.
{-uc | -uco | -uci}     -uc Задает вход с канала или вывод на канал 
                        в формате Юникод. 
                        -uco Задает вывод на канал или в файл 
                        в формате Юникод. 
                        -uci Задает вход с канала или из файла
                        в формате Юникод.

Примечание.
Если целевой объект не указан в командной строке, этот целевой объект будет 
получен через стандартный ввод (STDIN). Данные STDIN могут быть приняты с 
клавиатуры, перенаправлены из файла или из другой команды. Чтобы обозначить 
конец ввода данных STDIN с клавиатуры или перенаправленного файла, используйте 
CTRL+Z или символ конца файла (EOF).

Если указываемое значение содержит пробелы, заключите его в кавычки (например, 
"CN=Ivan Ivanov,CN=Users,DC=microsoft,DC=com").
Если вы указываете несколько значений, разделите их пробелами (например, список 
различающихся имен группы). 
См. также:
dsadd computer /? - сведения по добавлению компьютера в каталог.
dsadd contact /? - сведения по добавлению контакта в каталог.
dsadd group /? - сведения по добавлению группы в каталог.
dsadd ou /? - сведения по добавлению подразделения в каталог.
dsadd user /? - сведения по добавлению пользователя в каталог.
dsadd quota /? - сведения по добавлению квоты в каталог.

Дополнительные сведения по программам командной строки службы каталогов:
dsadd /? - сведения по добавлению объектов.
dsget /? - сведения по отображению объектов.
dsmod /? - сведения по изменению объектов.
dsmove /? - сведения по перемещению объектов.
dsquery /? - сведения по поиску объектов, отвечающих заданным условиям.
dsrm /? - сведения по удалению объектов.

После проверки, запускаем скрипт, который добавит пользователей в группы add_user_group.cmd (обращаю внимание что могут быть различия, проверяйте где были созданы группы и какие указаны у пользователя).

Вот и закончили миграцию, теперь у нас ActiveDirectory вместо SAMBA, пользователи имеют те же идентификаторы, осталось ввести в домен сами компьютеры и задача будет завершена, придётся только допиливать некоторые моменты, которые индивидуальны у каждого.

Текст, написанный мной, как обычно сумбурен, хаотичен, содержит ошибки, возможно неточности и др.

Все скрипты в одном месте (с примерами).

P.S.
Надеюсь все помнят что cmd, в данном случае, надо сохранять используя OEM кодировку?
Об ошибках прошу сообщать для исправления.