s3upgrade: Add idmap migration, users/groups import
[Samba.git] / source4 / scripting / python / samba / upgrade.py
blob38e6ed87bb019f54d1b139ad2ce16d8aad43b3dc
1 # backend code for upgrading from Samba3
2 # Copyright Jelmer Vernooij 2005-2007
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 """Support code for upgrading from Samba 3 to Samba 4."""
20 __docformat__ = "restructuredText"
22 import grp
23 import ldb
24 import time
25 import pwd
27 from samba import Ldb, registry
28 from samba.param import LoadParm
29 from samba.provision import provision, FILL_FULL
30 from samba.samba3 import passdb
31 from samba.samba3 import param as s3param
32 from samba.dcerpc import lsa
33 from samba.dcerpc.security import dom_sid
34 from samba import dsdb
35 from samba.ndr import ndr_pack
38 def import_sam_policy(samldb, policy, dn):
39 """Import a Samba 3 policy database."""
40 samldb.modify_ldif("""
41 dn: %s
42 changetype: modify
43 replace: minPwdLength
44 minPwdLength: %d
45 pwdHistoryLength: %d
46 minPwdAge: %d
47 maxPwdAge: %d
48 lockoutDuration: %d
49 samba3ResetCountMinutes: %d
50 samba3UserMustLogonToChangePassword: %d
51 samba3BadLockoutMinutes: %d
52 samba3DisconnectTime: %d
54 """ % (dn, policy.min_password_length,
55 policy.password_history, policy.minimum_password_age,
56 policy.maximum_password_age, policy.lockout_duration,
57 policy.reset_count_minutes, policy.user_must_logon_to_change_password,
58 policy.bad_lockout_minutes, policy.disconnect_time))
61 def import_sam_account(samldb,acc,domaindn,domainsid):
62 """Import a Samba 3 SAM account.
64 :param samldb: Samba 4 SAM Database handle
65 :param acc: Samba 3 account
66 :param domaindn: Domain DN
67 :param domainsid: Domain SID."""
68 if acc.nt_username is None or acc.nt_username == "":
69 acc.nt_username = acc.username
71 if acc.fullname is None:
72 try:
73 acc.fullname = pwd.getpwnam(acc.username)[4].split(",")[0]
74 except KeyError:
75 pass
77 if acc.fullname is None:
78 acc.fullname = acc.username
80 assert acc.fullname is not None
81 assert acc.nt_username is not None
83 samldb.add({
84 "dn": "cn=%s,%s" % (acc.fullname, domaindn),
85 "objectClass": ["top", "user"],
86 "lastLogon": str(acc.logon_time),
87 "lastLogoff": str(acc.logoff_time),
88 "unixName": acc.username,
89 "sAMAccountName": acc.nt_username,
90 "cn": acc.nt_username,
91 "description": acc.acct_desc,
92 "primaryGroupID": str(acc.group_rid),
93 "badPwdcount": str(acc.bad_password_count),
94 "logonCount": str(acc.logon_count),
95 "samba3Domain": acc.domain,
96 "samba3DirDrive": acc.dir_drive,
97 "samba3MungedDial": acc.munged_dial,
98 "samba3Homedir": acc.homedir,
99 "samba3LogonScript": acc.logon_script,
100 "samba3ProfilePath": acc.profile_path,
101 "samba3Workstations": acc.workstations,
102 "samba3KickOffTime": str(acc.kickoff_time),
103 "samba3BadPwdTime": str(acc.bad_password_time),
104 "samba3PassLastSetTime": str(acc.pass_last_set_time),
105 "samba3PassCanChangeTime": str(acc.pass_can_change_time),
106 "samba3PassMustChangeTime": str(acc.pass_must_change_time),
107 "objectSid": "%s-%d" % (domainsid, acc.user_rid),
108 "lmPwdHash:": acc.lm_password,
109 "ntPwdHash:": acc.nt_password,
113 def import_sam_group(samldb, sid, gid, sid_name_use, nt_name, comment, domaindn):
114 """Upgrade a SAM group.
116 :param samldb: SAM database.
117 :param gid: Group GID
118 :param sid_name_use: SID name use
119 :param nt_name: NT Group Name
120 :param comment: NT Group Comment
121 :param domaindn: Domain DN
124 if sid_name_use == 5: # Well-known group
125 return None
127 if nt_name in ("Domain Guests", "Domain Users", "Domain Admins"):
128 return None
130 if gid == -1:
131 gr = grp.getgrnam(nt_name)
132 else:
133 gr = grp.getgrgid(gid)
135 if gr is None:
136 unixname = "UNKNOWN"
137 else:
138 unixname = gr.gr_name
140 assert unixname is not None
142 samldb.add({
143 "dn": "cn=%s,%s" % (nt_name, domaindn),
144 "objectClass": ["top", "group"],
145 "description": comment,
146 "cn": nt_name,
147 "objectSid": sid,
148 "unixName": unixname,
149 "samba3SidNameUse": str(sid_name_use)
153 def add_idmap_entry(idmapdb, sid, xid, xid_type, logger):
154 """Create idmap entry"""
156 # First try to see if we already have this entry
157 found = False
158 try:
159 msg = idmapdb.search(expression='objectSid=%s' % str(sid))
160 if msg.count == 1:
161 found = True
162 except Exception, e:
163 raise e
165 if found:
166 print msg.count
167 print dir(msg)
168 try:
169 m = ldb.Message()
170 m.dn = ldb.Dn(idmapdb, msg[0]['dn'])
171 m['xidNumber'] = ldb.MessageElement(str(xid), ldb.FLAG_MOD_REPLACE, 'xidNumber')
172 m['type'] = ldb.MessageElement(xid_type, ldb.FLAG_MOD_REPLACE, 'type')
173 idmapdb.modify(m)
174 except ldb.LdbError, e:
175 logger.warn('Could not modify idmap entry for sid=%s, id=%s, type=%s (%s)',
176 str(sid), str(xid), xid_type, str(e))
177 except Exception, e:
178 raise e
179 else:
180 try:
181 idmapdb.add({"dn": "CN=%s" % str(sid),
182 "cn": str(sid),
183 "objectClass": "sidMap",
184 "objectSid": ndr_pack(sid),
185 "type": xid_type,
186 "xidNumber": str(xid)})
187 except ldb.LdbError, e:
188 logger.warn('Could not add idmap entry for sid=%s, id=%s, type=%s (%s)',
189 str(sid), str(xid), xid_type, str(e))
190 except Exception, e:
191 raise e
194 def import_idmap(idmapdb, samba3_idmap, logger):
195 """Import idmap data.
197 :param samba3_idmap: Samba 3 IDMAP database to import from
200 currentxid = max(samba3_idmap.get_user_hwm(), samba3_idmap.get_group_hwm())
201 lowerbound = currentxid
202 # FIXME: upperbound
204 m = ldb.Message()
205 m.dn = ldb.Dn(idmapdb, 'CN=CONFIG')
206 m['lowerbound'] = ldb.MessageElement(str(lowerbound), ldb.FLAG_MOD_REPLACE, 'lowerBound')
207 m['xidNumber'] = ldb.MessageElement(str(currentxid), ldb.FLAG_MOD_REPLACE, 'xidNumber')
208 idmapdb.modify(m)
210 for id_type, xid in samba3_idmap.ids():
211 if id_type == 'UID':
212 xid_type = 'ID_TYPE_UID'
213 elif id_type == 'GID':
214 xid_type = 'ID_TYPE_GID'
215 else:
216 logger.warn('Wrong type of entry in idmap (%s), Ignoring', id_type)
217 continue
219 sid = samba3_idmap.get_sid(xid, id_type)
220 add_idmap_entry(idmapdb, dom_sid(sid), xid, xid_type, logger)
223 def add_group_from_mapping_entry(samdb, groupmap, logger):
224 """Add or modify group from group mapping entry"""
226 # First try to see if we already have this entry
227 try:
228 msg = samdb.search(base='<SID=%s>' % str(groupmap.sid), scope=ldb.SCOPE_BASE)
229 found = True
230 except ldb.LdbError, (ecode, emsg):
231 if ecode == ldb.ERR_NO_SUCH_OBJECT:
232 found = False
233 else:
234 raise ldb.LdbError(ecode, emsg)
235 except Exception, e:
236 raise e
238 if found:
239 logger.warn('Group already exists sid=%s, groupname=%s existing_groupname=%s, Ignoring.',
240 str(groupmap.sid), groupmap.nt_name, msg[0]['sAMAccountName'][0])
241 else:
242 if groupmap.sid_name_use == lsa.SID_NAME_WKN_GRP:
243 return
245 m = ldb.Message()
246 m.dn = ldb.Dn(samdb, "CN=%s,CN=Users,%s" % (groupmap.nt_name, samdb.get_default_basedn()))
247 m['a01'] = ldb.MessageElement(groupmap.nt_name, ldb.FLAG_MOD_ADD, 'cn')
248 m['a02'] = ldb.MessageElement('group', ldb.FLAG_MOD_ADD, 'objectClass')
249 m['a03'] = ldb.MessageElement(ndr_pack(groupmap.sid), ldb.FLAG_MOD_ADD, 'objectSid')
250 m['a04'] = ldb.MessageElement(groupmap.comment, ldb.FLAG_MOD_ADD, 'description')
251 m['a05'] = ldb.MessageElement(groupmap.nt_name, ldb.FLAG_MOD_ADD, 'sAMAccountName')
253 if groupmap.sid_name_use == lsa.SID_NAME_ALIAS:
254 m['a06'] = ldb.MessageElement(str(dsdb.GTYPE_SECURITY_DOMAIN_LOCAL_GROUP), ldb.FLAG_MOD_ADD, 'groupType')
256 try:
257 samdb.add(m, controls=["relax:0"])
258 except ldb.LdbError, e:
259 logger.warn('Could not add group name=%s (%s)', groupmap.nt_name, str(e))
260 except Exception, e:
261 raise(e)
264 def add_users_to_group(samdb, group, members):
265 """Add user/member to group/alias"""
267 for member_sid in members:
268 m = ldb.Message()
269 m.dn = ldb.Dn(samdb, "<SID=%s" % str(group.sid))
270 m['a01'] = ldb.MessageElement("<SID=%s>" % str(member_sid), ldb.FLAG_MOD_REPLACE, 'member')
272 try:
273 samdb.modify(m)
274 except ldb.LdbError, e:
275 logger.warn("Could not add member to group '%s'", groupmap.nt_name)
276 except Exception, e:
277 raise(e)
280 def import_wins(samba4_winsdb, samba3_winsdb):
281 """Import settings from a Samba3 WINS database.
283 :param samba4_winsdb: WINS database to import to
284 :param samba3_winsdb: WINS database to import from
286 version_id = 0
288 for (name, (ttl, ips, nb_flags)) in samba3_winsdb.items():
289 version_id+=1
291 type = int(name.split("#", 1)[1], 16)
293 if type == 0x1C:
294 rType = 0x2
295 elif type & 0x80:
296 if len(ips) > 1:
297 rType = 0x2
298 else:
299 rType = 0x1
300 else:
301 if len(ips) > 1:
302 rType = 0x3
303 else:
304 rType = 0x0
306 if ttl > time.time():
307 rState = 0x0 # active
308 else:
309 rState = 0x1 # released
311 nType = ((nb_flags & 0x60)>>5)
313 samba4_winsdb.add({"dn": "name=%s,type=0x%s" % tuple(name.split("#")),
314 "type": name.split("#")[1],
315 "name": name.split("#")[0],
316 "objectClass": "winsRecord",
317 "recordType": str(rType),
318 "recordState": str(rState),
319 "nodeType": str(nType),
320 "expireTime": ldb.timestring(ttl),
321 "isStatic": "0",
322 "versionID": str(version_id),
323 "address": ips})
325 samba4_winsdb.add({"dn": "cn=VERSION",
326 "cn": "VERSION",
327 "objectClass": "winsMaxVersion",
328 "maxVersion": str(version_id)})
330 def enable_samba3sam(samdb, ldapurl):
331 """Enable Samba 3 LDAP URL database.
333 :param samdb: SAM Database.
334 :param ldapurl: Samba 3 LDAP URL
336 samdb.modify_ldif("""
337 dn: @MODULES
338 changetype: modify
339 replace: @LIST
340 @LIST: samldb,operational,objectguid,rdn_name,samba3sam
341 """)
343 samdb.add({"dn": "@MAP=samba3sam", "@MAP_URL": ldapurl})
346 smbconf_keep = [
347 "dos charset",
348 "unix charset",
349 "display charset",
350 "comment",
351 "path",
352 "directory",
353 "workgroup",
354 "realm",
355 "netbios name",
356 "netbios aliases",
357 "netbios scope",
358 "server string",
359 "interfaces",
360 "bind interfaces only",
361 "security",
362 "auth methods",
363 "encrypt passwords",
364 "null passwords",
365 "obey pam restrictions",
366 "password server",
367 "smb passwd file",
368 "private dir",
369 "passwd chat",
370 "password level",
371 "lanman auth",
372 "ntlm auth",
373 "client NTLMv2 auth",
374 "client lanman auth",
375 "client plaintext auth",
376 "read only",
377 "hosts allow",
378 "hosts deny",
379 "log level",
380 "debuglevel",
381 "log file",
382 "smb ports",
383 "large readwrite",
384 "max protocol",
385 "min protocol",
386 "unicode",
387 "read raw",
388 "write raw",
389 "disable netbios",
390 "nt status support",
391 "max mux",
392 "max xmit",
393 "name resolve order",
394 "max wins ttl",
395 "min wins ttl",
396 "time server",
397 "unix extensions",
398 "use spnego",
399 "server signing",
400 "client signing",
401 "max connections",
402 "paranoid server security",
403 "socket options",
404 "strict sync",
405 "max print jobs",
406 "printable",
407 "print ok",
408 "printer name",
409 "printer",
410 "map system",
411 "map hidden",
412 "map archive",
413 "preferred master",
414 "prefered master",
415 "local master",
416 "browseable",
417 "browsable",
418 "wins server",
419 "wins support",
420 "csc policy",
421 "strict locking",
422 "preload",
423 "auto services",
424 "lock dir",
425 "lock directory",
426 "pid directory",
427 "socket address",
428 "copy",
429 "include",
430 "available",
431 "volume",
432 "fstype",
433 "panic action",
434 "msdfs root",
435 "host msdfs",
436 "winbind separator"]
438 def upgrade_smbconf(oldconf,mark):
439 """Remove configuration variables not present in Samba4
441 :param oldconf: Old configuration structure
442 :param mark: Whether removed configuration variables should be
443 kept in the new configuration as "samba3:<name>"
445 data = oldconf.data()
446 newconf = LoadParm()
448 for s in data:
449 for p in data[s]:
450 keep = False
451 for k in smbconf_keep:
452 if smbconf_keep[k] == p:
453 keep = True
454 break
456 if keep:
457 newconf.set(s, p, oldconf.get(s, p))
458 elif mark:
459 newconf.set(s, "samba3:"+p, oldconf.get(s,p))
461 return newconf
463 SAMBA3_PREDEF_NAMES = {
464 'HKLM': registry.HKEY_LOCAL_MACHINE,
467 def import_registry(samba4_registry, samba3_regdb):
468 """Import a Samba 3 registry database into the Samba 4 registry.
470 :param samba4_registry: Samba 4 registry handle.
471 :param samba3_regdb: Samba 3 registry database handle.
473 def ensure_key_exists(keypath):
474 (predef_name, keypath) = keypath.split("/", 1)
475 predef_id = SAMBA3_PREDEF_NAMES[predef_name]
476 keypath = keypath.replace("/", "\\")
477 return samba4_registry.create_key(predef_id, keypath)
479 for key in samba3_regdb.keys():
480 key_handle = ensure_key_exists(key)
481 for subkey in samba3_regdb.subkeys(key):
482 ensure_key_exists(subkey)
483 for (value_name, (value_type, value_data)) in samba3_regdb.values(key).items():
484 key_handle.set_value(value_name, value_type, value_data)
487 def upgrade_from_samba3(samba3, logger, session_info, smbconf, targetdir):
488 """Upgrade from samba3 database to samba4 AD database
491 # Read samba3 smb.conf
492 oldconf = s3param.get_context();
493 oldconf.load(smbconf)
495 if oldconf.get("domain logons"):
496 serverrole = "domain controller"
497 else:
498 if oldconf.get("security") == "user":
499 serverrole = "standalone"
500 else:
501 serverrole = "member server"
503 domainname = oldconf.get("workgroup")
504 realm = oldconf.get("realm")
505 netbiosname = oldconf.get("netbios name")
507 # secrets db
508 secrets_db = samba3.get_secrets_db()
510 if not domainname:
511 domainname = secrets_db.domains()[0]
512 logger.warning("No domain specified in smb.conf file, assuming '%s'",
513 domainname)
515 if not realm:
516 if oldconf.get("domain logons"):
517 logger.warning("No realm specified in smb.conf file and being a DC. That upgrade path doesn't work! Please add a 'realm' directive to your old smb.conf to let us know which one you want to use (generally it's the upcased DNS domainname).")
518 return
519 else:
520 realm = domainname.upper()
521 logger.warning("No realm specified in smb.conf file, assuming '%s'",
522 realm)
524 # Find machine account and password
525 machinepass = None
526 machinerid = None
527 machinesid = None
528 next_rid = 1000
530 try:
531 machinepass = secrets_db.get_machine_password(netbiosname)
532 except:
533 pass
535 # We must close the direct pytdb database before the C code loads it
536 secrets_db.close()
538 passdb.set_secrets_dir(samba3.libdir)
540 # Get domain sid
541 try:
542 domainsid = passdb.get_global_sam_sid()
543 except:
544 raise Exception("Can't find domain sid for '%s', Exiting." % domainname)
546 # Get machine account, sid, rid
547 try:
548 machineacct = old_passdb.getsampwnam('%s$' % netbiosname)
549 machinesid, machinerid = machineacct.user_sid.split()
550 except:
551 pass
553 # Connect to old password backend
554 old_passdb = passdb.PDB(oldconf.get('passdb backend'))
556 # Import groups from old passdb backend
557 logger.info("Exporting groups")
558 grouplist = old_passdb.enum_group_mapping()
559 groupmembers = {}
560 for group in grouplist:
561 sid, rid = group.sid.split()
562 if sid == domainsid:
563 if rid >= next_rid:
564 next_rid = rid + 1
566 # Get members for each group/alias
567 if group.sid_name_use == lsa.SID_NAME_ALIAS or group.sid_name_use == lsa.SID_NAME_WKN_GRP:
568 members = old_passdb.enum_aliasmem(group.sid)
569 elif group.sid_name_use == lsa.SID_NAME_DOM_GRP:
570 try:
571 members = old_passdb.enum_group_members(group.sid)
572 except:
573 continue
574 else:
575 logger.warn("Ignoring group '%s' with sid_name_use=%d",
576 group.nt_name, group.sid_name_use)
577 continue
578 groupmembers[group.nt_name] = members
581 # Import users from old passdb backend
582 logger.info("Exporting users")
583 userlist = old_passdb.search_users(0)
584 userdata = {}
585 uids = {}
586 for entry in userlist:
587 if machinerid and machinerid == entry['rid']:
588 continue
589 username = entry['account_name']
590 if entry['rid'] < 1000:
591 logger.info(" Skipping wellknown rid=%d (for username=%s)", entry['rid'], username)
592 continue
593 if entry['rid'] >= next_rid:
594 next_rid = entry['rid'] + 1
596 userdata[username] = old_passdb.getsampwnam(username)
597 try:
598 uids[username] = old_passdb.sid_to_id(userdata[username].user_sid)[0]
599 except:
600 try:
601 uids[username] = pwd.getpwnam(username).pw_uid
602 except:
603 pass
605 logger.info("Next rid = %d", next_rid)
607 # Do full provision
608 result = provision(logger, session_info, None,
609 targetdir=targetdir, realm=realm, domain=domainname,
610 domainsid=str(domainsid), next_rid=next_rid,
611 dc_rid=machinerid,
612 hostname=netbiosname, machinepass=machinepass,
613 serverrole=serverrole, samdb_fill=FILL_FULL)
615 logger.info("Import WINS")
616 import_wins(Ldb(result.paths.winsdb), samba3.get_wins_db())
618 new_smbconf = result.lp.configfile
619 newconf = s3param.get_context()
620 newconf.load(new_smbconf)
622 # Migrate idmap
623 logger.info("Migrating idmap database")
624 import_idmap(result.idmap, samba3.get_idmap_db(), logger)
626 # Connect to samba4 backend
627 new_passdb = passdb.PDB('samba4')
629 # Export groups to samba4 backend
630 logger.info("Importing groups")
631 for g in grouplist:
632 # Ignore uninitialized groups (gid = -1)
633 if g.gid != 0xffffffff:
634 add_idmap_entry(result.idmap, g.sid, g.gid, "GID", logger)
635 add_group_from_mapping_entry(result.samdb, g, logger)
637 # Export users to samba4 backend
638 logger.info("Importing users")
639 for username in userdata:
640 new_passdb.add_sam_account(userdata[username])
641 if username in uids:
642 add_idmap_entry(result.idmap, userdata[username].user_sid, uids[username], "UID", logger)
644 logger.info("Adding users to groups")
645 for g in grouplist:
646 if g.nt_name in groupmembers:
647 add_users_to_group(result.samdb, g, groupmembers[g.nt_name])
649 # FIXME: import_registry(registry.Registry(), samba3.get_registry())