r26506: Start running (really trivial) tests for upgrade script.
[amitay/samba.git] / source4 / scripting / python / samba / upgrade.py
1 #!/usr/bin/python
2 #
3 #       backend code for upgrading from Samba3
4 #       Copyright Jelmer Vernooij 2005-2007
5 #       Released under the GNU GPL v3 or later
6 #
7
8 """Support code for upgrading from Samba 3 to Samba 4."""
9
10 from provision import findnss
11 import provision
12 import grp
13 import pwd
14 import uuid
15
16 def regkey_to_dn(name):
17     """Convert a registry key to a DN."""
18     dn = "hive=NONE"
19
20     if name == "":
21         return dn
22
23     for el in name.split("/"):
24         dn = "key=%s," % el + dn
25
26     return dn
27
28 # Where prefix is any of:
29 # - HKLM
30 #   HKU
31 #   HKCR
32 #   HKPD
33 #   HKPT
34 #
35
36 def upgrade_registry(regdb,prefix,ldb):
37     """Migrate registry contents."""
38     assert regdb is not None
39     prefix_up = prefix.upper()
40     ldif = []
41
42     for rk in regdb.keys:
43         pts = rk.name.split("/")
44
45         # Only handle selected hive
46         if pts[0].upper() != prefix_up:
47             continue
48
49         keydn = regkey_to_dn(rk.name)
50
51         pts = rk.name.split("/")
52
53         # Convert key name to dn
54         ldif[rk.name] = """
55 dn: %s
56 name: %s
57
58 """ % (keydn, pts[0])
59         
60         for rv in rk.values:
61             ldif[rk.name + " (" + rv.name + ")"] = """
62 dn: %s,value=%s
63 value: %s
64 type: %d
65 data:: %s""" % (keydn, rv.name, rv.name, rv.type, ldb.encode(rv.data))
66
67     return ldif
68
69 def upgrade_sam_policy(samba3,dn):
70     ldif = """
71 dn: %s
72 changetype: modify
73 replace: minPwdLength
74 minPwdLength: %d
75 pwdHistoryLength: %d
76 minPwdAge: %d
77 maxPwdAge: %d
78 lockoutDuration: %d
79 samba3ResetCountMinutes: %d
80 samba3UserMustLogonToChangePassword: %d
81 samba3BadLockoutMinutes: %d
82 samba3DisconnectTime: %d
83
84 """ % (dn, samba3.policy.min_password_length, 
85     samba3.policy.password_history, samba3.policy.minimum_password_age,
86     samba3.policy.maximum_password_age, samba3.policy.lockout_duration,
87     samba3.policy.reset_count_minutes, samba3.policy.user_must_logon_to_change_password,
88     samba3.policy.bad_lockout_minutes, samba3.policy.disconnect_time)
89     
90     return ldif
91
92 def upgrade_sam_account(ldb,acc,domaindn,domainsid):
93     """Upgrade a SAM account."""
94     if acc.nt_username is None or acc.nt_username == "":
95         acc.nt_username = acc.username
96
97     if acc.fullname is None:
98         acc.fullname = pwd.getpwnam(acc.fullname)[4]
99
100     acc.fullname = acc.fullname.split(",")[0]
101
102     if acc.fullname is None:
103         acc.fullname = acc.username
104     
105     assert acc.fullname is not None
106     assert acc.nt_username is not None
107
108     ldif = """dn: cn=%s,%s
109 objectClass: top
110 objectClass: user
111 lastLogon: %d
112 lastLogoff: %d
113 unixName: %s
114 sAMAccountName: %s
115 cn: %s
116 description: %s
117 primaryGroupID: %d
118 badPwdcount: %d
119 logonCount: %d
120 samba3Domain: %s
121 samba3DirDrive: %s
122 samba3MungedDial: %s
123 samba3Homedir: %s
124 samba3LogonScript: %s
125 samba3ProfilePath: %s
126 samba3Workstations: %s
127 samba3KickOffTime: %d
128 samba3BadPwdTime: %d
129 samba3PassLastSetTime: %d
130 samba3PassCanChangeTime: %d
131 samba3PassMustChangeTime: %d
132 objectSid: %s-%d
133 lmPwdHash:: %s
134 ntPwdHash:: %s
135
136 """ % (ldb.dn_escape(acc.fullname), domaindn, acc.logon_time, acc.logoff_time, acc.username, acc.nt_username, acc.nt_username, 
137 acc.acct_desc, acc.group_rid, acc.bad_password_count, acc.logon_count,
138 acc.domain, acc.dir_drive, acc.munged_dial, acc.homedir, acc.logon_script, 
139 acc.profile_path, acc.workstations, acc.kickoff_time, acc.bad_password_time, 
140 acc.pass_last_set_time, acc.pass_can_change_time, acc.pass_must_change_time, domainsid, acc.user_rid,
141     ldb.encode(acc.lm_pw), ldb.encode(acc.nt_pw))
142
143     return ldif
144
145 def upgrade_sam_group(group,domaindn):
146     """Upgrade a SAM group."""
147     if group.sid_name_use == 5: # Well-known group
148         return None
149
150     if group.nt_name in ("Domain Guests", "Domain Users", "Domain Admins"):
151         return None
152     
153     if group.gid == -1:
154         gr = grp.getgrnam(grp.nt_name)
155     else:
156         gr = grp.getgrgid(grp.gid)
157
158     if gr is None:
159         group.unixname = "UNKNOWN"
160     else:
161         group.unixname = gr.gr_name
162
163     assert group.unixname is not None
164     
165     ldif = """dn: cn=%s,%s
166 objectClass: top
167 objectClass: group
168 description: %s
169 cn: %s
170 objectSid: %s
171 unixName: %s
172 samba3SidNameUse: %d
173 """ % (group.nt_name, domaindn, 
174 group.comment, group.nt_name, group.sid, group.unixname, group.sid_name_use)
175
176     return ldif
177
178 def upgrade_winbind(samba3,domaindn):
179     ldif = """
180         
181 dn: dc=none
182 userHwm: %d
183 groupHwm: %d
184
185 """ % (samba3.idmap.user_hwm, samba3.idmap.group_hwm)
186
187     for m in samba3.idmap.mappings:
188         ldif += """
189 dn: SID=%s,%s
190 SID: %s
191 type: %d
192 unixID: %d""" % (m.sid, domaindn, m.sid, m.type, m.unix_id)
193     
194     return ldif
195
196 def upgrade_wins(samba3):
197     """Upgrade the WINS database."""
198     ldif = ""
199     version_id = 0
200
201     for e in samba3.winsentries:
202         now = sys.nttime()
203         ttl = sys.unix2nttime(e.ttl)
204
205         version_id+=1
206
207         numIPs = len(e.ips)
208
209         if e.type == 0x1C:
210             rType = 0x2
211         elif e.type & 0x80:
212             if numIPs > 1:
213                 rType = 0x2
214             else:
215                 rType = 0x1
216         else:
217             if numIPs > 1:
218                 rType = 0x3
219             else:
220                 rType = 0x0
221
222         if ttl > now:
223             rState = 0x0 # active
224         else:
225             rState = 0x1 # released
226
227         nType = ((e.nb_flags & 0x60)>>5)
228
229         ldif += """
230 dn: name=%s,type=0x%02X
231 type: 0x%02X
232 name: %s
233 objectClass: winsRecord
234 recordType: %u
235 recordState: %u
236 nodeType: %u
237 isStatic: 0
238 expireTime: %s
239 versionID: %llu
240 """ % (e.name, e.type, e.type, e.name, 
241    rType, rState, nType, 
242    ldaptime(ttl), version_id)
243
244         for ip in e.ips:
245             ldif += "address: %s\n" % ip
246
247     ldif += """
248 dn: CN=VERSION
249 objectClass: winsMaxVersion
250 maxVersion: %llu
251 """ % version_id
252
253     return ldif
254
255 def upgrade_provision(lp, samba3):
256     subobj = Object()
257
258     domainname = samba3.configuration.get("workgroup")
259     
260     if domainname is None:
261         domainname = samba3.secrets.domains[0].name
262         print "No domain specified in smb.conf file, assuming '%s'\n" % domainname
263     
264     domsec = samba3.find_domainsecrets(domainname)
265     hostsec = samba3.find_domainsecrets(hostname())
266     realm = samba3.configuration.get("realm")
267
268     if realm is None:
269         realm = domainname
270         print "No realm specified in smb.conf file, assuming '%s'\n" % realm
271     random_init(local)
272
273     subobj.realm        = realm
274     subobj.domain       = domainname
275     subobj.hostname     = hostname()
276
277     assert subobj.realm is not None
278     assert subobj.domain is not None
279     assert subobj.hostname is not None
280
281     subobj.HOSTIP       = hostip()
282     if domsec is not None:
283         subobj.DOMAINGUID   = domsec.guid
284         subobj.DOMAINSID    = domsec.sid
285     else:
286         print "Can't find domain secrets for '%s'; using random SID and GUID\n" % domainname
287         subobj.DOMAINGUID = uuid.random()
288         subobj.DOMAINSID = randsid()
289     
290     if hostsec:
291         subobj.HOSTGUID     = hostsec.guid
292     else:
293         subobj.HOSTGUID = uuid.random()
294     subobj.invocationid = uuid.random()
295     subobj.krbtgtpass   = randpass(12)
296     subobj.machinepass  = randpass(12)
297     subobj.adminpass    = randpass(12)
298     subobj.datestring   = datestring()
299     subobj.root         = findnss(pwd.getpwnam, "root")[4]
300     subobj.nobody       = findnss(pwd.getpwnam, "nobody")[4]
301     subobj.nogroup      = findnss(grp.getgrnam, "nogroup", "nobody")[2]
302     subobj.wheel        = findnss(grp.getgrnam, "wheel", "root")[2]
303     subobj.users        = findnss(grp.getgrnam, "users", "guest", "other")[2]
304     subobj.dnsdomain    = subobj.realm.lower()
305     subobj.dnsname      = "%s.%s" % (subobj.hostname.lower(), subobj.dnsdomain)
306     subobj.basedn       = "DC=" + ",DC=".join(subobj.realm.split("."))
307     rdn_list = subobj.dnsdomain.split(".")
308     subobj.domaindn     = "DC=" + ",DC=".join(rdn_list)
309     subobj.domaindn_ldb = "users.ldb"
310     subobj.rootdn       = subobj.domaindn
311
312     modules_list        = ["rootdse",
313                     "kludge_acl",
314                     "paged_results",
315                     "server_sort",
316                     "extended_dn",
317                     "asq",
318                     "samldb",
319                     "password_hash",
320                     "operational",
321                     "objectclass",
322                     "rdn_name",
323                     "show_deleted",
324                     "partition"]
325     subobj.modules_list = ",".join(modules_list)
326
327     return subobj
328
329 smbconf_keep = [
330     "dos charset", 
331     "unix charset",
332     "display charset",
333     "comment",
334     "path",
335     "directory",
336     "workgroup",
337     "realm",
338     "netbios name",
339     "netbios aliases",
340     "netbios scope",
341     "server string",
342     "interfaces",
343     "bind interfaces only",
344     "security",
345     "auth methods",
346     "encrypt passwords",
347     "null passwords",
348     "obey pam restrictions",
349     "password server",
350     "smb passwd file",
351     "private dir",
352     "passwd chat",
353     "password level",
354     "lanman auth",
355     "ntlm auth",
356     "client NTLMv2 auth",
357     "client lanman auth",
358     "client plaintext auth",
359     "read only",
360     "hosts allow",
361     "hosts deny",
362     "log level",
363     "debuglevel",
364     "log file",
365     "smb ports",
366     "large readwrite",
367     "max protocol",
368     "min protocol",
369     "unicode",
370     "read raw",
371     "write raw",
372     "disable netbios",
373     "nt status support",
374     "announce version",
375     "announce as",
376     "max mux",
377     "max xmit",
378     "name resolve order",
379     "max wins ttl",
380     "min wins ttl",
381     "time server",
382     "unix extensions",
383     "use spnego",
384     "server signing",
385     "client signing",
386     "max connections",
387     "paranoid server security",
388     "socket options",
389     "strict sync",
390     "max print jobs",
391     "printable",
392     "print ok",
393     "printer name",
394     "printer",
395     "map system",
396     "map hidden",
397     "map archive",
398     "preferred master",
399     "prefered master",
400     "local master",
401     "browseable",
402     "browsable",
403     "wins server",
404     "wins support",
405     "csc policy",
406     "strict locking",
407     "preload",
408     "auto services",
409     "lock dir",
410     "lock directory",
411     "pid directory",
412     "socket address",
413     "copy",
414     "include",
415     "available",
416     "volume",
417     "fstype",
418     "panic action",
419     "msdfs root",
420     "host msdfs",
421     "winbind separator"]
422
423 def upgrade_smbconf(oldconf,mark):
424     """Remove configuration variables not present in Samba4
425
426     :param oldconf: Old configuration structure
427     :param mark: Whether removed configuration variables should be 
428         kept in the new configuration as "samba3:<name>"
429     """
430     data = oldconf.data()
431     newconf = param_init()
432
433     for s in data:
434         for p in data[s]:
435             keep = False
436             for k in smbconf_keep:
437                 if smbconf_keep[k] == p:
438                     keep = True
439                     break
440
441             if keep:
442                 newconf.set(s, p, oldconf.get(s, p))
443             elif mark:
444                 newconf.set(s, "samba3:"+p, oldconf.get(s,p))
445
446     if oldconf.get("domain logons") == "True":
447         newconf.set("server role", "domain controller")
448     else:
449         if oldconf.get("security") == "user":
450             newconf.set("server role", "standalone")
451         else:
452             newconf.set("server role", "member server")
453
454     return newconf
455
456 def upgrade(subobj, samba3, message, paths, session_info, credentials):
457     ret = 0
458     lp = loadparm_init()
459     samdb = Ldb(paths.samdb, session_info=session_info, credentials=credentials)
460
461     message("Writing configuration")
462     newconf = upgrade_smbconf(samba3.configuration,True)
463     newconf.save(paths.smbconf)
464
465     message("Importing account policies")
466     ldif = upgrade_sam_policy(samba3,subobj.BASEDN)
467     samdb.modify(ldif)
468     regdb = Ldb(paths.hklm)
469
470     regdb.modify("""
471 dn: value=RefusePasswordChange,key=Parameters,key=Netlogon,key=Services,key=CurrentControlSet,key=System,HIVE=NONE
472 replace: type
473 type: 4
474 replace: data
475 data: %d
476 """ % samba3.policy.refuse_machine_password_change)
477
478     message("Importing users")
479     for account in samba3.samaccounts:
480         msg = "... " + account.username
481         ldif = upgrade_sam_account(samdb, accounts,subobj.BASEDN,subobj.DOMAINSID)
482         try:
483             samdb.add(ldif)
484         except LdbError, e:
485             # FIXME: Ignore 'Record exists' errors
486             msg += "... error: " + str(e)
487             ret += 1; 
488         message(msg)
489
490     message("Importing groups")
491     for mapping in samba3.groupmappings:
492         msg = "... " + mapping.nt_name
493         ldif = upgrade_sam_group(mapping, subobj.BASEDN)
494         if ldif is not None:
495             try:
496                 samdb.add(ldif)
497             except LdbError, e:
498                 # FIXME: Ignore 'Record exists' errors
499                 msg += "... error: " + str(e)
500                 ret += 1
501         message(msg)
502
503     message("Importing registry data")
504     for hive in ["hkcr","hkcu","hklm","hkpd","hku","hkpt"]:
505         message("... " + hive)
506         regdb = Ldb(paths[hive])
507         ldif = upgrade_registry(samba3.registry, hive, regdb)
508         for j in ldif:
509             msg = "... ... " + j
510             try:
511                 regdb.add(ldif[j])
512             except LdbError, e:
513                 # FIXME: Ignore 'Record exists' errors
514                 msg += "... error: " + str(e)
515                 ret += 1
516             message(msg)
517
518     message("Importing WINS data")
519     winsdb = Ldb(paths.winsdb)
520     ldb_erase(winsdb)
521
522     ldif = upgrade_wins(samba3)
523     winsdb.add(ldif)
524
525     # figure out ldapurl, if applicable
526     ldapurl = None
527     pdb = samba3.configuration.get_list("passdb backend")
528     if pdb is not None:
529         for backend in pdb:
530             if len(backend) >= 7 and backend[0:7] == "ldapsam":
531                 ldapurl = backend[7:]
532
533     # URL was not specified in passdb backend but ldap /is/ used
534     if ldapurl == "":
535         ldapurl = "ldap://%s" % samba3.configuration.get("ldap server")
536
537     # Enable samba3sam module if original passdb backend was ldap
538     if ldapurl is not None:
539         message("Enabling Samba3 LDAP mappings for SAM database")
540
541         samdb.modify("""
542 dn: @MODULES
543 changetype: modify
544 replace: @LIST
545 @LIST: samldb,operational,objectguid,rdn_name,samba3sam
546 """)
547
548         samdb.add("""
549 dn: @MAP=samba3sam
550 @MAP_URL: %s""" % ldapurl)
551
552     return ret
553
554 def upgrade_verify(subobj, samba3, paths, message):
555     message("Verifying account policies")
556
557     samldb = Ldb(paths.samdb)
558
559     for account in samba3.samaccounts:
560         msg = samldb.search("(&(sAMAccountName=" + account.nt_username + ")(objectclass=user))")
561         assert(len(msg) >= 1)
562     
563     # FIXME