s4-provision Add support for fixing the DC rid to a particular value
[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
33 def import_sam_policy(samldb, policy, dn):
34     """Import a Samba 3 policy database."""
35     samldb.modify_ldif("""
36 dn: %s
37 changetype: modify
38 replace: minPwdLength
39 minPwdLength: %d
40 pwdHistoryLength: %d
41 minPwdAge: %d
42 maxPwdAge: %d
43 lockoutDuration: %d
44 samba3ResetCountMinutes: %d
45 samba3UserMustLogonToChangePassword: %d
46 samba3BadLockoutMinutes: %d
47 samba3DisconnectTime: %d
48
49 """ % (dn, policy.min_password_length,
50     policy.password_history, policy.minimum_password_age,
51     policy.maximum_password_age, policy.lockout_duration,
52     policy.reset_count_minutes, policy.user_must_logon_to_change_password,
53     policy.bad_lockout_minutes, policy.disconnect_time))
54
55
56 def import_sam_account(samldb,acc,domaindn,domainsid):
57     """Import a Samba 3 SAM account.
58
59     :param samldb: Samba 4 SAM Database handle
60     :param acc: Samba 3 account
61     :param domaindn: Domain DN
62     :param domainsid: Domain SID."""
63     if acc.nt_username is None or acc.nt_username == "":
64         acc.nt_username = acc.username
65
66     if acc.fullname is None:
67         try:
68             acc.fullname = pwd.getpwnam(acc.username)[4].split(",")[0]
69         except KeyError:
70             pass
71
72     if acc.fullname is None:
73         acc.fullname = acc.username
74
75     assert acc.fullname is not None
76     assert acc.nt_username is not None
77
78     samldb.add({
79         "dn": "cn=%s,%s" % (acc.fullname, domaindn),
80         "objectClass": ["top", "user"],
81         "lastLogon": str(acc.logon_time),
82         "lastLogoff": str(acc.logoff_time),
83         "unixName": acc.username,
84         "sAMAccountName": acc.nt_username,
85         "cn": acc.nt_username,
86         "description": acc.acct_desc,
87         "primaryGroupID": str(acc.group_rid),
88         "badPwdcount": str(acc.bad_password_count),
89         "logonCount": str(acc.logon_count),
90         "samba3Domain": acc.domain,
91         "samba3DirDrive": acc.dir_drive,
92         "samba3MungedDial": acc.munged_dial,
93         "samba3Homedir": acc.homedir,
94         "samba3LogonScript": acc.logon_script,
95         "samba3ProfilePath": acc.profile_path,
96         "samba3Workstations": acc.workstations,
97         "samba3KickOffTime": str(acc.kickoff_time),
98         "samba3BadPwdTime": str(acc.bad_password_time),
99         "samba3PassLastSetTime": str(acc.pass_last_set_time),
100         "samba3PassCanChangeTime": str(acc.pass_can_change_time),
101         "samba3PassMustChangeTime": str(acc.pass_must_change_time),
102         "objectSid": "%s-%d" % (domainsid, acc.user_rid),
103         "lmPwdHash:": acc.lm_password,
104         "ntPwdHash:": acc.nt_password,
105         })
106
107
108 def import_sam_group(samldb, sid, gid, sid_name_use, nt_name, comment, domaindn):
109     """Upgrade a SAM group.
110
111     :param samldb: SAM database.
112     :param gid: Group GID
113     :param sid_name_use: SID name use
114     :param nt_name: NT Group Name
115     :param comment: NT Group Comment
116     :param domaindn: Domain DN
117     """
118
119     if sid_name_use == 5: # Well-known group
120         return None
121
122     if nt_name in ("Domain Guests", "Domain Users", "Domain Admins"):
123         return None
124
125     if gid == -1:
126         gr = grp.getgrnam(nt_name)
127     else:
128         gr = grp.getgrgid(gid)
129
130     if gr is None:
131         unixname = "UNKNOWN"
132     else:
133         unixname = gr.gr_name
134
135     assert unixname is not None
136
137     samldb.add({
138         "dn": "cn=%s,%s" % (nt_name, domaindn),
139         "objectClass": ["top", "group"],
140         "description": comment,
141         "cn": nt_name,
142         "objectSid": sid,
143         "unixName": unixname,
144         "samba3SidNameUse": str(sid_name_use)
145         })
146
147
148 def import_idmap(samdb,samba3_idmap,domaindn):
149     """Import idmap data.
150
151     :param samdb: SamDB handle.
152     :param samba3_idmap: Samba 3 IDMAP database to import from
153     :param domaindn: Domain DN.
154     """
155     samdb.add({
156         "dn": domaindn,
157         "userHwm": str(samba3_idmap.get_user_hwm()),
158         "groupHwm": str(samba3_idmap.get_group_hwm())})
159
160     for uid in samba3_idmap.uids():
161         samdb.add({"dn": "SID=%s,%s" % (samba3_idmap.get_user_sid(uid), domaindn),
162                           "SID": samba3_idmap.get_user_sid(uid),
163                           "type": "user",
164                           "unixID": str(uid)})
165
166     for gid in samba3_idmap.uids():
167         samdb.add({"dn": "SID=%s,%s" % (samba3_idmap.get_group_sid(gid), domaindn),
168                           "SID": samba3_idmap.get_group_sid(gid),
169                           "type": "group",
170                           "unixID": str(gid)})
171
172
173 def import_wins(samba4_winsdb, samba3_winsdb):
174     """Import settings from a Samba3 WINS database.
175
176     :param samba4_winsdb: WINS database to import to
177     :param samba3_winsdb: WINS database to import from
178     """
179     version_id = 0
180
181     for (name, (ttl, ips, nb_flags)) in samba3_winsdb.items():
182         version_id+=1
183
184         type = int(name.split("#", 1)[1], 16)
185
186         if type == 0x1C:
187             rType = 0x2
188         elif type & 0x80:
189             if len(ips) > 1:
190                 rType = 0x2
191             else:
192                 rType = 0x1
193         else:
194             if len(ips) > 1:
195                 rType = 0x3
196             else:
197                 rType = 0x0
198
199         if ttl > time.time():
200             rState = 0x0 # active
201         else:
202             rState = 0x1 # released
203
204         nType = ((nb_flags & 0x60)>>5)
205
206         samba4_winsdb.add({"dn": "name=%s,type=0x%s" % tuple(name.split("#")),
207                            "type": name.split("#")[1],
208                            "name": name.split("#")[0],
209                            "objectClass": "winsRecord",
210                            "recordType": str(rType),
211                            "recordState": str(rState),
212                            "nodeType": str(nType),
213                            "expireTime": ldb.timestring(ttl),
214                            "isStatic": "0",
215                            "versionID": str(version_id),
216                            "address": ips})
217
218     samba4_winsdb.add({"dn": "cn=VERSION",
219                        "cn": "VERSION",
220                        "objectClass": "winsMaxVersion",
221                        "maxVersion": str(version_id)})
222
223 def enable_samba3sam(samdb, ldapurl):
224     """Enable Samba 3 LDAP URL database.
225
226     :param samdb: SAM Database.
227     :param ldapurl: Samba 3 LDAP URL
228     """
229     samdb.modify_ldif("""
230 dn: @MODULES
231 changetype: modify
232 replace: @LIST
233 @LIST: samldb,operational,objectguid,rdn_name,samba3sam
234 """)
235
236     samdb.add({"dn": "@MAP=samba3sam", "@MAP_URL": ldapurl})
237
238
239 smbconf_keep = [
240     "dos charset",
241     "unix charset",
242     "display charset",
243     "comment",
244     "path",
245     "directory",
246     "workgroup",
247     "realm",
248     "netbios name",
249     "netbios aliases",
250     "netbios scope",
251     "server string",
252     "interfaces",
253     "bind interfaces only",
254     "security",
255     "auth methods",
256     "encrypt passwords",
257     "null passwords",
258     "obey pam restrictions",
259     "password server",
260     "smb passwd file",
261     "private dir",
262     "passwd chat",
263     "password level",
264     "lanman auth",
265     "ntlm auth",
266     "client NTLMv2 auth",
267     "client lanman auth",
268     "client plaintext auth",
269     "read only",
270     "hosts allow",
271     "hosts deny",
272     "log level",
273     "debuglevel",
274     "log file",
275     "smb ports",
276     "large readwrite",
277     "max protocol",
278     "min protocol",
279     "unicode",
280     "read raw",
281     "write raw",
282     "disable netbios",
283     "nt status support",
284     "max mux",
285     "max xmit",
286     "name resolve order",
287     "max wins ttl",
288     "min wins ttl",
289     "time server",
290     "unix extensions",
291     "use spnego",
292     "server signing",
293     "client signing",
294     "max connections",
295     "paranoid server security",
296     "socket options",
297     "strict sync",
298     "max print jobs",
299     "printable",
300     "print ok",
301     "printer name",
302     "printer",
303     "map system",
304     "map hidden",
305     "map archive",
306     "preferred master",
307     "prefered master",
308     "local master",
309     "browseable",
310     "browsable",
311     "wins server",
312     "wins support",
313     "csc policy",
314     "strict locking",
315     "preload",
316     "auto services",
317     "lock dir",
318     "lock directory",
319     "pid directory",
320     "socket address",
321     "copy",
322     "include",
323     "available",
324     "volume",
325     "fstype",
326     "panic action",
327     "msdfs root",
328     "host msdfs",
329     "winbind separator"]
330
331 def upgrade_smbconf(oldconf,mark):
332     """Remove configuration variables not present in Samba4
333
334     :param oldconf: Old configuration structure
335     :param mark: Whether removed configuration variables should be
336         kept in the new configuration as "samba3:<name>"
337     """
338     data = oldconf.data()
339     newconf = LoadParm()
340
341     for s in data:
342         for p in data[s]:
343             keep = False
344             for k in smbconf_keep:
345                 if smbconf_keep[k] == p:
346                     keep = True
347                     break
348
349             if keep:
350                 newconf.set(s, p, oldconf.get(s, p))
351             elif mark:
352                 newconf.set(s, "samba3:"+p, oldconf.get(s,p))
353
354     return newconf
355
356 SAMBA3_PREDEF_NAMES = {
357         'HKLM': registry.HKEY_LOCAL_MACHINE,
358 }
359
360 def import_registry(samba4_registry, samba3_regdb):
361     """Import a Samba 3 registry database into the Samba 4 registry.
362
363     :param samba4_registry: Samba 4 registry handle.
364     :param samba3_regdb: Samba 3 registry database handle.
365     """
366     def ensure_key_exists(keypath):
367         (predef_name, keypath) = keypath.split("/", 1)
368         predef_id = SAMBA3_PREDEF_NAMES[predef_name]
369         keypath = keypath.replace("/", "\\")
370         return samba4_registry.create_key(predef_id, keypath)
371
372     for key in samba3_regdb.keys():
373         key_handle = ensure_key_exists(key)
374         for subkey in samba3_regdb.subkeys(key):
375             ensure_key_exists(subkey)
376         for (value_name, (value_type, value_data)) in samba3_regdb.values(key).items():
377             key_handle.set_value(value_name, value_type, value_data)
378
379
380 def upgrade_from_samba3(samba3, logger, session_info, smbconf, targetdir):
381     """Upgrade from samba3 database to samba4 AD database
382     """
383
384     # Read samba3 smb.conf
385     oldconf = s3param.get_context();
386     oldconf.load(smbconf)
387
388     if oldconf.get("domain logons"):
389         serverrole = "domain controller"
390     else:
391         if oldconf.get("security") == "user":
392             serverrole = "standalone"
393         else:
394             serverrole = "member server"
395
396     domainname = oldconf.get("workgroup")
397     realm = oldconf.get("realm")
398     netbiosname = oldconf.get("netbios name")
399
400     # secrets db
401     secrets_db = samba3.get_secrets_db()
402
403     if not domainname:
404         domainname = secrets_db.domains()[0]
405         logger.warning("No domain specified in smb.conf file, assuming '%s'",
406                 domainname)
407
408     if not realm:
409         if oldconf.get("domain logons"):
410             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).")
411             return
412         else:
413             realm = domainname.upper()
414             logger.warning("No realm specified in smb.conf file, assuming '%s'",
415                     realm)
416
417     # Find machine account and password
418     machinepass = None
419     machinerid = None
420     machinesid = None
421     next_rid = 1000
422
423     try:
424         machinepass = secrets_db.get_machine_password(netbiosname)
425     except:
426         pass
427
428     # We must close the direct pytdb database before the C code loads it
429     secrets_db.close()
430
431     # We must load the group mapping into memory before the passdb code touches it
432     groupdb = samba3.get_groupmapping_db()
433     for sid in groupdb.groupsids():
434         (gid, sid_name_use, nt_name, comment) = groupdb.get_group(sid)
435         # FIXME: import_sam_group(samdb, sid, gid, sid_name_use, nt_name, comment, domaindn)
436     groupdb.close()
437
438     passdb.set_secrets_dir(samba3.libdir)
439
440     try:
441         domainsid = str(passdb.get_global_sam_sid())
442     except:
443         pass
444
445     try:
446         machineacct = old_passdb.getsampwnam('%s$' % netbiosname)
447         machinesid, machinerid = machineacct.user_sid.split()
448     except:
449         pass
450
451     if domainsid is None:
452         logger.warning("Can't find domain secrets for '%s'; using random SID",
453             domainname)
454
455     # Import users from old passdb backend
456     old_passdb = passdb.PDB(oldconf.get('passdb backend'))
457     userlist = old_passdb.search_users(0)
458     userdata = {}
459     for entry in userlist:
460         if machinerid and machinerid == entry['rid']:
461             continue
462         username = entry['account_name']
463         if entry['rid'] < 1000:
464             print("Skipping wellknown rid=%d (for username=%s)\n" % (entry['rid'], username))
465             continue
466         if entry['rid'] >= next_rid:
467             next_rid = entry['rid'] + 1
468         
469         userdata[username] = old_passdb.getsampwnam(username)
470
471     # Do full provision
472     result = provision(logger, session_info, None,
473                        targetdir=targetdir, realm=realm, domain=domainname,
474                        domainsid=domainsid, next_rid=next_rid,
475                        dc_rid=machinerid,
476                        hostname=netbiosname, machinepass=machinepass,
477                        serverrole=serverrole, samdb_fill=FILL_FULL)
478
479     import_wins(Ldb(result.paths.winsdb), samba3.get_wins_db())
480
481     # FIXME: import_registry(registry.Registry(), samba3.get_registry())
482
483     # FIXME: import_idmap(samdb,samba3.get_idmap_db(),domaindn)
484
485     # FIXME: Aliases
486
487     # Export users to samba4 backend
488     new_smbconf = result.lp.configfile
489     newconf = s3param.get_context()
490     newconf.load(new_smbconf)
491
492     new_passdb = passdb.PDB('samba4')
493
494     for username in userdata:
495         print "adding user %s" % username
496         new_passdb.add_sam_account(userdata[username])