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