s3_upgrade: Let python generate backtrace for unknown exceptions
[samba.git] / source4 / scripting / python / samba / upgrade.py
1 # backend code for upgrading from Samba3
2 # Copyright Jelmer Vernooij 2005-2007
3 #
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.
8 #
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.
13 #
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/>.
16 #
17
18 """Support code for upgrading from Samba 3 to Samba 4."""
19
20 __docformat__ = "restructuredText"
21
22 import grp
23 import ldb
24 import time
25 import pwd
26
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
36
37
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
53
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))
59
60
61 def import_sam_account(samldb,acc,domaindn,domainsid):
62     """Import a Samba 3 SAM account.
63
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
70
71     if acc.fullname is None:
72         try:
73             acc.fullname = pwd.getpwnam(acc.username)[4].split(",")[0]
74         except KeyError:
75             pass
76
77     if acc.fullname is None:
78         acc.fullname = acc.username
79
80     assert acc.fullname is not None
81     assert acc.nt_username is not None
82
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,
110         })
111
112
113 def import_sam_group(samldb, sid, gid, sid_name_use, nt_name, comment, domaindn):
114     """Upgrade a SAM group.
115
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
122     """
123
124     if sid_name_use == 5: # Well-known group
125         return None
126
127     if nt_name in ("Domain Guests", "Domain Users", "Domain Admins"):
128         return None
129
130     if gid == -1:
131         gr = grp.getgrnam(nt_name)
132     else:
133         gr = grp.getgrgid(gid)
134
135     if gr is None:
136         unixname = "UNKNOWN"
137     else:
138         unixname = gr.gr_name
139
140     assert unixname is not None
141
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)
150         })
151
152
153 def add_idmap_entry(idmapdb, sid, xid, xid_type, logger):
154     """Create idmap entry
155
156     :param idmapdb: Samba4 IDMAP database
157     :param sid: user/group sid
158     :param xid: user/group id
159     :param xid_type: type of id (UID/GID)
160     :param logger: Logger object
161     """
162
163     # First try to see if we already have this entry
164     found = False
165     msg = idmapdb.search(expression='objectSid=%s' % str(sid))
166     if msg.count == 1:
167         found = True
168
169     if found:
170         try:
171             m = ldb.Message()
172             m.dn = ldb.Dn(idmapdb, msg[0]['dn'])
173             m['xidNumber'] = ldb.MessageElement(str(xid), ldb.FLAG_MOD_REPLACE, 'xidNumber')
174             m['type'] = ldb.MessageElement(xid_type, ldb.FLAG_MOD_REPLACE, 'type')
175             idmapdb.modify(m)
176         except ldb.LdbError, e:
177             logger.warn('Could not modify idmap entry for sid=%s, id=%s, type=%s (%s)',
178                             str(sid), str(xid), xid_type, str(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
191
192 def import_idmap(idmapdb, samba3_idmap, logger):
193     """Import idmap data.
194
195     :param idmapdb: Samba4 IDMAP database
196     :param samba3_idmap: Samba3 IDMAP database to import from
197     :param logger: Logger object
198     """
199     currentxid = max(samba3_idmap.get_user_hwm(), samba3_idmap.get_group_hwm())
200     lowerbound = currentxid
201     # FIXME: upperbound
202
203     m = ldb.Message()
204     m.dn = ldb.Dn(idmapdb, 'CN=CONFIG')
205     m['lowerbound'] = ldb.MessageElement(str(lowerbound), ldb.FLAG_MOD_REPLACE, 'lowerBound')
206     m['xidNumber'] = ldb.MessageElement(str(currentxid), ldb.FLAG_MOD_REPLACE, 'xidNumber')
207     idmapdb.modify(m)
208
209     for id_type, xid in samba3_idmap.ids():
210         if id_type == 'UID':
211             xid_type = 'ID_TYPE_UID'
212         elif id_type == 'GID':
213             xid_type = 'ID_TYPE_GID'
214         else:
215             logger.warn('Wrong type of entry in idmap (%s), Ignoring', id_type)
216             continue
217
218         sid = samba3_idmap.get_sid(xid, id_type)
219         add_idmap_entry(idmapdb, dom_sid(sid), xid, xid_type, logger)
220
221
222 def add_group_from_mapping_entry(samdb, groupmap, logger):
223     """Add or modify group from group mapping entry
224
225     param samdb: Samba4 SAM database
226     param groupmap: Groupmap entry
227     param logger: Logger object
228     """
229
230     # First try to see if we already have this entry
231     try:
232         msg = samdb.search(base='<SID=%s>' % str(groupmap.sid), scope=ldb.SCOPE_BASE)
233         found = True
234     except ldb.LdbError, (ecode, emsg):
235         if ecode == ldb.ERR_NO_SUCH_OBJECT:
236             found = False
237         else:
238             raise ldb.LdbError(ecode, emsg)
239
240     if found:
241         logger.warn('Group already exists sid=%s, groupname=%s existing_groupname=%s, Ignoring.',
242                             str(groupmap.sid), groupmap.nt_name, msg[0]['sAMAccountName'][0])
243     else:
244         if groupmap.sid_name_use == lsa.SID_NAME_WKN_GRP:
245             return
246
247         m = ldb.Message()
248         m.dn = ldb.Dn(samdb, "CN=%s,CN=Users,%s" % (groupmap.nt_name, samdb.get_default_basedn()))
249         m['a01'] = ldb.MessageElement(groupmap.nt_name, ldb.FLAG_MOD_ADD, 'cn')
250         m['a02'] = ldb.MessageElement('group', ldb.FLAG_MOD_ADD, 'objectClass')
251         m['a03'] = ldb.MessageElement(ndr_pack(groupmap.sid), ldb.FLAG_MOD_ADD, 'objectSid')
252         m['a04'] = ldb.MessageElement(groupmap.comment, ldb.FLAG_MOD_ADD, 'description')
253         m['a05'] = ldb.MessageElement(groupmap.nt_name, ldb.FLAG_MOD_ADD, 'sAMAccountName')
254
255         if groupmap.sid_name_use == lsa.SID_NAME_ALIAS:
256             m['a06'] = ldb.MessageElement(str(dsdb.GTYPE_SECURITY_DOMAIN_LOCAL_GROUP), ldb.FLAG_MOD_ADD, 'groupType')
257
258         try:
259             samdb.add(m, controls=["relax:0"])
260         except ldb.LdbError, e:
261             logger.warn('Could not add group name=%s (%s)', groupmap.nt_name, str(e))
262
263
264 def add_users_to_group(samdb, group, members, logger):
265     """Add user/member to group/alias
266
267     param samdb: Samba4 SAM database
268     param group: Groupmap object
269     param members: List of member SIDs
270     param logger: Logger object
271     """
272     for member_sid in members:
273         m = ldb.Message()
274         m.dn = ldb.Dn(samdb, "<SID=%s" % str(group.sid))
275         m['a01'] = ldb.MessageElement("<SID=%s>" % str(member_sid), ldb.FLAG_MOD_REPLACE, 'member')
276
277         try:
278             samdb.modify(m)
279         except ldb.LdbError, e:
280             logger.warn("Could not add member to group '%s'", groupmap.nt_name)
281
282
283 def import_wins(samba4_winsdb, samba3_winsdb):
284     """Import settings from a Samba3 WINS database.
285
286     :param samba4_winsdb: WINS database to import to
287     :param samba3_winsdb: WINS database to import from
288     """
289     version_id = 0
290
291     for (name, (ttl, ips, nb_flags)) in samba3_winsdb.items():
292         version_id+=1
293
294         type = int(name.split("#", 1)[1], 16)
295
296         if type == 0x1C:
297             rType = 0x2
298         elif type & 0x80:
299             if len(ips) > 1:
300                 rType = 0x2
301             else:
302                 rType = 0x1
303         else:
304             if len(ips) > 1:
305                 rType = 0x3
306             else:
307                 rType = 0x0
308
309         if ttl > time.time():
310             rState = 0x0 # active
311         else:
312             rState = 0x1 # released
313
314         nType = ((nb_flags & 0x60)>>5)
315
316         samba4_winsdb.add({"dn": "name=%s,type=0x%s" % tuple(name.split("#")),
317                            "type": name.split("#")[1],
318                            "name": name.split("#")[0],
319                            "objectClass": "winsRecord",
320                            "recordType": str(rType),
321                            "recordState": str(rState),
322                            "nodeType": str(nType),
323                            "expireTime": ldb.timestring(ttl),
324                            "isStatic": "0",
325                            "versionID": str(version_id),
326                            "address": ips})
327
328     samba4_winsdb.add({"dn": "cn=VERSION",
329                        "cn": "VERSION",
330                        "objectClass": "winsMaxVersion",
331                        "maxVersion": str(version_id)})
332
333 def enable_samba3sam(samdb, ldapurl):
334     """Enable Samba 3 LDAP URL database.
335
336     :param samdb: SAM Database.
337     :param ldapurl: Samba 3 LDAP URL
338     """
339     samdb.modify_ldif("""
340 dn: @MODULES
341 changetype: modify
342 replace: @LIST
343 @LIST: samldb,operational,objectguid,rdn_name,samba3sam
344 """)
345
346     samdb.add({"dn": "@MAP=samba3sam", "@MAP_URL": ldapurl})
347
348
349 smbconf_keep = [
350     "dos charset",
351     "unix charset",
352     "display charset",
353     "comment",
354     "path",
355     "directory",
356     "workgroup",
357     "realm",
358     "netbios name",
359     "netbios aliases",
360     "netbios scope",
361     "server string",
362     "interfaces",
363     "bind interfaces only",
364     "security",
365     "auth methods",
366     "encrypt passwords",
367     "null passwords",
368     "obey pam restrictions",
369     "password server",
370     "smb passwd file",
371     "private dir",
372     "passwd chat",
373     "password level",
374     "lanman auth",
375     "ntlm auth",
376     "client NTLMv2 auth",
377     "client lanman auth",
378     "client plaintext auth",
379     "read only",
380     "hosts allow",
381     "hosts deny",
382     "log level",
383     "debuglevel",
384     "log file",
385     "smb ports",
386     "large readwrite",
387     "max protocol",
388     "min protocol",
389     "unicode",
390     "read raw",
391     "write raw",
392     "disable netbios",
393     "nt status support",
394     "max mux",
395     "max xmit",
396     "name resolve order",
397     "max wins ttl",
398     "min wins ttl",
399     "time server",
400     "unix extensions",
401     "use spnego",
402     "server signing",
403     "client signing",
404     "max connections",
405     "paranoid server security",
406     "socket options",
407     "strict sync",
408     "max print jobs",
409     "printable",
410     "print ok",
411     "printer name",
412     "printer",
413     "map system",
414     "map hidden",
415     "map archive",
416     "preferred master",
417     "prefered master",
418     "local master",
419     "browseable",
420     "browsable",
421     "wins server",
422     "wins support",
423     "csc policy",
424     "strict locking",
425     "preload",
426     "auto services",
427     "lock dir",
428     "lock directory",
429     "pid directory",
430     "socket address",
431     "copy",
432     "include",
433     "available",
434     "volume",
435     "fstype",
436     "panic action",
437     "msdfs root",
438     "host msdfs",
439     "winbind separator"]
440
441 def upgrade_smbconf(oldconf,mark):
442     """Remove configuration variables not present in Samba4
443
444     :param oldconf: Old configuration structure
445     :param mark: Whether removed configuration variables should be
446         kept in the new configuration as "samba3:<name>"
447     """
448     data = oldconf.data()
449     newconf = LoadParm()
450
451     for s in data:
452         for p in data[s]:
453             keep = False
454             for k in smbconf_keep:
455                 if smbconf_keep[k] == p:
456                     keep = True
457                     break
458
459             if keep:
460                 newconf.set(s, p, oldconf.get(s, p))
461             elif mark:
462                 newconf.set(s, "samba3:"+p, oldconf.get(s,p))
463
464     return newconf
465
466 SAMBA3_PREDEF_NAMES = {
467         'HKLM': registry.HKEY_LOCAL_MACHINE,
468 }
469
470 def import_registry(samba4_registry, samba3_regdb):
471     """Import a Samba 3 registry database into the Samba 4 registry.
472
473     :param samba4_registry: Samba 4 registry handle.
474     :param samba3_regdb: Samba 3 registry database handle.
475     """
476     def ensure_key_exists(keypath):
477         (predef_name, keypath) = keypath.split("/", 1)
478         predef_id = SAMBA3_PREDEF_NAMES[predef_name]
479         keypath = keypath.replace("/", "\\")
480         return samba4_registry.create_key(predef_id, keypath)
481
482     for key in samba3_regdb.keys():
483         key_handle = ensure_key_exists(key)
484         for subkey in samba3_regdb.subkeys(key):
485             ensure_key_exists(subkey)
486         for (value_name, (value_type, value_data)) in samba3_regdb.values(key).items():
487             key_handle.set_value(value_name, value_type, value_data)
488
489
490 def upgrade_from_samba3(samba3, logger, session_info, smbconf, targetdir):
491     """Upgrade from samba3 database to samba4 AD database
492     """
493
494     # Read samba3 smb.conf
495     oldconf = s3param.get_context();
496     oldconf.load(smbconf)
497
498     if oldconf.get("domain logons"):
499         serverrole = "domain controller"
500     else:
501         if oldconf.get("security") == "user":
502             serverrole = "standalone"
503         else:
504             serverrole = "member server"
505
506     domainname = oldconf.get("workgroup")
507     realm = oldconf.get("realm")
508     netbiosname = oldconf.get("netbios name")
509
510     # secrets db
511     secrets_db = samba3.get_secrets_db()
512
513     if not domainname:
514         domainname = secrets_db.domains()[0]
515         logger.warning("No domain specified in smb.conf file, assuming '%s'",
516                 domainname)
517
518     if not realm:
519         if oldconf.get("domain logons"):
520             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).")
521             return
522         else:
523             realm = domainname.upper()
524             logger.warning("No realm specified in smb.conf file, assuming '%s'",
525                     realm)
526
527     # Find machine account and password
528     machinepass = None
529     machinerid = None
530     machinesid = None
531     next_rid = 1000
532
533     try:
534         machinepass = secrets_db.get_machine_password(netbiosname)
535     except:
536         pass
537
538     # We must close the direct pytdb database before the C code loads it
539     secrets_db.close()
540
541     passdb.set_secrets_dir(samba3.privatedir)
542
543     # Get domain sid
544     try:
545         domainsid = passdb.get_global_sam_sid()
546     except passdb.error:
547         raise Exception("Can't find domain sid for '%s', Exiting." % domainname)
548
549     # Get machine account, sid, rid
550     try:
551         machineacct = old_passdb.getsampwnam('%s$' % netbiosname)
552         machinesid, machinerid = machineacct.user_sid.split()
553     except:
554         pass
555
556     # Connect to old password backend
557     old_passdb = passdb.PDB(oldconf.get('passdb backend'))
558
559     # Import groups from old passdb backend
560     logger.info("Exporting groups")
561     grouplist = old_passdb.enum_group_mapping()
562     groupmembers = {}
563     for group in grouplist:
564         sid, rid = group.sid.split()
565         if sid == domainsid:
566             if rid >= next_rid:
567                next_rid = rid + 1
568
569         # Get members for each group/alias
570         if group.sid_name_use == lsa.SID_NAME_ALIAS or group.sid_name_use == lsa.SID_NAME_WKN_GRP:
571             members = old_passdb.enum_aliasmem(group.sid)
572         elif group.sid_name_use == lsa.SID_NAME_DOM_GRP:
573             try:
574                 members = old_passdb.enum_group_members(group.sid)
575             except:
576                 continue
577         else:
578             logger.warn("Ignoring group '%s' with sid_name_use=%d",
579                         group.nt_name, group.sid_name_use)
580             continue
581         groupmembers[group.nt_name] = members
582
583
584     # Import users from old passdb backend
585     logger.info("Exporting users")
586     userlist = old_passdb.search_users(0)
587     userdata = {}
588     uids = {}
589     admin_user = None
590     for entry in userlist:
591         if machinerid and machinerid == entry['rid']:
592             continue
593         username = entry['account_name']
594         if entry['rid'] < 1000:
595             logger.info("  Skipping wellknown rid=%d (for username=%s)", entry['rid'], username)
596             continue
597         if entry['rid'] >= next_rid:
598             next_rid = entry['rid'] + 1
599         
600         userdata[username] = old_passdb.getsampwnam(username)
601         try:
602             uids[username] = old_passdb.sid_to_id(userdata[username].user_sid)[0]
603         except:
604             try:
605                 uids[username] = pwd.getpwnam(username).pw_uid
606             except:
607                 pass
608
609         if not admin_user and username.lower() == 'root':
610             admin_user = username
611         if username.lower() == 'administrator':
612             admin_user = username
613
614
615     logger.info("Next rid = %d", next_rid)
616
617     # Do full provision
618     result = provision(logger, session_info, None,
619                        targetdir=targetdir, realm=realm, domain=domainname,
620                        domainsid=str(domainsid), next_rid=next_rid,
621                        dc_rid=machinerid,
622                        hostname=netbiosname, machinepass=machinepass,
623                        serverrole=serverrole, samdb_fill=FILL_FULL)
624
625     logger.info("Import WINS")
626     import_wins(Ldb(result.paths.winsdb), samba3.get_wins_db())
627
628     new_smbconf = result.lp.configfile
629     newconf = s3param.get_context()
630     newconf.load(new_smbconf)
631
632     # Migrate idmap
633     logger.info("Migrating idmap database")
634     import_idmap(result.idmap, samba3.get_idmap_db(), logger)
635
636     # Connect to samba4 backend
637     new_passdb = passdb.PDB('samba4')
638
639     # Export groups to samba4 backend
640     logger.info("Importing groups")
641     for g in grouplist:
642         # Ignore uninitialized groups (gid = -1)
643         if g.gid != 0xffffffff:
644             add_idmap_entry(result.idmap, g.sid, g.gid, "GID", logger)
645             add_group_from_mapping_entry(result.samdb, g, logger)
646
647     # Export users to samba4 backend
648     logger.info("Importing users")
649     for username in userdata:
650         if username.lower() == 'administrator' or username.lower() == 'root':
651             continue
652         new_passdb.add_sam_account(userdata[username])
653         if username in uids:
654             add_idmap_entry(result.idmap, userdata[username].user_sid, uids[username], "UID", logger)
655
656     logger.info("Adding users to groups")
657     for g in grouplist:
658         if g.nt_name in groupmembers:
659             add_users_to_group(result.samdb, g, groupmembers[g.nt_name], logger)
660
661     # Set password for administrator
662     if admin_user:
663         logger.info("Setting password for administrator")
664         admin_userdata = new_passdb.getsampwnam("administrator")
665         admin_userdata.nt_passwd = userdata[admin_user].nt_passwd
666         if userdata[admin_user].lanman_passwd:
667             admin_userdata.lanman_passwd = userdata[admin_user].lanman_passwd
668         admin_userdata.pass_last_set_time = userdata[admin_user].pass_last_set_time
669         if userdata[admin_user].pw_history:
670             admin_userdata.pw_history = userdata[admin_user].pw_history
671         new_passdb.update_sam_account(admin_userdata)
672         logger.info("Administrator password has been set to password of user '%s'", admin_user)
673
674     # FIXME: import_registry(registry.Registry(), samba3.get_registry())