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