Все, что делалось из LDAP-интерфейса, можно сделать на Samba 4 Python Scripting. Преимущества — файловый доступ, значит высокая скорость, некоторые фичи, которых нет в LDAP. Например, можно взять хэш паролей пользователей из одной базы и перекинуть в другую. Да и самих пользователей с их SID-ами, паролями и всем прочим перекинуть в другой домен (без заморочек с SID-history).
Документации маловато, но есть примеры в каталоге <samba-source>/python/samba, если есть исходники, иначе где-то в /usr/lib/python2.7/dist-packages/samba.
Наибольший интерес представляет файл samdb.py — реализация большинства операций в AD.
Пусть мы имеем установленную Samba4 в конфигурации AD domain controller. Попробуем подключиться к базе AD из Python-программы. Для начала импортируем необходимые библиотеки:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import ldb
from samba.samdb import SamDB
from samba.auth import system_session
from samba.ndr import ndr_pack, ndr_unpack
from samba.dcerpc import security
import samba.param
import base64
import binascii
Подключение к основной базе /sam.ldb:
lp = samba.param.LoadParm()
lp.load(samba.param.default_path()) #или lp.load("/etc/samba/smb.conf")
sam = SamDB(lp=lp,session_info=system_session())
(Возможно подключение и при нестандартном расположении файлов и каталогов инсталляции Samba4 и даже к отдельно стоящей временной базе. Об этом ниже.)
Теперь объект sam позволяет осуществлять поиск и модификацию базы AD в полном соответствии с синтаксисом LDAP.
Например, поиск по базе (base — узел дерева LDAP типа «CN=Users,DC=myDom,DC=lan», expression — необязательное условие отбора, attrs — список желаемых атрибутов):
res = sam.search(base=base, expression=expression, attrs=[*])
Пусть пользователи лежат в ОУ:
base = "OU=myUsers,DC=myDom,DC=lan" # можно не заморачиваться с ОУ и тогда base = "CN=Users,DC=myDom,DC=lan"
Создадим пользователя «tst» c паролем «secret». Класс SamDB имеет готовый метод — newuser(), но можно попробовать и так:
newUsr = "tst"
usrPass = "secret"
ld = {'dn': 'CN=%s,%s' % (newUsr,base),
"sAMAccountName": newUsr,
"userPrincipalName": "%s@%s" % (newUsr,"myDom.lan"),
"objectClass": "user",
"displayName": newUsr,
"description": newUsr,
"homeDirectory": r"\\%s\users\%s" % ("myHost",newUsr),
'scriptPath': "loginScr.cmd",
}
sam.transaction_start()
try:
sam.add(ld)
sam.setpassword("(samAccountName=%s)" % ldb.binary_encode(newUsr), usrPass, False)
except:
sam.transaction_cancel()
print '!!!error'
else:
sam.transaction_commit()
Как видим, SamDB поддерживает транзакции.
Всю базу AD, если она не очень большая, можем посмотреть (и отредактировать) командой:
:~# ldbedit -e nano -H /var/lib/samba/private/sam.ldb
Но лучше ограничивать выборку с помощью опции -s или -b (база), например, -b 'CN=RID Manager$,CN=System,DC=myDom,DC=com'.
Перенос хешей паролей можно сделать по такой схеме:
Пусть у нас есть старая база AD — тоже на Samba4. Можно получить реплику базы из Win AD, подключив новую инсталляцию Samba4 в качестве дополнительного AD DC — хорошо документированная и простая процедура — см. здесь.
Скопируем и подключимся к ней — назовем соединение sam0. Подключение с нестандартными путями (пусть скопирована в /tmp/priv и там же его smb.conf):
lp0 = samba.param.LoadParm()
lp0.load('/tmp/priv/smb.conf')
lp0.set('private directory','/tmp/priv')
sam0 = SamDB(lp=lp0,session_info=system_session())
Чтобы получить весь список пользователей, да еще с паролями, сделаем такой запрос:
res = sam0.search(base="DC=oldDom,DC=myDom,DC=ru",expression="(&(objectCategory=person)(objectClass=user))", attrs=['*','unicodePwd'])
Будем перебирать базу пользователей и добавлять их в новую базу. Схематически это выгляди так:
for r in res:
dn = str(r.dn)# это старый DN пользователя, его нужно поменять чтобы соотв. новому домену!
-------
sd = ndr_unpack(security.dom_sid,r['objectSid'][0])# это SID пользователя, можем оставить его прежним
(dom_sid, rid) = sd.split()# так SID разделяется на SID домена и RID
-------
#.... после необходимых преобразований добавляем пользователя в новую базу - sam.add(ld). (Работающий пример - ниже)
#теперь трюк для переноса пароля:
setpw = """
dn: %s
changetype: modify
replace: unicodePwd
unicodePwd:: %s
""" % (dn, base64.b64encode(str(r['unicodePwd'])))
sam.transaction_start()
try:
sam.modify_ldif(setpw,["local_oid:1.3.6.1.4.1.7165.4.3.12:0"])
except:
sam.transaction_cancel()
print( '!!! ERROR SET PASSWORD USER : %s' % r['sAMAccountName'])
else:
sam.transaction_commit()
Теперь реальный пример переноса пользователей из домена под Win 2003 в Samba4.
В старом домене накопились проблемы (начиная даже с неправильного имени домена). Нормальная репликация с DC на Samba4 (в обратную сторону — Samba4 DC -> W2003 DC) никак не завелась, вероятно из-за внутридоменных проблем.
Задача отягощалась наличием файлового сервера на Samba3, поэтому надо было сохранить мапинг sAMAccountName <-> (UID,GID), уже существующий в Samba3 (обычно /var/lib/samba/winbindd_idmap.tdb). Собственно задача была похожа на описанную здесь.
Все эксперименты и конечный вариант делались на серверах Ubuntu 14.04, запущенных в контейнерах OpenVZ (CentOS 6)
Установка, настройка Samba4 описана много раз. Например, уже упомянуто, здесь. Для нормального отображения Unix ID в схеме с rfc2307 использовался sssd. Кстати, сборку Samba4 от sernet, которую многие рекомендуют, лучше не использовать — ее трудно подружить с пакетом sssd.
Чтобы сохранить пароли и SID пользователей, надо иметь старую базу AD уже в виде private directory Samba4, как уже говорилось выше. Пропуская подробности (см. здесь), — «samba-tool domain join samdom.example.com DC -Uadministrator --realm=samdom.example.com» — можно на этом и остановиться, не запуская сервис samba, поскольку необходимая база уже создана. Если же надо будет далее актуализировать базу, то без запуска сервиса samba не обойтись.
Воздействие на существующий Win AD домен минимально (создается еще один контроллер, почти неработающий, поэтому в логах будет много ошибок NTDS Replication), после создания автономой базы AD можно без риска поупражняться в виртуальных средах. Если MS Win домен должен еще какое-то время нормально работать, лучше эту временную Samba4 убить и вычистить информацию об этом DC из работающих DC.
Полученную private directory (обычно /var/lib/samba/private или /usr/local/samba/private) надо скопировать куда-нибудь на будущий Samba4 и туда же скопировать smb.conf из /etc/samba. Теперь все данные о старом домене хранятся в одном месте и к тому же доступны в локальной ФС.
Если есть еще файл-сервер на Samba3, то где-нибудь рядом надо также положить и каталог /var/lib/samba от Samba3 (там нужны 2 файла — winbindd_idmap.tdb и group_mapping.tdb), если мы хотим сохранить сложившийся в Samba3 idmap.
Исходные параметры оформляем в виде файла conf.py:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
smb_conf = '/etc/samba/smb.conf'
smb_priv = '/var/lib/samba/private'
dom0 = 'olddom.mydom.ru' # название старого домена
dom1 = 'newdom.lan' # название нового домена. (!)точек не больше, чем в старом - для определенности и простоты преобразования
host = 'newdc' # сетевое имя хоста
maildom = 'mydom.ru' #почтовый домен. необязательно
#homeDirectory, homeDrive = r'\\newdc\Users','Z:' # необязательно
smb_priv0 = '/var/lib/samba/private-0' #private directory старого домена на samba4 (и там же его smb.conf!)
smb3db = '/var/lib/samba/samba3' #если был файл-сервер на самба3 (обычно /var/lib/samba (скопировать)), иначе None
start_unix_id = 50000 # начало нумерации GID и UID для новых пользователей.
############ ниже лучше не трогать
d0, d1 = dom0.split('.'), dom1.split('.')
base0 = ','.join(['DC='+x for x in d0])
base, dom, realm = ','.join(['DC='+x for x in d1]), d1[0].upper(), '.'.join(d1).upper()
Соберем основные функции переноса в один файл
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import string
import ldb
from struct import unpack
#from samba.idmap import IDmapDB
from samba.samdb import SamDB
from samba.auth import system_session
from samba.ndr import ndr_unpack
from samba.dcerpc import security
import samba.param
import base64
from conf import *
def get_1out(cmd): #возвращает 1 строку вывода внешней команды
import subprocess
return subprocess.Popen(cmd, stdout=subprocess.PIPE).stdout.read().splitlines()[0]
if not 'host' in globals(): host = get_1out('hostname').upper()
if not 'smb3db' in globals(): smb3db = None
my_log = lambda *x: None
chgDc = lambda x: x
def set_my_log(): #вывод сообщений в лог-файл. Имя файла соотв. времени создания
from datetime import datetime
global my_log
f_log = open(datetime.strftime(datetime.now(), "log_%y-%m-%d_%H:%M:%S.txt"),'w')
def my_log(*x):
try:
xx = ' '.join(x)
except:
xx = ' '.join(map(lambda a: str(a),x))
f_log.write(xx+'\n')
print xx
def mk_chg(d0=d0,d1=d1): #формирователь смены имен. d0,d1 списки элементов старого и нового домена. !!!: len(d0) >= len(d1)
import re
global chgDc
if d0 == d1: return
dif = len(d0)-len(d1)+1 #если в новом домене меньше элементов имени,
ddx = [d0[0],] + d0[dif:] #получаем список с равным числом элементов, иначе ddx == d0
#1 удалить лишнее, 2 заменить маленькие буквы, 3 .большие. :
myRe = [('',re.compile(r'\b(DC=)?%s\b[,.]?' % x, re.I)) for x in d0[1:dif]] + [(b,re.compile(r'\b%s\b' % a)) for (a,b) in zip(ddx,d1)] + [(b.upper(),re.compile(r'\b%s\b' % a.upper(), re.I)) for (a,b) in zip(ddx,d1)]
def chg(s): #заменяет старое на новое d0 -> d1
for r in myRe: s = r[1].sub(r[0],s)
return s
chgDc = chg
return chg
def mk_sam0(smb_priv0=smb_priv0): #подключение к базе старого домена. smb_priv0 - private directory старого домена (и там же его smb.conf!)
import os
global sam0
lp0 = samba.param.LoadParm()
lp0.load(smb_priv0+'/smb.conf') # smb.conf надо обязательно скопировать в private directory!!!
lp0.set('private directory',smb_priv0)
sam0 = SamDB(lp=lp0,session_info=system_session())
os.unsetenv('SMB_CONF_PATH')
return sam0
def mk_sam(host=host): #подключение к базе нового домена и нахождение нужных параметров
global sam, ridSet, ridMan, sfu, idmap, minRid, nis
lp = samba.param.LoadParm()
lp.load(smb_conf)
lp.set('private directory',smb_priv)
sam = SamDB(lp=lp,session_info=system_session())
# idmap = IDmapDB(lp=lp)
sfu = "CN=%s,CN=ypservers,CN=ypServ30,CN=RpcServices,CN=System,%s" % (dom,base) # dn для sfu (msSFU30OrderNumber...)
ridSet = "CN=RID Set,CN=%s,OU=Domain Controllers,%s" % (host, base) # dn для rIDNextRID
ridMan = "CN=RID Manager$,CN=System,"+base # dn для RID Manager
if not 'minRid' in globals():
minRid = int(sam.search(base = ridSet, attrs=["rIDNextRID"])[0]["rIDNextRID"][0])
my_log('\tminRid=%s' % str(minRid))
if not 'nis' in globals():
nis = str(sam.search(base=sfu, attrs=['msSFU30Domains'])[0]['msSFU30Domains'][0])
sam.nis = nis
return sam
def get_map0(i=None, path=smb3db, maps = {}):
# соответствие sAMAccountName <-> UID из файлов idmap smb3 - словарь maps
if not maps:
if path != None:
from samba.samba3 import DbDatabase
mapdb = DbDatabase(path+'/group_mapping')
for x in mapdb.db.iterkeys():
if x.startswith('UNIXGROUP') :
y = mapdb.db.get(x)
maps[y[8:-2]] = ('GID',unpack('<L',y[0:4])[0])
mapdb.close()
mapdb = DbDatabase(path+'/winbindd_idmap')
for x in mapdb.db.iterkeys():
if x.startswith('S-1'):
y = mapdb.db.get(x)
res = sam0.search(base=base0,expression="(objectSid=%s)" % x.rstrip("\x00"),
scope=ldb.SCOPE_SUBTREE, attrs=["sAMAccountName"])
if len(res) > 0 and 'sAMAccountName' in res[0]:
maps[str(res[0]["sAMAccountName"][0])] = y.rstrip("\x00").split(" ")
mapdb.close()
else: # если не задана база idmap, все равно надо задать или определить ID 'Domain Users'
my_log('!? Not MAP0')
maps['Domain Users'] = ['GID',start_unix_id]
if 'sam' in globals():
res = sam.search(base=base,expression="(sAMAccountName=Domain Users)",attrs=['gidNumber'])
if len(res) > 0 and 'gidNumber' in res[0]:
maps['Domain Users'][1] = int(res[0]['gidNumber'][0])
maps['_users'] = maps['Domain Users'][1] # спец. группа для всех
my_log('Set Domain Users = %d' % maps['_users'])
maps['Administrator'] = ('UID',0)
maps['Administrators'] = ('GID',0)
if i == None: return maps
return maps[i] if i in maps else False
def mk_fill_matrix(m,r): # helper для заполнения словаря m в cp_usr() и cp_grp()
def rp(k,chg=0): # если chg!=0 - используется преобразование имен!
if k in r:
m[k] = str(r[k][0]) if chg==0 else chgDc(str(r[k][0]))
return rp
def mk_fill_ldb_msg(dn): # helper для создания и заполнения объекта ldb.Message m2
m2 = ldb.Message()
m2.dn = ldb.Dn(sam, str(dn))
def rp(fld=None,val='',flg=ldb.FLAG_MOD_REPLACE):
if fld:
m2[fld] = ldb.MessageElement(str(val), flg, fld)
return m2
return rp
def usn_sort(res): # сортировка объектов выборки res в порядке создания - важно для вложенных объектов (ou, grp)
x = [r for r in res]
x.sort(key = lambda r: int(r["uSNCreated"][0]))
return x
def rid_sort(res): # сортировка объектов выборки res в порядке RID
x = [r for r in res]
x.sort(key = lambda r: int(unpack('<I',r['objectSid'][0][-4:])[0]))
return x
def set_grp_gid(r,gid): #установить Unix GID для группы
rp = mk_fill_ldb_msg(r.dn)
rp("msSFU30NisDomain",nis)
rp("msSFU30Name",r['sAMAccountName'][0])
rp("gidNumber",gid)
sam.modify(rp())
def set_usr_gid_uid(r,uid): #установить Unix GID, UID для пользователя
rp = mk_fill_ldb_msg(r.dn)
rp("msSFU30NisDomain",nis)
rp("uid",r['sAMAccountName'][0])
rp("uidNumber",uid)
rp("gidNumber",get_map0('_users'))
# rp('objectClass','posixAccount',ldb.FLAG_MOD_ADD)
sam.modify(rp())
def map_grp(): # ставим соотв GID из IdMap базы Samba 3
res = sam.search(base=base,expression="(objectClass=group)")
my_log( "\tmap_grp ALL GRP COUNT: %s" % len(res))
sam.transaction_start()
try:
for r in res:
x = get_map0(str(r['sAMAccountName'][0]))
if x:
set_grp_gid(r,x[1])
except:
sam.transaction_cancel()
my_log( '!!! ERROR MAP GRP %s' % r['sAMAccountName'])
else:
sam.transaction_commit()
def map_usr(): # ставим соотв UID из IdMap базы Samba 3
res = sam.search(base=base,expression="(&(objectCategory=person)(objectClass=user))",attrs=["sAMAccountName"])
my_log( "\tmap_usr ALL USR COUNT: %s" % len(res))
sam.transaction_start()
try:
for r in res:
x = get_map0(str(r['sAMAccountName'][0]))
if x:
set_usr_gid_uid(r,x[1])
except:
sam.transaction_cancel()
my_log( '!!! ERROR SET SFU30 ATTR. USER %s !!!' % x[1])
else:
sam.transaction_commit()
def cp_ou(): # перенос organizationalUnit
ous = [str(r.dn) for r in sam.search(base=base,expression="(objectClass=organizationalUnit)", attrs=[])]
res = sam0.search(base=base0,expression="(objectClass=organizationalUnit)")
my_log( "\tOU COUNT: %s" % len(res))
for r in usn_sort(res):
dn = chgDc(str(r.dn))
if not dn in ous:
m = {"dn": dn, "objectClass": "organizationalUnit", "name": str(r["name"][0])}
try: sam.add(m)
except: my_log("!!!Error Add OU : %s" % dn)
def cp_usr(): # перенос пользователей
users = [str(r.dn) for r in sam.search(base=base,expression="(&(objectCategory=person)(objectClass=user))", attrs=[])]
res = sam0.search(base=base0,expression="(&(objectCategory=person)(objectClass=user))", attrs=['*','unicodePwd'])
my_log( "\tNEW USR COUNT: %s" % len(res))
for r in res:
dn = chgDc(str(r.dn))
if dn in users: continue
sd = ndr_unpack(security.dom_sid,r['objectSid'][0])
(group_dom_sid, rid) = sd.split()
if rid <= minRid:
my_log( '!! MinRid ERR : ', r['sAMAccountName'][0] )
continue
m = {"dn": dn, "objectClass": "user"}
rp = mk_fill_matrix(m,r)
rp('userPrincipalName',1)
rp('sAMAccountName')
rp('sn')
rp('name')
rp('initials')
rp('displayName')
rp('scriptPath')
rp('description')
rp('userAccountControl')
rp('pwdLastSet')
m["nTSecurityDescriptor"] = r['objectSid']
if 'maildom' in globals():
m["mail"] = "%s@%s" % (r['sAMAccountName'][0], maildom)
if 'homeDirectory' in globals():
m["homeDirectory"] = r"%s\%s" % (homeDirectory,r['sAMAccountName'][0])
if 'homeDrive' in globals():
m["homeDrive"] = homeDrive
sam.transaction_start()
try:
sam.add(m)
except:
sam.transaction_cancel()
my_log( '!!! ERROR ADD USER : %s' % m['sAMAccountName'])
else:
sam.transaction_commit()
# Copy the password for it
if not 'unicodePwd' in r:
my_log( '!!! NOT PASSWD FOR USER : %s' % m['sAMAccountName'])
continue
setpw = """
dn: %s
changetype: modify
replace: unicodePwd
unicodePwd:: %s
""" % (dn, base64.b64encode(str(r['unicodePwd'])))
sam.transaction_start()
try:
sam.modify_ldif(setpw,["local_oid:1.3.6.1.4.1.7165.4.3.12:0"])
except:
sam.transaction_cancel()
my_log( '!!! ERROR SET PASSWORD USER : %s' % m['sAMAccountName'])
else:
sam.transaction_commit()
def cp_grp(): # перенос групп
grps = [str(r.dn) for r in sam.search(base=base,expression="(objectClass=group)", attrs=[])]
res = sam0.search(base=base0,expression="(&(objectClass=group)(objectCategory=Group))")
my_log( "\tNEW GRP COUNT: %s" % len(res))
for r in usn_sort(res):
dn = chgDc(str(r.dn))
if dn in grps: continue
sd = ndr_unpack(security.dom_sid,r['objectSid'][0])
(group_dom_sid, rid) = sd.split()
if rid <= minRid:
my_log( '!! MinRid ERR : ', r['name'][0] )
continue
m = {"dn": dn, "objectClass": "group"}
rp = mk_fill_matrix(m,r)
rp('sAMAccountName')
rp('groupType')
rp('description')
m["nTSecurityDescriptor"] = r['objectSid']
sam.transaction_start()
try:
sam.add(m)
except:
sam.transaction_cancel()
my_log( '!!! ERROR add GRP %s !!!' % m['sAMAccountName'])
else:
sam.transaction_commit()
def grp_fill(): #заполнить группы членами
my_log( "\tgrp_fill")
grps ={}
for r in sam.search(base=base,expression="(&(objectClass=group)(objectCategory=Group))",attrs=['member']):
grps[str(r.dn)] = r['member'] if 'member' in r else []
users = [str(r.dn) for r in sam.search(base=base,expression="(&(objectCategory=person)(objectClass=user))", attrs=[])]
for r in sam0.search(base=base0,expression="(&(objectClass=group)(objectCategory=Group))",attrs=['member']):
if not 'member' in r: continue
grp = chgDc(str(r.dn))
if not grps.has_key(grp):
my_log( "!!not found group:\t",grp)
continue
add_m = ''
for m in r['member']:
m = chgDc(m)
if m in grps[grp]: continue
if not m in users:
try: sam.search(base=m, attrs=[])
except:
my_log( "!? err (not found) add %s \tto %s" % (m,grp))
continue
add_m += "add: member\nmember: %s\n" % (m)
if add_m == '': continue
add_m = "\ndn: %s\nchangetype: modify\n%s\n" % (grp,add_m)
sam.transaction_start()
try:
sam.modify_ldif(add_m)
except:
sam.transaction_cancel()
my_log( "!!!Error fill grp "+grp)
else:
sam.transaction_commit()
def set_max_gid_uid(max_gid=start_unix_id, max_uid=start_unix_id): # установить max GID, UID в системе
my_log('set_max_gid_uid start: set max_gid=%s, max_uid=%s' % (max_gid, max_uid))
chg = ''
r = sam.search(base=sfu)[0]
for x in (['msSFU30MaxGidNumber',max_gid],['msSFU30MaxUidNumber',max_uid]):
x[1] = max(x[1],start_unix_id)
if not x[0] in r or x[1] > int(r[x[0]][0]):
chg += "replace: %s\n%s: %d\n" % (x[0],x[0],x[1])
if chg != '':
chg = "\ndn: %s\nchangetype: modify\n%s\n" % (sfu,chg)
sam.transaction_start()
try:
sam.modify_ldif(chg)
except:
sam.transaction_cancel()
my_log( "!!!Error set msSFU30Max...")
else:
sam.transaction_commit()
def get_next_uid(): # забрать следующий UID в системе
nm = 'msSFU30MaxUidNumber'
r = sam.search(base=sfu)[0]
x = start_unix_id
if nm in r:
x = max(x, int(r[nm][0]))
sam.transaction_start()
try:
sam.modify_ldif("\ndn: %s\nchangetype: modify\nreplace: %s\n%s: %d\n" % (sfu,nm,nm,x+1))
except:
sam.transaction_cancel()
my_log( "!!!Error set msSFU30Max...")
raise
else:
sam.transaction_commit()
return x
def set_max_id(): #найти max GID, UID, использованные в системе, и установить +1
max_gid = max([int(r['gidNumber'][0]) for r in sam.search(base=base,expression="(&(objectClass=group)(gidNumber=*))",attrs=['gidNumber'])]+[0])
max_uid = max([int(r['uidNumber'][0]) for r in sam.search(base=base,expression="(&(objectCategory=person)(objectClass=user)(uidNumber=*))",attrs=['uidNumber'])]+[0])
set_max_gid_uid(max_gid=max_gid+1,max_uid=max_uid+1)
def check_id(): #проверить отсутств. GID, UID и заполнить
r = sam.search(base=sfu)[0]
(max_gid,max_uid) = [int(r[x][0]) if x in r else start_unix_id for x in ('msSFU30MaxGidNumber','msSFU30MaxUidNumber')]
my_log('check_id start: initial max_gid=%d, max_uid=%d' % (max_gid, max_uid))
sam.transaction_start()
try:
for r in rid_sort(sam.search(base=base,expression="(&(objectClass=group)(!(gidNumber=*)))",attrs=['sAMAccountName','objectSid'])):
set_grp_gid(r,max_gid)
max_gid += 1
for r in usn_sort(sam.search(base=base,expression="(&(objectCategory=person)(objectClass=user)(!(uidNumber=*)))",attrs=['sAMAccountName','uSNCreated'])):
set_usr_gid_uid(r,max_uid)
max_uid += 1
except:
sam.transaction_cancel()
my_log( '!!! ERROR check_id %s' % r['sAMAccountName'])
else:
sam.transaction_commit()
set_max_gid_uid(max_gid=max_gid,max_uid=max_uid)
def set_max_rid(): #найти max RID в системе, установить корректные "RID Set" и "RID Manager"
res = sam.search(base=base,expression="(&(sAMAccountName=*)(objectSid=*))",attrs=["objectSid"])
my_log( "\tSID COUNT: %s" % len(res))
x = max([int(unpack('<I',r['objectSid'][0][-4:])[0]) for r in res])
rmin = (x-100)/500*500+100
pool = rmin + ((rmin + 499) << 32)
my_log("\tRid Set: %d %d %d " % (rmin,x,rmin + 499))
m = mk_fill_ldb_msg(ridSet)
m('rIDNextRID',x)
m('rIDAllocationPool',pool)
m('rIDPreviousAllocationPool',pool)
m2 = mk_fill_ldb_msg(ridMan)
m2('rIDAvailablePool',rmin + 500 + (1073741823 << 32))
sam.transaction_start()
try:
sam.modify(m())
sam.modify(m2())
except:
sam.transaction_cancel()
my_log( '!!! ERROR Set rIDNextRID %s' % x)
else:
sam.transaction_commit()
return x
def mk_dom(): # Инициализация нового домена на samba 4 с прежним SID, сброшенной проверкой сложности пароля и паролем administrator = "1" !!!
from samba.netcmd.main import cmd_sambatool
def cmd(args, subcom='domain'):
cmd = cmd_sambatool()
try:
retval = cmd._run("samba-tool", subcom, *args)
except SystemExit, e:
retval = e.code
except Exception, e:
cmd.show_command_error(e)
retval = 1
if retval: sys.exit(retval)
cmd(('provision',
'--host-name=%s' % host,
'--realm=%s' % realm,
'--domain=%s' % dom,
'--domain-sid=%s' % get_1out('./get_dom_sid.py'),
'--adminpass=UJHkjhm7KH$$2vrXy',
'--function-level=2003',
'--server-role=dc',
'--use-rfc2307',
'--dns-backend=SAMBA_INTERNAL'))
cmd(('passwordsettings', 'set', '--complexity=off', '--history-length=0', '--min-pwd-length=0', '--min-pwd-age=0', '--max-pwd-age=0'))
cmd(('setpassword', 'administrator', '--newpassword=1'),subcom='user') # сброс пароля на 1. Не забыть потом установить нормальный!!
Здесь присутствует вызов get_dom_sid.py как внешней программы в функции mk_dom() — инициализация нового домена.
get_dom_sid.py просто печатает SID старого домена:
#!/usr/bin/env python
from lib1 import mk_sam0
print mk_sam0().domain_sid
Так пришлось сделать, поскольку при подключении к базе старого домена в том же потоке, что и создание нового происходило замещение переменных окружения старыми данными.
Итак, после того, как установлены все необходимые пакеты (samba4, sssd и зависимости), скопированы в нужные места каталоги старых баз, можем начать создание нового домена.
Инициализация домена — запуск mk_dom.py:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from lib1 import *
set_my_log()
mk_dom()
Если все прошло нормально (лог-файл имеет имя log_%y-%m-%d_%H:%M:%S.txt) смотрим smb.conf, временно добавляем в секции [global]
dns forwarder = <адрес старого DC> (на время перевода рабочих станций в новый домен).
Копируем krb5.conf из /var/lib/samba/private в /etc (или делаем символический линк). Далее запускаем скрипт копирования объектов старого домена cp_dom.py:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from lib1 import *
set_my_log()
my_log(base0,' ->',base, dom, realm)
mk_chg() #создать функцию преобразования имен
mk_sam0() #подключение к старой базе
mk_sam() #подключение к новой базе
cp_ou() #копирование organizationalUnit
cp_grp() #копирование групп
cp_usr() #копирование пользователей
grp_fill() #заполнение групп членами
get_map0() #заполнить матрицу соответствия sAMAccountName <-> UID (с учетом наличия idmap Samba3)
map_grp() #заполнить unix GID для групп
map_usr() #заполнить unix GID, UID для пользователей
set_max_rid() #найти max RID в системе, установить корректные "RID Set" и "RID Manager"
set_max_id() #найти max GID, UID, использованные в системе, и установить +1 для следующих
check_id() #проверить отсутствующие GID, UID и заполнить
В лог файл неизбежно валятся ошибки. Наиболее серьезные — с тремя знаками "!" впереди. Типа:
!!! ERROR ADD USER: 5CA6ADDF-A2C8-46E5-A
!!! ERROR SET PASSWORD USER: 5CA6ADDF-A2C8-46E5-A
Чаще всего это объекты, отсутствующие в текущей схеме и по сути ненужные. Если в домене были пользователи из другого домена леса, их добавление тоже не получится и попадет в лог. Несущественны ошибки типа !! MinRid ERR: Пользователи DCOM
Если все терпимо, стартуем Samba:
start samba-ad-dc
Готовим керберос для sssd:
samba-tool domain exportkeytab /etc/krb5.sssd.keytab --principal=<myHostName>$
chown root:root /etc/krb5.sssd.keytab
chmod 600 /etc/krb5.sssd.keytab
Файл /etc/sssd/sssd.conf:
[sssd]
services = nss, pam
config_file_version = 2
domains = newdom.lan
[nss]
[pam]
[domain/newdom.lan]
id_provider = ad
auth_provider = ad
ldap_schema = ad
krb5_keytab = /etc/krb5.sssd.keytab
access_provider = ad
ldap_id_mapping=false
enumerate = true
Сброс кэш sssd:
sss_cache -GU
Перезапуск sssd:
restart sssd
Кстати, сброс кэш sssd может не помочь при больших изменениях AD. Тогда надо, при остановленном sssd, удалить каталог из /var/lib/sss/ и восстановить их пустую структуру (из установочного пакета).
Проверяем отображение пользователей и групп (база sssd заполняется некоторое время):
getent passwd
getent group
Перетащить пользователей проще всего утилитой netdom.exe (netdom.exe move /?), добавив ее в логон-скрипт на старом сервере. Только надо запускать подходящую для ОС версию netdom.exe. Поскольку SID, GID, UID и пароли пользователей сохраняются, то перемещение почти прозрачно для пользователей — локальные папки остаются с ними, сетевые ресурсы тоже. Надо только переименовать домен в конфигурации файлового сервера Samba.
У меня было еще проще — поскольку весь это зоопарк жил под OpenVZ, сетевой ресурс на отдельной ФС легко монтировался к разным файловым серверам одновременно (можно и DC сделать файловым сервером — нормально), а проблемы доступа автоматически решались соответствием GID и UID.
Объекты Group Policy не переносились в новый домен.
Комментарии (10)
Deepwalker
28.05.2015 21:00Черт, доктор, мы его теряем. Вначале пользователей из самбы копирует, а потом начнет сайты писать, и мир потеряет еще одного админа :)
rrrav Автор
28.05.2015 21:31+1Отряд не заметит потери бойца… А что касается веб-программирования — уже пришлось написать сайтик для администрирования юзеров самбы ограниченным персоналом (Апач + пхп + CGI на том же Питоне)
xlin
29.05.2015 10:52Было бы интересно почитать как переехать со старой Samba 4 beta на нормальную (актуальную) samba 4. А то у меня на Ubuntu как завелась она в тестовом режиме, так и прижилась. Трогать страшно, но хочется все перекинуть на CentOS. Пользователей около 150 с перемещаемыми профилями.
Может кто делал?rrrav Автор
29.05.2015 11:08А что мешает попробовать на тестовой площадке? Сделать полную исходную копию и смоделировать переход. Благо дело теперь нет проблем с виртуальными средами. Вот здесь немного про апгрейд wiki.samba.org/index.php/Updating_Samba#On_Samba_Active_Directory_DCs
xlin
29.05.2015 11:13Ничего не мешает. Контроллер крутится на цитриксе, можно и копию сделать на поиграться. Проблема лишь в том, где почитать как это сделать. Т.е. перенести самбу тем же таром и развернуть на новой ос. Потестить и потом потушить старый.
rrrav Автор
29.05.2015 11:13Можно еще попробовать подключить новую Samba4 в качестве дополнительного AD DC, если получится — перенести на нее все роли FSMO и затем удалить старый DC.
xlin
29.05.2015 11:16Как это сделать где то написано? Я самбу поднял года 3 назад и более ее не трогал. Работает пока без проблем. Правда в идеале вынашиваю идею поднять актуальную самбу на CentOS и каким то макаром сделать еще и дополнительный AD DC.
rrrav Автор
29.05.2015 11:29+1В статье была ссылка wiki.samba.org/index.php/Join_a_domain_as_a_DC. Достаточно детальное описание, как подключиться к существующему домену в качестве DC.
rrrav Автор
29.05.2015 11:49Ну и не забудьте про перенос SysVol
rsync -XAavz --delete-after /var/lib/samba/sysvol NewSambaDC:/var/lib/samba
Ну и если не использовали rfc2307 + sssd скопируйте idmap.ldb на новый сервер, иначе будут проблемы с правами пользователей.
Meklon
Прекрасная статья, спасибо.