s4-s3-upgrade Handle expected errors, error out on unexpected ones
[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, ProvisioningError
30 from samba.samba3 import passdb
31 from samba.samba3 import param as s3param
32 from samba.dcerpc import lsa, samr, security
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(samdb, policy, logger):
39     """Import a Samba 3 policy.
40
41     :param samdb: Samba4 SAM database
42     :param policy: Samba3 account policy
43     :param logger: Logger object
44     """
45
46     # Following entries are used -
47     #    min password length, password history, minimum password age,
48     #    maximum password age, lockout duration
49     #
50     # Following entries are not used -
51     #    reset count minutes, user must logon to change password,
52     #    bad lockout minutes, disconnect time
53
54     m = ldb.Message()
55     m.dn = samdb.get_default_basedn()
56     m['a01'] = ldb.MessageElement(str(policy['min password length']), ldb.FLAG_MOD_REPLACE,
57                             'minPwdLength')
58     m['a02'] = ldb.MessageElement(str(policy['password history']), ldb.FLAG_MOD_REPLACE,
59                             'pwdHistoryLength')
60     m['a03'] = ldb.MessageElement(str(policy['minimum password age']), ldb.FLAG_MOD_REPLACE,
61                             'minPwdAge')
62     m['a04'] = ldb.MessageElement(str(policy['maximum password age']), ldb.FLAG_MOD_REPLACE,
63                             'maxPwdAge')
64     m['a05'] = ldb.MessageElement(str(policy['lockout duration']), ldb.FLAG_MOD_REPLACE,
65                             'lockoutDuration')
66
67     try:
68         samdb.modify(m)
69     except ldb.LdbError, e:
70         logger.warn("Could not set account policy, (%s)", str(e))
71
72
73 def add_idmap_entry(idmapdb, sid, xid, xid_type, logger):
74     """Create idmap entry
75
76     :param idmapdb: Samba4 IDMAP database
77     :param sid: user/group sid
78     :param xid: user/group id
79     :param xid_type: type of id (UID/GID)
80     :param logger: Logger object
81     """
82
83     # First try to see if we already have this entry
84     found = False
85     msg = idmapdb.search(expression='objectSid=%s' % str(sid))
86     if msg.count == 1:
87         found = True
88
89     if found:
90         try:
91             m = ldb.Message()
92             m.dn = msg[0]['dn']
93             m['xidNumber'] = ldb.MessageElement(str(xid), ldb.FLAG_MOD_REPLACE, 'xidNumber')
94             m['type'] = ldb.MessageElement(xid_type, ldb.FLAG_MOD_REPLACE, 'type')
95             idmapdb.modify(m)
96         except ldb.LdbError, e:
97             logger.warn('Could not modify idmap entry for sid=%s, id=%s, type=%s (%s)',
98                             str(sid), str(xid), xid_type, str(e))
99     else:
100         try:
101             idmapdb.add({"dn": "CN=%s" % str(sid),
102                         "cn": str(sid),
103                         "objectClass": "sidMap",
104                         "objectSid": ndr_pack(sid),
105                         "type": xid_type,
106                         "xidNumber": str(xid)})
107         except ldb.LdbError, e:
108             logger.warn('Could not add idmap entry for sid=%s, id=%s, type=%s (%s)',
109                             str(sid), str(xid), xid_type, str(e))
110
111
112 def import_idmap(idmapdb, samba3, logger):
113     """Import idmap data.
114
115     :param idmapdb: Samba4 IDMAP database
116     :param samba3_idmap: Samba3 IDMAP database to import from
117     :param logger: Logger object
118     """
119
120     try:
121         samba3_idmap = samba3.get_idmap_db()
122     except IOError as (errno, strerror):
123         logger.warn('Cannot open idmap database, Ignoring: ({0}): {1}'.format(errno, strerror))
124         return
125
126     currentxid = max(samba3_idmap.get_user_hwm(), samba3_idmap.get_group_hwm())
127     lowerbound = currentxid
128     # FIXME: upperbound
129
130     m = ldb.Message()
131     m.dn = ldb.Dn(idmapdb, 'CN=CONFIG')
132     m['lowerbound'] = ldb.MessageElement(str(lowerbound), ldb.FLAG_MOD_REPLACE, 'lowerBound')
133     m['xidNumber'] = ldb.MessageElement(str(currentxid), ldb.FLAG_MOD_REPLACE, 'xidNumber')
134     idmapdb.modify(m)
135
136     for id_type, xid in samba3_idmap.ids():
137         if id_type == 'UID':
138             xid_type = 'ID_TYPE_UID'
139         elif id_type == 'GID':
140             xid_type = 'ID_TYPE_GID'
141         else:
142             logger.warn('Wrong type of entry in idmap (%s), Ignoring', id_type)
143             continue
144
145         sid = samba3_idmap.get_sid(xid, id_type)
146         add_idmap_entry(idmapdb, dom_sid(sid), xid, xid_type, logger)
147
148
149 def add_group_from_mapping_entry(samdb, groupmap, logger):
150     """Add or modify group from group mapping entry
151
152     param samdb: Samba4 SAM database
153     param groupmap: Groupmap entry
154     param logger: Logger object
155     """
156
157     # First try to see if we already have this entry
158     try:
159         msg = samdb.search(base='<SID=%s>' % str(groupmap.sid), scope=ldb.SCOPE_BASE)
160         found = True
161     except ldb.LdbError, (ecode, emsg):
162         if ecode == ldb.ERR_NO_SUCH_OBJECT:
163             found = False
164         else:
165             raise ldb.LdbError(ecode, emsg)
166
167     if found:
168         logger.warn('Group already exists sid=%s, groupname=%s existing_groupname=%s, Ignoring.',
169                             str(groupmap.sid), groupmap.nt_name, msg[0]['sAMAccountName'][0])
170     else:
171         if groupmap.sid_name_use == lsa.SID_NAME_WKN_GRP:
172             # In a lot of Samba3 databases, aliases are marked as well known groups
173             (group_dom_sid, rid) = group.sid.split()
174             if (group_dom_sid != security.dom_sid(security.SID_BUILTIN)):
175                 return
176
177         m = ldb.Message()
178         m.dn = ldb.Dn(samdb, "CN=%s,CN=Users,%s" % (groupmap.nt_name, samdb.get_default_basedn()))
179         m['a01'] = ldb.MessageElement(groupmap.nt_name, ldb.FLAG_MOD_ADD, 'cn')
180         m['a02'] = ldb.MessageElement('group', ldb.FLAG_MOD_ADD, 'objectClass')
181         m['a03'] = ldb.MessageElement(ndr_pack(groupmap.sid), ldb.FLAG_MOD_ADD, 'objectSid')
182         m['a04'] = ldb.MessageElement(groupmap.comment, ldb.FLAG_MOD_ADD, 'description')
183         m['a05'] = ldb.MessageElement(groupmap.nt_name, ldb.FLAG_MOD_ADD, 'sAMAccountName')
184
185         # Fix up incorrect 'well known' groups that are actually builtin (per test above) to be aliases
186         if groupmap.sid_name_use == lsa.SID_NAME_ALIAS or groupmap.sid_name_use == lsa.SID_NAME_WKN_GRP:
187             m['a06'] = ldb.MessageElement(str(dsdb.GTYPE_SECURITY_DOMAIN_LOCAL_GROUP), ldb.FLAG_MOD_ADD, 'groupType')
188
189         try:
190             samdb.add(m, controls=["relax:0"])
191         except ldb.LdbError, e:
192             logger.warn('Could not add group name=%s (%s)', groupmap.nt_name, str(e))
193
194
195 def add_users_to_group(samdb, group, members, logger):
196     """Add user/member to group/alias
197
198     param samdb: Samba4 SAM database
199     param group: Groupmap object
200     param members: List of member SIDs
201     param logger: Logger object
202     """
203     for member_sid in members:
204         m = ldb.Message()
205         m.dn = ldb.Dn(samdb, "<SID=%s>" % str(group.sid))
206         m['a01'] = ldb.MessageElement("<SID=%s>" % str(member_sid), ldb.FLAG_MOD_ADD, 'member')
207
208         try:
209             samdb.modify(m)
210         except ldb.LdbError, (ecode, emsg):
211             if ecode == ldb.ERR_ENTRY_ALREADY_EXISTS:
212                 logger.info("skipped re-adding member '%s' to group '%s': %s", member_sid, group.sid, emsg)
213             elif ecode == ldb.ERR_NO_SUCH_OBJECT:
214                 raise ProvisioningError("Could not add member '%s' to group '%s' as either group or user record doesn't exist: %s" % (member_sid, group.sid, emsg))
215             else:
216                 raise ProvisioningError("Could not add member '%s' to group '%s': %s" % (member_sid, group.sid, emsg))
217
218
219 def import_wins(samba4_winsdb, samba3_winsdb):
220     """Import settings from a Samba3 WINS database.
221
222     :param samba4_winsdb: WINS database to import to
223     :param samba3_winsdb: WINS database to import from
224     """
225     version_id = 0
226
227     for (name, (ttl, ips, nb_flags)) in samba3_winsdb.items():
228         version_id+=1
229
230         type = int(name.split("#", 1)[1], 16)
231
232         if type == 0x1C:
233             rType = 0x2
234         elif type & 0x80:
235             if len(ips) > 1:
236                 rType = 0x2
237             else:
238                 rType = 0x1
239         else:
240             if len(ips) > 1:
241                 rType = 0x3
242             else:
243                 rType = 0x0
244
245         if ttl > time.time():
246             rState = 0x0 # active
247         else:
248             rState = 0x1 # released
249
250         nType = ((nb_flags & 0x60)>>5)
251
252         samba4_winsdb.add({"dn": "name=%s,type=0x%s" % tuple(name.split("#")),
253                            "type": name.split("#")[1],
254                            "name": name.split("#")[0],
255                            "objectClass": "winsRecord",
256                            "recordType": str(rType),
257                            "recordState": str(rState),
258                            "nodeType": str(nType),
259                            "expireTime": ldb.timestring(ttl),
260                            "isStatic": "0",
261                            "versionID": str(version_id),
262                            "address": ips})
263
264     samba4_winsdb.add({"dn": "cn=VERSION",
265                        "cn": "VERSION",
266                        "objectClass": "winsMaxVersion",
267                        "maxVersion": str(version_id)})
268
269 def enable_samba3sam(samdb, ldapurl):
270     """Enable Samba 3 LDAP URL database.
271
272     :param samdb: SAM Database.
273     :param ldapurl: Samba 3 LDAP URL
274     """
275     samdb.modify_ldif("""
276 dn: @MODULES
277 changetype: modify
278 replace: @LIST
279 @LIST: samldb,operational,objectguid,rdn_name,samba3sam
280 """)
281
282     samdb.add({"dn": "@MAP=samba3sam", "@MAP_URL": ldapurl})
283
284
285 smbconf_keep = [
286     "dos charset",
287     "unix charset",
288     "display charset",
289     "comment",
290     "path",
291     "directory",
292     "workgroup",
293     "realm",
294     "netbios name",
295     "netbios aliases",
296     "netbios scope",
297     "server string",
298     "interfaces",
299     "bind interfaces only",
300     "security",
301     "auth methods",
302     "encrypt passwords",
303     "null passwords",
304     "obey pam restrictions",
305     "password server",
306     "smb passwd file",
307     "private dir",
308     "passwd chat",
309     "password level",
310     "lanman auth",
311     "ntlm auth",
312     "client NTLMv2 auth",
313     "client lanman auth",
314     "client plaintext auth",
315     "read only",
316     "hosts allow",
317     "hosts deny",
318     "log level",
319     "debuglevel",
320     "log file",
321     "smb ports",
322     "large readwrite",
323     "max protocol",
324     "min protocol",
325     "unicode",
326     "read raw",
327     "write raw",
328     "disable netbios",
329     "nt status support",
330     "max mux",
331     "max xmit",
332     "name resolve order",
333     "max wins ttl",
334     "min wins ttl",
335     "time server",
336     "unix extensions",
337     "use spnego",
338     "server signing",
339     "client signing",
340     "max connections",
341     "paranoid server security",
342     "socket options",
343     "strict sync",
344     "max print jobs",
345     "printable",
346     "print ok",
347     "printer name",
348     "printer",
349     "map system",
350     "map hidden",
351     "map archive",
352     "preferred master",
353     "prefered master",
354     "local master",
355     "browseable",
356     "browsable",
357     "wins server",
358     "wins support",
359     "csc policy",
360     "strict locking",
361     "preload",
362     "auto services",
363     "lock dir",
364     "lock directory",
365     "pid directory",
366     "socket address",
367     "copy",
368     "include",
369     "available",
370     "volume",
371     "fstype",
372     "panic action",
373     "msdfs root",
374     "host msdfs",
375     "winbind separator"]
376
377 def upgrade_smbconf(oldconf,mark):
378     """Remove configuration variables not present in Samba4
379
380     :param oldconf: Old configuration structure
381     :param mark: Whether removed configuration variables should be
382         kept in the new configuration as "samba3:<name>"
383     """
384     data = oldconf.data()
385     newconf = LoadParm()
386
387     for s in data:
388         for p in data[s]:
389             keep = False
390             for k in smbconf_keep:
391                 if smbconf_keep[k] == p:
392                     keep = True
393                     break
394
395             if keep:
396                 newconf.set(s, p, oldconf.get(s, p))
397             elif mark:
398                 newconf.set(s, "samba3:"+p, oldconf.get(s,p))
399
400     return newconf
401
402 SAMBA3_PREDEF_NAMES = {
403         'HKLM': registry.HKEY_LOCAL_MACHINE,
404 }
405
406 def import_registry(samba4_registry, samba3_regdb):
407     """Import a Samba 3 registry database into the Samba 4 registry.
408
409     :param samba4_registry: Samba 4 registry handle.
410     :param samba3_regdb: Samba 3 registry database handle.
411     """
412     def ensure_key_exists(keypath):
413         (predef_name, keypath) = keypath.split("/", 1)
414         predef_id = SAMBA3_PREDEF_NAMES[predef_name]
415         keypath = keypath.replace("/", "\\")
416         return samba4_registry.create_key(predef_id, keypath)
417
418     for key in samba3_regdb.keys():
419         key_handle = ensure_key_exists(key)
420         for subkey in samba3_regdb.subkeys(key):
421             ensure_key_exists(subkey)
422         for (value_name, (value_type, value_data)) in samba3_regdb.values(key).items():
423             key_handle.set_value(value_name, value_type, value_data)
424
425
426 def upgrade_from_samba3(samba3, logger, targetdir, session_info=None, useeadb=False):
427     """Upgrade from samba3 database to samba4 AD database
428
429     :param samba3: samba3 object
430     :param logger: Logger object
431     :param targetdir: samba4 database directory
432     :param session_info: Session information
433     """
434
435     if samba3.lp.get("domain logons"):
436         serverrole = "domain controller"
437     else:
438         if samba3.lp.get("security") == "user":
439             serverrole = "standalone"
440         else:
441             serverrole = "member server"
442
443     domainname = samba3.lp.get("workgroup")
444     realm = samba3.lp.get("realm")
445     netbiosname = samba3.lp.get("netbios name")
446
447     # secrets db
448     secrets_db = samba3.get_secrets_db()
449
450     if not domainname:
451         domainname = secrets_db.domains()[0]
452         logger.warning("No workgroup specified in smb.conf file, assuming '%s'",
453                 domainname)
454
455     if not realm:
456         if serverrole == "domain controller":
457             raise ProvisioningError("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 (it is the DNS name of the AD domain you wish to create.")
458         else:
459             realm = domainname.upper()
460             logger.warning("No realm specified in smb.conf file, assuming '%s'",
461                     realm)
462
463     # Find machine account and password
464     machinepass = None
465     machinerid = None
466     machinesid = None
467     next_rid = 1000
468
469     try:
470         machinepass = secrets_db.get_machine_password(netbiosname)
471     except:
472         pass
473
474     # We must close the direct pytdb database before the C code loads it
475     secrets_db.close()
476
477     # Connect to old password backend
478     passdb.set_secrets_dir(samba3.lp.get("private dir"))
479     s3db = samba3.get_sam_db()
480
481     # Get domain sid
482     try:
483         domainsid = passdb.get_global_sam_sid()
484     except passdb.error:
485         raise Exception("Can't find domain sid for '%s', Exiting." % domainname)
486
487     # Get machine account, sid, rid
488     try:
489         machineacct = s3db.getsampwnam('%s$' % netbiosname)
490         machinesid, machinerid = machineacct.user_sid.split()
491     except:
492         pass
493
494     # Export account policy
495     logger.info("Exporting account policy")
496     policy = s3db.get_account_policy()
497
498     # Export groups from old passdb backend
499     logger.info("Exporting groups")
500     grouplist = s3db.enum_group_mapping()
501     groupmembers = {}
502     for group in grouplist:
503         sid, rid = group.sid.split()
504         if sid == domainsid:
505             if rid >= next_rid:
506                next_rid = rid + 1
507
508         # Get members for each group/alias
509         if group.sid_name_use == lsa.SID_NAME_ALIAS:
510             members = s3db.enum_aliasmem(group.sid)
511         elif group.sid_name_use == lsa.SID_NAME_DOM_GRP:
512             try:
513                 members = s3db.enum_group_members(group.sid)
514             except:
515                 continue
516             groupmembers[group.nt_name] = members
517         elif group.sid_name_use == lsa.SID_NAME_WKN_GRP:
518             (group_dom_sid, rid) = group.sid.split()
519             if (group_dom_sid != security.dom_sid(security.SID_BUILTIN)):
520                 logger.warn("Ignoring 'well known' group '%s' (should already be in AD, and have no members)",
521                             group.nt_name)
522                 continue
523             # A number of buggy databases mix up well known groups and aliases.
524             members = s3db.enum_aliasmem(group.sid)
525         else:
526             logger.warn("Ignoring group '%s' with sid_name_use=%d",
527                         group.nt_name, group.sid_name_use)
528             continue
529
530
531     # Export users from old passdb backend
532     logger.info("Exporting users")
533     userlist = s3db.search_users(0)
534     userdata = {}
535     uids = {}
536     admin_user = None
537     for entry in userlist:
538         if machinerid and machinerid == entry['rid']:
539             continue
540         username = entry['account_name']
541         if entry['rid'] < 1000:
542             logger.info("  Skipping wellknown rid=%d (for username=%s)", entry['rid'], username)
543             continue
544         if entry['rid'] >= next_rid:
545             next_rid = entry['rid'] + 1
546
547         user = s3db.getsampwnam(username)
548         acct_type = (user.acct_ctrl & (samr.ACB_NORMAL|samr.ACB_WSTRUST|samr.ACB_SVRTRUST|samr.ACB_DOMTRUST))
549         if (acct_type == samr.ACB_NORMAL or acct_type == samr.ACB_WSTRUST or acct_type == samr.ACB_SVRTRUST):
550             pass
551         elif acct_type == samr.ACB_DOMTRUST:
552             logger.warn("  Skipping inter-domain trust from domain %s, this trust must be re-created as an AD trust" % username[:-1])
553             continue
554         elif acct_type == (samr.ACB_NORMAL|samr.ACB_WSTRUST) and username[-1] == '$':
555             logger.warn("  Fixing account %s which had both ACB_NORMAL (U) and ACB_WSTRUST (W) set.  Account will be marked as ACB_WSTRUST (W), i.e. as a domain member" % username)
556             user.acct_ctrl = (user.acct_ctrl & ~samr.ACB_NORMAL)
557         else:
558             raise ProvisioningError("""Failed to upgrade due to invalid account %s, account control flags 0x%08X must have exactly one of
559 ACB_NORMAL (N, 0x%08X), ACB_WSTRUST (W 0x%08X), ACB_SVRTRUST (S 0x%08X) or ACB_DOMTRUST (D 0x%08X).
560
561 Please fix this account before attempting to upgrade again
562 """
563                                     % (user.acct_flags, username,
564                                        samr.ACB_NORMAL, samr.ACB_WSTRUST, samr.ACB_SVRTRUST, samr.ACB_DOMTRUST))
565         
566         userdata[username] = user
567         try:
568             uids[username] = s3db.sid_to_id(user.user_sid)[0]
569         except:
570             try:
571                 uids[username] = pwd.getpwnam(username).pw_uid
572             except:
573                 pass
574
575         if not admin_user and username.lower() == 'root':
576             admin_user = username
577         if username.lower() == 'administrator':
578             admin_user = username
579
580     logger.info("Next rid = %d", next_rid)
581
582     # Do full provision
583     result = provision(logger, session_info, None,
584                        targetdir=targetdir, realm=realm, domain=domainname,
585                        domainsid=str(domainsid), next_rid=next_rid,
586                        dc_rid=machinerid,
587                        hostname=netbiosname, machinepass=machinepass,
588                        serverrole=serverrole, samdb_fill=FILL_FULL,
589                        useeadb=useeadb)
590
591     # Import WINS database
592     logger.info("Importing WINS database")
593     import_wins(Ldb(result.paths.winsdb), samba3.get_wins_db())
594
595     # Set Account policy
596     logger.info("Importing Account policy")
597     import_sam_policy(result.samdb, policy, logger)
598
599     # Migrate IDMAP database
600     logger.info("Importing idmap database")
601     import_idmap(result.idmap, samba3, logger)
602
603     # Set the s3 context for samba4 configuration
604     new_lp_ctx = s3param.get_context()
605     new_lp_ctx.load(result.lp.configfile)
606     new_lp_ctx.set("private dir", result.lp.get("private dir"))
607     new_lp_ctx.set("state directory", result.lp.get("state directory"))
608     new_lp_ctx.set("lock directory", result.lp.get("lock directory"))
609
610     # Connect to samba4 backend
611     s4_passdb = passdb.PDB(new_lp_ctx.get("passdb backend"))
612
613     # Export groups to samba4 backend
614     logger.info("Importing groups")
615     for g in grouplist:
616         # Ignore uninitialized groups (gid = -1)
617         if g.gid != 0xffffffff:
618             add_idmap_entry(result.idmap, g.sid, g.gid, "GID", logger)
619             add_group_from_mapping_entry(result.samdb, g, logger)
620
621     # Export users to samba4 backend
622     logger.info("Importing users")
623     for username in userdata:
624         if username.lower() == 'administrator' or username.lower() == 'root':
625             continue
626         s4_passdb.add_sam_account(userdata[username])
627         if username in uids:
628             add_idmap_entry(result.idmap, userdata[username].user_sid, uids[username], "UID", logger)
629
630     logger.info("Adding users to groups")
631     for g in grouplist:
632         if g.nt_name in groupmembers:
633             add_users_to_group(result.samdb, g, groupmembers[g.nt_name], logger)
634
635     # Set password for administrator
636     if admin_user:
637         logger.info("Setting password for administrator")
638         admin_userdata = s4_passdb.getsampwnam("administrator")
639         admin_userdata.nt_passwd = userdata[admin_user].nt_passwd
640         if userdata[admin_user].lanman_passwd:
641             admin_userdata.lanman_passwd = userdata[admin_user].lanman_passwd
642         admin_userdata.pass_last_set_time = userdata[admin_user].pass_last_set_time
643         if userdata[admin_user].pw_history:
644             admin_userdata.pw_history = userdata[admin_user].pw_history
645         s4_passdb.update_sam_account(admin_userdata)
646         logger.info("Administrator password has been set to password of user '%s'", admin_user)
647
648     # FIXME: import_registry(registry.Registry(), samba3.get_registry())
649     # FIXME: shares