s4-provision handle a number of invalid but real-world upgrade cases
[amitay/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_REPLACE, 'member')
207
208         try:
209             samdb.modify(m)
210         except ldb.LdbError, e:
211             logger.warn("Could not add member to group '%s'", groupmap.nt_name)
212
213
214 def import_wins(samba4_winsdb, samba3_winsdb):
215     """Import settings from a Samba3 WINS database.
216
217     :param samba4_winsdb: WINS database to import to
218     :param samba3_winsdb: WINS database to import from
219     """
220     version_id = 0
221
222     for (name, (ttl, ips, nb_flags)) in samba3_winsdb.items():
223         version_id+=1
224
225         type = int(name.split("#", 1)[1], 16)
226
227         if type == 0x1C:
228             rType = 0x2
229         elif type & 0x80:
230             if len(ips) > 1:
231                 rType = 0x2
232             else:
233                 rType = 0x1
234         else:
235             if len(ips) > 1:
236                 rType = 0x3
237             else:
238                 rType = 0x0
239
240         if ttl > time.time():
241             rState = 0x0 # active
242         else:
243             rState = 0x1 # released
244
245         nType = ((nb_flags & 0x60)>>5)
246
247         samba4_winsdb.add({"dn": "name=%s,type=0x%s" % tuple(name.split("#")),
248                            "type": name.split("#")[1],
249                            "name": name.split("#")[0],
250                            "objectClass": "winsRecord",
251                            "recordType": str(rType),
252                            "recordState": str(rState),
253                            "nodeType": str(nType),
254                            "expireTime": ldb.timestring(ttl),
255                            "isStatic": "0",
256                            "versionID": str(version_id),
257                            "address": ips})
258
259     samba4_winsdb.add({"dn": "cn=VERSION",
260                        "cn": "VERSION",
261                        "objectClass": "winsMaxVersion",
262                        "maxVersion": str(version_id)})
263
264 def enable_samba3sam(samdb, ldapurl):
265     """Enable Samba 3 LDAP URL database.
266
267     :param samdb: SAM Database.
268     :param ldapurl: Samba 3 LDAP URL
269     """
270     samdb.modify_ldif("""
271 dn: @MODULES
272 changetype: modify
273 replace: @LIST
274 @LIST: samldb,operational,objectguid,rdn_name,samba3sam
275 """)
276
277     samdb.add({"dn": "@MAP=samba3sam", "@MAP_URL": ldapurl})
278
279
280 smbconf_keep = [
281     "dos charset",
282     "unix charset",
283     "display charset",
284     "comment",
285     "path",
286     "directory",
287     "workgroup",
288     "realm",
289     "netbios name",
290     "netbios aliases",
291     "netbios scope",
292     "server string",
293     "interfaces",
294     "bind interfaces only",
295     "security",
296     "auth methods",
297     "encrypt passwords",
298     "null passwords",
299     "obey pam restrictions",
300     "password server",
301     "smb passwd file",
302     "private dir",
303     "passwd chat",
304     "password level",
305     "lanman auth",
306     "ntlm auth",
307     "client NTLMv2 auth",
308     "client lanman auth",
309     "client plaintext auth",
310     "read only",
311     "hosts allow",
312     "hosts deny",
313     "log level",
314     "debuglevel",
315     "log file",
316     "smb ports",
317     "large readwrite",
318     "max protocol",
319     "min protocol",
320     "unicode",
321     "read raw",
322     "write raw",
323     "disable netbios",
324     "nt status support",
325     "max mux",
326     "max xmit",
327     "name resolve order",
328     "max wins ttl",
329     "min wins ttl",
330     "time server",
331     "unix extensions",
332     "use spnego",
333     "server signing",
334     "client signing",
335     "max connections",
336     "paranoid server security",
337     "socket options",
338     "strict sync",
339     "max print jobs",
340     "printable",
341     "print ok",
342     "printer name",
343     "printer",
344     "map system",
345     "map hidden",
346     "map archive",
347     "preferred master",
348     "prefered master",
349     "local master",
350     "browseable",
351     "browsable",
352     "wins server",
353     "wins support",
354     "csc policy",
355     "strict locking",
356     "preload",
357     "auto services",
358     "lock dir",
359     "lock directory",
360     "pid directory",
361     "socket address",
362     "copy",
363     "include",
364     "available",
365     "volume",
366     "fstype",
367     "panic action",
368     "msdfs root",
369     "host msdfs",
370     "winbind separator"]
371
372 def upgrade_smbconf(oldconf,mark):
373     """Remove configuration variables not present in Samba4
374
375     :param oldconf: Old configuration structure
376     :param mark: Whether removed configuration variables should be
377         kept in the new configuration as "samba3:<name>"
378     """
379     data = oldconf.data()
380     newconf = LoadParm()
381
382     for s in data:
383         for p in data[s]:
384             keep = False
385             for k in smbconf_keep:
386                 if smbconf_keep[k] == p:
387                     keep = True
388                     break
389
390             if keep:
391                 newconf.set(s, p, oldconf.get(s, p))
392             elif mark:
393                 newconf.set(s, "samba3:"+p, oldconf.get(s,p))
394
395     return newconf
396
397 SAMBA3_PREDEF_NAMES = {
398         'HKLM': registry.HKEY_LOCAL_MACHINE,
399 }
400
401 def import_registry(samba4_registry, samba3_regdb):
402     """Import a Samba 3 registry database into the Samba 4 registry.
403
404     :param samba4_registry: Samba 4 registry handle.
405     :param samba3_regdb: Samba 3 registry database handle.
406     """
407     def ensure_key_exists(keypath):
408         (predef_name, keypath) = keypath.split("/", 1)
409         predef_id = SAMBA3_PREDEF_NAMES[predef_name]
410         keypath = keypath.replace("/", "\\")
411         return samba4_registry.create_key(predef_id, keypath)
412
413     for key in samba3_regdb.keys():
414         key_handle = ensure_key_exists(key)
415         for subkey in samba3_regdb.subkeys(key):
416             ensure_key_exists(subkey)
417         for (value_name, (value_type, value_data)) in samba3_regdb.values(key).items():
418             key_handle.set_value(value_name, value_type, value_data)
419
420
421 def upgrade_from_samba3(samba3, logger, targetdir, session_info=None, useeadb=False):
422     """Upgrade from samba3 database to samba4 AD database
423
424     :param samba3: samba3 object
425     :param logger: Logger object
426     :param targetdir: samba4 database directory
427     :param session_info: Session information
428     """
429
430     if samba3.lp.get("domain logons"):
431         serverrole = "domain controller"
432     else:
433         if samba3.lp.get("security") == "user":
434             serverrole = "standalone"
435         else:
436             serverrole = "member server"
437
438     domainname = samba3.lp.get("workgroup")
439     realm = samba3.lp.get("realm")
440     netbiosname = samba3.lp.get("netbios name")
441
442     # secrets db
443     secrets_db = samba3.get_secrets_db()
444
445     if not domainname:
446         domainname = secrets_db.domains()[0]
447         logger.warning("No workgroup specified in smb.conf file, assuming '%s'",
448                 domainname)
449
450     if not realm:
451         if serverrole == "domain controller":
452             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.")
453         else:
454             realm = domainname.upper()
455             logger.warning("No realm specified in smb.conf file, assuming '%s'",
456                     realm)
457
458     # Find machine account and password
459     machinepass = None
460     machinerid = None
461     machinesid = None
462     next_rid = 1000
463
464     try:
465         machinepass = secrets_db.get_machine_password(netbiosname)
466     except:
467         pass
468
469     # We must close the direct pytdb database before the C code loads it
470     secrets_db.close()
471
472     # Connect to old password backend
473     passdb.set_secrets_dir(samba3.lp.get("private dir"))
474     s3db = samba3.get_sam_db()
475
476     # Get domain sid
477     try:
478         domainsid = passdb.get_global_sam_sid()
479     except passdb.error:
480         raise Exception("Can't find domain sid for '%s', Exiting." % domainname)
481
482     # Get machine account, sid, rid
483     try:
484         machineacct = s3db.getsampwnam('%s$' % netbiosname)
485         machinesid, machinerid = machineacct.user_sid.split()
486     except:
487         pass
488
489     # Export account policy
490     logger.info("Exporting account policy")
491     policy = s3db.get_account_policy()
492
493     # Export groups from old passdb backend
494     logger.info("Exporting groups")
495     grouplist = s3db.enum_group_mapping()
496     groupmembers = {}
497     for group in grouplist:
498         sid, rid = group.sid.split()
499         if sid == domainsid:
500             if rid >= next_rid:
501                next_rid = rid + 1
502
503         # Get members for each group/alias
504         if group.sid_name_use == lsa.SID_NAME_ALIAS:
505             members = s3db.enum_aliasmem(group.sid)
506         elif group.sid_name_use == lsa.SID_NAME_DOM_GRP:
507             try:
508                 members = s3db.enum_group_members(group.sid)
509             except:
510                 continue
511             groupmembers[group.nt_name] = members
512         elif group.sid_name_use == lsa.SID_NAME_WKN_GRP:
513             (group_dom_sid, rid) = group.sid.split()
514             if (group_dom_sid != security.dom_sid(security.SID_BUILTIN)):
515                 logger.warn("Ignoring 'well known' group '%s' (should already be in AD, and have no members)",
516                             group.nt_name)
517                 continue
518             # A number of buggy databases mix up well known groups and aliases.
519             members = s3db.enum_aliasmem(group.sid)
520         else:
521             logger.warn("Ignoring group '%s' with sid_name_use=%d",
522                         group.nt_name, group.sid_name_use)
523             continue
524
525
526     # Export users from old passdb backend
527     logger.info("Exporting users")
528     userlist = s3db.search_users(0)
529     userdata = {}
530     uids = {}
531     admin_user = None
532     for entry in userlist:
533         if machinerid and machinerid == entry['rid']:
534             continue
535         username = entry['account_name']
536         if entry['rid'] < 1000:
537             logger.info("  Skipping wellknown rid=%d (for username=%s)", entry['rid'], username)
538             continue
539         if entry['rid'] >= next_rid:
540             next_rid = entry['rid'] + 1
541
542         user = s3db.getsampwnam(username)
543         acct_type = (user.acct_ctrl & (samr.ACB_NORMAL|samr.ACB_WSTRUST|samr.ACB_SVRTRUST|samr.ACB_DOMTRUST))
544         if (acct_type == samr.ACB_NORMAL or acct_type == samr.ACB_WSTRUST or acct_type == samr.ACB_SVRTRUST):
545             pass
546         elif acct_type == samr.ACB_DOMTRUST:
547             logger.warn("  Skipping inter-domain trust from domain %s, this trust must be re-created as an AD trust" % username[:-1])
548             continue
549         elif acct_type == (samr.ACB_NORMAL|samr.ACB_WSTRUST) and username[-1] == '$':
550             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)
551             user.acct_ctrl = (user.acct_ctrl & ~samr.ACB_NORMAL)
552         else:
553             raise ProvisioningError("""Failed to upgrade due to invalid account %s, account control flags 0x%08X must have exactly one of
554 ACB_NORMAL (N, 0x%08X), ACB_WSTRUST (W 0x%08X), ACB_SVRTRUST (S 0x%08X) or ACB_DOMTRUST (D 0x%08X).
555
556 Please fix this account before attempting to upgrade again
557 """
558                                     % (user.acct_flags, username,
559                                        samr.ACB_NORMAL, samr.ACB_WSTRUST, samr.ACB_SVRTRUST, samr.ACB_DOMTRUST))
560         
561         userdata[username] = user
562         try:
563             uids[username] = s3db.sid_to_id(user.user_sid)[0]
564         except:
565             try:
566                 uids[username] = pwd.getpwnam(username).pw_uid
567             except:
568                 pass
569
570         if not admin_user and username.lower() == 'root':
571             admin_user = username
572         if username.lower() == 'administrator':
573             admin_user = username
574
575     logger.info("Next rid = %d", next_rid)
576
577     # Do full provision
578     result = provision(logger, session_info, None,
579                        targetdir=targetdir, realm=realm, domain=domainname,
580                        domainsid=str(domainsid), next_rid=next_rid,
581                        dc_rid=machinerid,
582                        hostname=netbiosname, machinepass=machinepass,
583                        serverrole=serverrole, samdb_fill=FILL_FULL,
584                        useeadb=useeadb)
585
586     # Import WINS database
587     logger.info("Importing WINS database")
588     import_wins(Ldb(result.paths.winsdb), samba3.get_wins_db())
589
590     # Set Account policy
591     logger.info("Importing Account policy")
592     import_sam_policy(result.samdb, policy, logger)
593
594     # Migrate IDMAP database
595     logger.info("Importing idmap database")
596     import_idmap(result.idmap, samba3, logger)
597
598     # Set the s3 context for samba4 configuration
599     new_lp_ctx = s3param.get_context()
600     new_lp_ctx.load(result.lp.configfile)
601     new_lp_ctx.set("private dir", result.lp.get("private dir"))
602     new_lp_ctx.set("state directory", result.lp.get("state directory"))
603     new_lp_ctx.set("lock directory", result.lp.get("lock directory"))
604
605     # Connect to samba4 backend
606     s4_passdb = passdb.PDB(new_lp_ctx.get("passdb backend"))
607
608     # Export groups to samba4 backend
609     logger.info("Importing groups")
610     for g in grouplist:
611         # Ignore uninitialized groups (gid = -1)
612         if g.gid != 0xffffffff:
613             add_idmap_entry(result.idmap, g.sid, g.gid, "GID", logger)
614             add_group_from_mapping_entry(result.samdb, g, logger)
615
616     # Export users to samba4 backend
617     logger.info("Importing users")
618     for username in userdata:
619         if username.lower() == 'administrator' or username.lower() == 'root':
620             continue
621         s4_passdb.add_sam_account(userdata[username])
622         if username in uids:
623             add_idmap_entry(result.idmap, userdata[username].user_sid, uids[username], "UID", logger)
624
625     logger.info("Adding users to groups")
626     for g in grouplist:
627         if g.nt_name in groupmembers:
628             add_users_to_group(result.samdb, g, groupmembers[g.nt_name], logger)
629
630     # Set password for administrator
631     if admin_user:
632         logger.info("Setting password for administrator")
633         admin_userdata = s4_passdb.getsampwnam("administrator")
634         admin_userdata.nt_passwd = userdata[admin_user].nt_passwd
635         if userdata[admin_user].lanman_passwd:
636             admin_userdata.lanman_passwd = userdata[admin_user].lanman_passwd
637         admin_userdata.pass_last_set_time = userdata[admin_user].pass_last_set_time
638         if userdata[admin_user].pw_history:
639             admin_userdata.pw_history = userdata[admin_user].pw_history
640         s4_passdb.update_sam_account(admin_userdata)
641         logger.info("Administrator password has been set to password of user '%s'", admin_user)
642
643     # FIXME: import_registry(registry.Registry(), samba3.get_registry())
644     # FIXME: shares