Pull to refresh

Samba4 — использование Python Scripting Interface

Reading time17 min
Views19K
Samba4 имеет встроенный интерфейс на Python. Многие утилиты (samba-tool, например) полностью реализованы на Python с применением этого интерфейса.

Все, что делалось из 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()

Соберем основные функции переноса в один файл
lib1.py
#!/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
Детально описано здесь (Method 1: Connecting to AD via Kerberos).
Готовим керберос для 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 не переносились в новый домен.
Tags:
Hubs:
+11
Comments10

Articles