8199c41aba7f2d063ed5beb62283ddbd00fffbe3
[ddiss/samba.git] / source4 / scripting / python / samba / netcmd / domain.py
1 # domain management
2 #
3 # Copyright Matthias Dieter Wallnoefer 2009
4 # Copyright Andrew Kroeger 2009
5 # Copyright Jelmer Vernooij 2009
6 # Copyright Giampaolo Lauria 2011
7 # Copyright Matthieu Patou <mat@matws.net> 2011
8 #
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
21 #
22
23
24
25 import samba.getopt as options
26 import ldb
27 import string
28 import os
29 import tempfile
30 import logging
31 from samba.net import Net, LIBNET_JOIN_AUTOMATIC
32 import samba.ntacls
33 from samba.join import join_RODC, join_DC, join_subdomain
34 from samba.auth import system_session
35 from samba.samdb import SamDB
36 from samba.dcerpc import drsuapi
37 from samba.dcerpc.samr import DOMAIN_PASSWORD_COMPLEX, DOMAIN_PASSWORD_STORE_CLEARTEXT
38 from samba.netcmd import (
39     Command,
40     CommandError,
41     SuperCommand,
42     Option
43     )
44 from samba.netcmd.common import netcmd_get_domain_infos_via_cldap
45 from samba.samba3 import Samba3
46 from samba.samba3 import param as s3param
47 from samba.upgrade import upgrade_from_samba3
48 from samba.drs_utils import (
49                             sendDsReplicaSync, drsuapi_connect, drsException,
50                             sendRemoveDsServer)
51
52
53 from samba.dsdb import (
54     DS_DOMAIN_FUNCTION_2000,
55     DS_DOMAIN_FUNCTION_2003,
56     DS_DOMAIN_FUNCTION_2003_MIXED,
57     DS_DOMAIN_FUNCTION_2008,
58     DS_DOMAIN_FUNCTION_2008_R2,
59     DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL,
60     DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL,
61     UF_WORKSTATION_TRUST_ACCOUNT,
62     UF_SERVER_TRUST_ACCOUNT,
63     UF_TRUSTED_FOR_DELEGATION
64     )
65
66 def get_testparm_var(testparm, smbconf, varname):
67     cmd = "%s -s -l --parameter-name='%s' %s 2>/dev/null" % (testparm, varname, smbconf)
68     output = os.popen(cmd, 'r').readline()
69     return output.strip()
70
71 try:
72    import samba.dckeytab
73    class cmd_domain_export_keytab(Command):
74        """Dumps kerberos keys of the domain into a keytab"""
75
76        synopsis = "%prog <keytab> [options]"
77
78        takes_optiongroups = {
79            "sambaopts": options.SambaOptions,
80            "credopts": options.CredentialsOptions,
81            "versionopts": options.VersionOptions,
82            }
83
84        takes_options = [
85            Option("--principal", help="extract only this principal", type=str),
86            ]
87
88        takes_args = ["keytab"]
89
90        def run(self, keytab, credopts=None, sambaopts=None, versionopts=None, principal=None):
91            lp = sambaopts.get_loadparm()
92            net = Net(None, lp)
93            net.export_keytab(keytab=keytab, principal=principal)
94 except:
95    cmd_domain_export_keytab = None
96
97
98 class cmd_domain_info(Command):
99     """Print basic info about a domain and the DC passed as parameter"""
100
101     synopsis = "%prog <ip_address> [options]"
102
103     takes_options = [
104         ]
105
106     takes_optiongroups = {
107         "sambaopts": options.SambaOptions,
108         "credopts": options.CredentialsOptions,
109         "versionopts": options.VersionOptions,
110         }
111
112     takes_args = ["address"]
113
114     def run(self, address, credopts=None, sambaopts=None, versionopts=None):
115         lp = sambaopts.get_loadparm()
116         try:
117             res = netcmd_get_domain_infos_via_cldap(lp, None, address)
118             print "Forest           : %s" % res.forest
119             print "Domain           : %s" % res.dns_domain
120             print "Netbios domain   : %s" % res.domain_name
121             print "DC name          : %s" % res.pdc_dns_name
122             print "DC netbios name  : %s" % res.pdc_name
123             print "Server site      : %s" % res.server_site
124             print "Client site      : %s" % res.client_site
125         except RuntimeError:
126             raise CommandError("Invalid IP address '" + address + "'!")
127
128
129 class cmd_domain_join(Command):
130     """Joins domain as either member or backup domain controller"""
131
132     synopsis = "%prog <dnsdomain> [DC|RODC|MEMBER|SUBDOMAIN] [options]"
133
134     takes_optiongroups = {
135         "sambaopts": options.SambaOptions,
136         "versionopts": options.VersionOptions,
137         "credopts": options.CredentialsOptions,
138     }
139
140     takes_options = [
141         Option("--server", help="DC to join", type=str),
142         Option("--site", help="site to join", type=str),
143         Option("--targetdir", help="where to store provision", type=str),
144         Option("--parent-domain", help="parent domain to create subdomain under", type=str),
145         Option("--domain-critical-only",
146                help="only replicate critical domain objects",
147                action="store_true"),
148         Option("--machinepass", type=str, metavar="PASSWORD",
149                help="choose machine password (otherwise random)"),
150         Option("--use-ntvfs", help="Use NTVFS for the fileserver (default = no)",
151                action="store_true")
152         ]
153
154     takes_args = ["domain", "role?"]
155
156     def run(self, domain, role=None, sambaopts=None, credopts=None,
157             versionopts=None, server=None, site=None, targetdir=None,
158             domain_critical_only=False, parent_domain=None, machinepass=None,
159             use_ntvfs=False):
160         lp = sambaopts.get_loadparm()
161         creds = credopts.get_credentials(lp)
162         net = Net(creds, lp, server=credopts.ipaddress)
163
164         if site is None:
165             site = "Default-First-Site-Name"
166
167         netbios_name = lp.get("netbios name")
168
169         if not role is None:
170             role = role.upper()
171
172         if role is None or role == "MEMBER":
173             (join_password, sid, domain_name) = net.join_member(domain,
174                                                                 netbios_name,
175                                                                 LIBNET_JOIN_AUTOMATIC,
176                                                                 machinepass=machinepass)
177
178             self.outf.write("Joined domain %s (%s)\n" % (domain_name, sid))
179             return
180         elif role == "DC":
181             join_DC(server=server, creds=creds, lp=lp, domain=domain,
182                     site=site, netbios_name=netbios_name, targetdir=targetdir,
183                     domain_critical_only=domain_critical_only,
184                     machinepass=machinepass, use_ntvfs=use_ntvfs)
185             return
186         elif role == "RODC":
187             join_RODC(server=server, creds=creds, lp=lp, domain=domain,
188                       site=site, netbios_name=netbios_name, targetdir=targetdir,
189                       domain_critical_only=domain_critical_only,
190                       machinepass=machinepass, use_ntvfs=use_ntvfs)
191             return
192         elif role == "SUBDOMAIN":
193             netbios_domain = lp.get("workgroup")
194             if parent_domain is None:
195                 parent_domain = ".".join(domain.split(".")[1:])
196             join_subdomain(server=server, creds=creds, lp=lp, dnsdomain=domain, parent_domain=parent_domain,
197                            site=site, netbios_name=netbios_name, netbios_domain=netbios_domain, targetdir=targetdir,
198                            machinepass=machinepass, use_ntvfs=use_ntvfs)
199             return
200         else:
201             raise CommandError("Invalid role '%s' (possible values: MEMBER, DC, RODC, SUBDOMAIN)" % role)
202
203
204
205 class cmd_domain_demote(Command):
206     """Demote ourselves from the role of Domain Controller"""
207
208     synopsis = "%prog [options]"
209
210     takes_options = [
211         Option("--server", help="DC to force replication before demote", type=str),
212         Option("--targetdir", help="where provision is stored", type=str),
213         ]
214
215     takes_optiongroups = {
216         "sambaopts": options.SambaOptions,
217         "credopts": options.CredentialsOptions,
218         "versionopts": options.VersionOptions,
219         }
220
221     def run(self, sambaopts=None, credopts=None,
222             versionopts=None, server=None, targetdir=None):
223         lp = sambaopts.get_loadparm()
224         creds = credopts.get_credentials(lp)
225         net = Net(creds, lp, server=credopts.ipaddress)
226
227         netbios_name = lp.get("netbios name")
228         samdb = SamDB(session_info=system_session(), credentials=creds, lp=lp)
229         if not server:
230             res = samdb.search(expression='(&(objectClass=computer)(serverReferenceBL=*))', attrs=["dnsHostName", "name"])
231             if (len(res) == 0):
232                 raise CommandError("Unable to search for servers")
233
234             if (len(res) == 1):
235                 raise CommandError("You are the latest server in the domain")
236
237             server = None
238             for e in res:
239                 if str(e["name"]).lower() != netbios_name.lower():
240                     server = e["dnsHostName"]
241                     break
242
243         ntds_guid = samdb.get_ntds_GUID()
244         msg = samdb.search(base=str(samdb.get_config_basedn()), scope=ldb.SCOPE_SUBTREE,
245                                 expression="(objectGUID=%s)" % ntds_guid,
246                                 attrs=['options'])
247         if len(msg) == 0 or "options" not in msg[0]:
248             raise CommandError("Failed to find options on %s" % ntds_guid)
249
250         ntds_dn = msg[0].dn
251         dsa_options = int(str(msg[0]['options']))
252
253         res = samdb.search(expression="(fSMORoleOwner=%s)" % str(ntds_dn),
254                             controls=["search_options:1:2"])
255
256         if len(res) != 0:
257             raise CommandError("Current DC is still the owner of %d role(s), use the role command to transfer roles to another DC" % len(res))
258
259         print "Using %s as partner server for the demotion" % server
260         (drsuapiBind, drsuapi_handle, supportedExtensions) = drsuapi_connect(server, lp, creds)
261
262         print "Desactivating inbound replication"
263
264         nmsg = ldb.Message()
265         nmsg.dn = msg[0].dn
266
267         dsa_options |= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
268         nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
269         samdb.modify(nmsg)
270
271         if not (dsa_options & DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL) and not samdb.am_rodc():
272
273             print "Asking partner server %s to synchronize from us" % server
274             for part in (samdb.get_schema_basedn(),
275                             samdb.get_config_basedn(),
276                             samdb.get_root_basedn()):
277                 try:
278                     sendDsReplicaSync(drsuapiBind, drsuapi_handle, ntds_guid, str(part), drsuapi.DRSUAPI_DRS_WRIT_REP)
279                 except drsException, e:
280                     print "Error while demoting, re-enabling inbound replication"
281                     dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
282                     nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
283                     samdb.modify(nmsg)
284                     raise CommandError("Error while sending a DsReplicaSync for partion %s" % str(part), e)
285         try:
286             remote_samdb = SamDB(url="ldap://%s" % server,
287                                 session_info=system_session(),
288                                 credentials=creds, lp=lp)
289
290             print "Changing userControl and container"
291             res = remote_samdb.search(base=str(remote_samdb.get_root_basedn()),
292                                 expression="(&(objectClass=user)(sAMAccountName=%s$))" %
293                                             netbios_name.upper(),
294                                 attrs=["userAccountControl"])
295             dc_dn = res[0].dn
296             uac = int(str(res[0]["userAccountControl"]))
297
298         except Exception, e:
299                 print "Error while demoting, re-enabling inbound replication"
300                 dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
301                 nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
302                 samdb.modify(nmsg)
303                 raise CommandError("Error while changing account control", e)
304
305         if (len(res) != 1):
306             print "Error while demoting, re-enabling inbound replication"
307             dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
308             nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
309             samdb.modify(nmsg)
310             raise CommandError("Unable to find object with samaccountName = %s$"
311                                " in the remote dc" % netbios_name.upper())
312
313         olduac = uac
314
315         uac ^= (UF_SERVER_TRUST_ACCOUNT|UF_TRUSTED_FOR_DELEGATION)
316         uac |= UF_WORKSTATION_TRUST_ACCOUNT
317
318         msg = ldb.Message()
319         msg.dn = dc_dn
320
321         msg["userAccountControl"] = ldb.MessageElement("%d" % uac,
322                                                         ldb.FLAG_MOD_REPLACE,
323                                                         "userAccountControl")
324         try:
325             remote_samdb.modify(msg)
326         except Exception, e:
327             print "Error while demoting, re-enabling inbound replication"
328             dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
329             nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
330             samdb.modify(nmsg)
331
332             raise CommandError("Error while changing account control", e)
333
334         parent = msg.dn.parent()
335         rdn = str(res[0].dn)
336         rdn = string.replace(rdn, ",%s" % str(parent), "")
337         # Let's move to the Computer container
338         i = 0
339         newrdn = rdn
340
341         computer_dn = ldb.Dn(remote_samdb, "CN=Computers,%s" % str(remote_samdb.get_root_basedn()))
342         res = remote_samdb.search(base=computer_dn, expression=rdn, scope=ldb.SCOPE_ONELEVEL)
343
344         if (len(res) != 0):
345             res = remote_samdb.search(base=computer_dn, expression="%s-%d" % (rdn, i),
346                                         scope=ldb.SCOPE_ONELEVEL)
347             while(len(res) != 0 and i < 100):
348                 i = i + 1
349                 res = remote_samdb.search(base=computer_dn, expression="%s-%d" % (rdn, i),
350                                             scope=ldb.SCOPE_ONELEVEL)
351
352             if i == 100:
353                 print "Error while demoting, re-enabling inbound replication"
354                 dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
355                 nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
356                 samdb.modify(nmsg)
357
358                 msg = ldb.Message()
359                 msg.dn = dc_dn
360
361                 msg["userAccountControl"] = ldb.MessageElement("%d" % uac,
362                                                         ldb.FLAG_MOD_REPLACE,
363                                                         "userAccountControl")
364
365                 remote_samdb.modify(msg)
366
367                 raise CommandError("Unable to find a slot for renaming %s,"
368                                     " all names from %s-1 to %s-%d seemed used" %
369                                     (str(dc_dn), rdn, rdn, i - 9))
370
371             newrdn = "%s-%d" % (rdn, i)
372
373         try:
374             newdn = ldb.Dn(remote_samdb, "%s,%s" % (newrdn, str(computer_dn)))
375             remote_samdb.rename(dc_dn, newdn)
376         except Exception, e:
377             print "Error while demoting, re-enabling inbound replication"
378             dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
379             nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
380             samdb.modify(nmsg)
381
382             msg = ldb.Message()
383             msg.dn = dc_dn
384
385             msg["userAccountControl"] = ldb.MessageElement("%d" % uac,
386                                                     ldb.FLAG_MOD_REPLACE,
387                                                     "userAccountControl")
388
389             remote_samdb.modify(msg)
390             raise CommandError("Error while renaming %s to %s" % (str(dc_dn), str(newdn)), e)
391
392
393         server_dsa_dn = samdb.get_serverName()
394         domain = remote_samdb.get_root_basedn()
395
396         try:
397             sendRemoveDsServer(drsuapiBind, drsuapi_handle, server_dsa_dn, domain)
398         except drsException, e:
399             print "Error while demoting, re-enabling inbound replication"
400             dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
401             nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
402             samdb.modify(nmsg)
403
404             msg = ldb.Message()
405             msg.dn = newdn
406
407             msg["userAccountControl"] = ldb.MessageElement("%d" % uac,
408                                                     ldb.FLAG_MOD_REPLACE,
409                                                     "userAccountControl")
410             print str(dc_dn)
411             remote_samdb.modify(msg)
412             remote_samdb.rename(newdn, dc_dn)
413             raise CommandError("Error while sending a removeDsServer", e)
414
415         for s in ("CN=Entreprise,CN=Microsoft System Volumes,CN=System,CN=Configuration",
416                   "CN=%s,CN=Microsoft System Volumes,CN=System,CN=Configuration" % lp.get("realm"),
417                   "CN=Domain System Volumes (SYSVOL share),CN=File Replication Service,CN=System"):
418             try:
419                 remote_samdb.delete(ldb.Dn(remote_samdb,
420                                     "%s,%s,%s" % (str(rdn), s, str(remote_samdb.get_root_basedn()))))
421             except ldb.LdbError, l:
422                 pass
423
424         for s in ("CN=Entreprise,CN=NTFRS Subscriptions",
425                   "CN=%s, CN=NTFRS Subscriptions" % lp.get("realm"),
426                   "CN=Domain system Volumes (SYSVOL Share), CN=NTFRS Subscriptions",
427                   "CN=NTFRS Subscriptions"):
428             try:
429                 remote_samdb.delete(ldb.Dn(remote_samdb,
430                                     "%s,%s" % (s, str(newdn))))
431             except ldb.LdbError, l:
432                 pass
433
434         self.outf.write("Demote successfull\n")
435
436
437 class cmd_domain_level(Command):
438     """Raises domain and forest function levels"""
439
440     synopsis = "%prog (show|raise <options>) [options]"
441
442     takes_optiongroups = {
443         "sambaopts": options.SambaOptions,
444         "credopts": options.CredentialsOptions,
445         "versionopts": options.VersionOptions,
446         }
447
448     takes_options = [
449         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
450                metavar="URL", dest="H"),
451         Option("--quiet", help="Be quiet", action="store_true"),
452         Option("--forest-level", type="choice", choices=["2003", "2008", "2008_R2"],
453             help="The forest function level (2003 | 2008 | 2008_R2)"),
454         Option("--domain-level", type="choice", choices=["2003", "2008", "2008_R2"],
455             help="The domain function level (2003 | 2008 | 2008_R2)")
456             ]
457
458     takes_args = ["subcommand"]
459
460     def run(self, subcommand, H=None, forest_level=None, domain_level=None,
461             quiet=False, credopts=None, sambaopts=None, versionopts=None):
462         lp = sambaopts.get_loadparm()
463         creds = credopts.get_credentials(lp, fallback_machine=True)
464
465         samdb = SamDB(url=H, session_info=system_session(),
466             credentials=creds, lp=lp)
467
468         domain_dn = samdb.domain_dn()
469
470         res_forest = samdb.search("CN=Partitions,%s" % samdb.get_config_basedn(),
471           scope=ldb.SCOPE_BASE, attrs=["msDS-Behavior-Version"])
472         assert len(res_forest) == 1
473
474         res_domain = samdb.search(domain_dn, scope=ldb.SCOPE_BASE,
475           attrs=["msDS-Behavior-Version", "nTMixedDomain"])
476         assert len(res_domain) == 1
477
478         res_dc_s = samdb.search("CN=Sites,%s" % samdb.get_config_basedn(),
479           scope=ldb.SCOPE_SUBTREE, expression="(objectClass=nTDSDSA)",
480           attrs=["msDS-Behavior-Version"])
481         assert len(res_dc_s) >= 1
482
483         try:
484             level_forest = int(res_forest[0]["msDS-Behavior-Version"][0])
485             level_domain = int(res_domain[0]["msDS-Behavior-Version"][0])
486             level_domain_mixed = int(res_domain[0]["nTMixedDomain"][0])
487
488             min_level_dc = int(res_dc_s[0]["msDS-Behavior-Version"][0]) # Init value
489             for msg in res_dc_s:
490                 if int(msg["msDS-Behavior-Version"][0]) < min_level_dc:
491                     min_level_dc = int(msg["msDS-Behavior-Version"][0])
492
493             if level_forest < 0 or level_domain < 0:
494                 raise CommandError("Domain and/or forest function level(s) is/are invalid. Correct them or reprovision!")
495             if min_level_dc < 0:
496                 raise CommandError("Lowest function level of a DC is invalid. Correct this or reprovision!")
497             if level_forest > level_domain:
498                 raise CommandError("Forest function level is higher than the domain level(s). Correct this or reprovision!")
499             if level_domain > min_level_dc:
500                 raise CommandError("Domain function level is higher than the lowest function level of a DC. Correct this or reprovision!")
501
502         except KeyError:
503             raise CommandError("Could not retrieve the actual domain, forest level and/or lowest DC function level!")
504
505         if subcommand == "show":
506             self.message("Domain and forest function level for domain '%s'" % domain_dn)
507             if level_forest == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0:
508                 self.message("\nATTENTION: You run SAMBA 4 on a forest function level lower than Windows 2000 (Native). This isn't supported! Please raise!")
509             if level_domain == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0:
510                 self.message("\nATTENTION: You run SAMBA 4 on a domain function level lower than Windows 2000 (Native). This isn't supported! Please raise!")
511             if min_level_dc == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0:
512                 self.message("\nATTENTION: You run SAMBA 4 on a lowest function level of a DC lower than Windows 2003. This isn't supported! Please step-up or upgrade the concerning DC(s)!")
513
514             self.message("")
515
516             if level_forest == DS_DOMAIN_FUNCTION_2000:
517                 outstr = "2000"
518             elif level_forest == DS_DOMAIN_FUNCTION_2003_MIXED:
519                 outstr = "2003 with mixed domains/interim (NT4 DC support)"
520             elif level_forest == DS_DOMAIN_FUNCTION_2003:
521                 outstr = "2003"
522             elif level_forest == DS_DOMAIN_FUNCTION_2008:
523                 outstr = "2008"
524             elif level_forest == DS_DOMAIN_FUNCTION_2008_R2:
525                 outstr = "2008 R2"
526             else:
527                 outstr = "higher than 2008 R2"
528             self.message("Forest function level: (Windows) " + outstr)
529
530             if level_domain == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0:
531                 outstr = "2000 mixed (NT4 DC support)"
532             elif level_domain == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed == 0:
533                 outstr = "2000"
534             elif level_domain == DS_DOMAIN_FUNCTION_2003_MIXED:
535                 outstr = "2003 with mixed domains/interim (NT4 DC support)"
536             elif level_domain == DS_DOMAIN_FUNCTION_2003:
537                 outstr = "2003"
538             elif level_domain == DS_DOMAIN_FUNCTION_2008:
539                 outstr = "2008"
540             elif level_domain == DS_DOMAIN_FUNCTION_2008_R2:
541                 outstr = "2008 R2"
542             else:
543                 outstr = "higher than 2008 R2"
544             self.message("Domain function level: (Windows) " + outstr)
545
546             if min_level_dc == DS_DOMAIN_FUNCTION_2000:
547                 outstr = "2000"
548             elif min_level_dc == DS_DOMAIN_FUNCTION_2003:
549                 outstr = "2003"
550             elif min_level_dc == DS_DOMAIN_FUNCTION_2008:
551                 outstr = "2008"
552             elif min_level_dc == DS_DOMAIN_FUNCTION_2008_R2:
553                 outstr = "2008 R2"
554             else:
555                 outstr = "higher than 2008 R2"
556             self.message("Lowest function level of a DC: (Windows) " + outstr)
557
558         elif subcommand == "raise":
559             msgs = []
560
561             if domain_level is not None:
562                 if domain_level == "2003":
563                     new_level_domain = DS_DOMAIN_FUNCTION_2003
564                 elif domain_level == "2008":
565                     new_level_domain = DS_DOMAIN_FUNCTION_2008
566                 elif domain_level == "2008_R2":
567                     new_level_domain = DS_DOMAIN_FUNCTION_2008_R2
568
569                 if new_level_domain <= level_domain and level_domain_mixed == 0:
570                     raise CommandError("Domain function level can't be smaller than or equal to the actual one!")
571
572                 if new_level_domain > min_level_dc:
573                     raise CommandError("Domain function level can't be higher than the lowest function level of a DC!")
574
575                 # Deactivate mixed/interim domain support
576                 if level_domain_mixed != 0:
577                     # Directly on the base DN
578                     m = ldb.Message()
579                     m.dn = ldb.Dn(samdb, domain_dn)
580                     m["nTMixedDomain"] = ldb.MessageElement("0",
581                       ldb.FLAG_MOD_REPLACE, "nTMixedDomain")
582                     samdb.modify(m)
583                     # Under partitions
584                     m = ldb.Message()
585                     m.dn = ldb.Dn(samdb, "CN=" + lp.get("workgroup") + ",CN=Partitions,%s" % samdb.get_config_basedn())
586                     m["nTMixedDomain"] = ldb.MessageElement("0",
587                       ldb.FLAG_MOD_REPLACE, "nTMixedDomain")
588                     try:
589                         samdb.modify(m)
590                     except ldb.LdbError, (enum, emsg):
591                         if enum != ldb.ERR_UNWILLING_TO_PERFORM:
592                             raise
593
594                 # Directly on the base DN
595                 m = ldb.Message()
596                 m.dn = ldb.Dn(samdb, domain_dn)
597                 m["msDS-Behavior-Version"]= ldb.MessageElement(
598                   str(new_level_domain), ldb.FLAG_MOD_REPLACE,
599                             "msDS-Behavior-Version")
600                 samdb.modify(m)
601                 # Under partitions
602                 m = ldb.Message()
603                 m.dn = ldb.Dn(samdb, "CN=" + lp.get("workgroup")
604                   + ",CN=Partitions,%s" % samdb.get_config_basedn())
605                 m["msDS-Behavior-Version"]= ldb.MessageElement(
606                   str(new_level_domain), ldb.FLAG_MOD_REPLACE,
607                           "msDS-Behavior-Version")
608                 try:
609                     samdb.modify(m)
610                 except ldb.LdbError, (enum, emsg):
611                     if enum != ldb.ERR_UNWILLING_TO_PERFORM:
612                         raise
613
614                 level_domain = new_level_domain
615                 msgs.append("Domain function level changed!")
616
617             if forest_level is not None:
618                 if forest_level == "2003":
619                     new_level_forest = DS_DOMAIN_FUNCTION_2003
620                 elif forest_level == "2008":
621                     new_level_forest = DS_DOMAIN_FUNCTION_2008
622                 elif forest_level == "2008_R2":
623                     new_level_forest = DS_DOMAIN_FUNCTION_2008_R2
624                 if new_level_forest <= level_forest:
625                     raise CommandError("Forest function level can't be smaller than or equal to the actual one!")
626                 if new_level_forest > level_domain:
627                     raise CommandError("Forest function level can't be higher than the domain function level(s). Please raise it/them first!")
628                 m = ldb.Message()
629                 m.dn = ldb.Dn(samdb, "CN=Partitions,%s" % samdb.get_config_basedn())
630                 m["msDS-Behavior-Version"]= ldb.MessageElement(
631                   str(new_level_forest), ldb.FLAG_MOD_REPLACE,
632                           "msDS-Behavior-Version")
633                 samdb.modify(m)
634                 msgs.append("Forest function level changed!")
635             msgs.append("All changes applied successfully!")
636             self.message("\n".join(msgs))
637         else:
638             raise CommandError("invalid argument: '%s' (choose from 'show', 'raise')" % subcommand)
639
640
641 class cmd_domain_passwordsettings(Command):
642     """Sets password settings
643
644     Password complexity, history length, minimum password length, the minimum
645     and maximum password age) on a Samba4 server.
646     """
647
648     synopsis = "%prog (show|set <options>) [options]"
649
650     takes_optiongroups = {
651         "sambaopts": options.SambaOptions,
652         "versionopts": options.VersionOptions,
653         "credopts": options.CredentialsOptions,
654         }
655
656     takes_options = [
657         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
658                metavar="URL", dest="H"),
659         Option("--quiet", help="Be quiet", action="store_true"),
660         Option("--complexity", type="choice", choices=["on","off","default"],
661           help="The password complexity (on | off | default). Default is 'on'"),
662         Option("--store-plaintext", type="choice", choices=["on","off","default"],
663           help="Store plaintext passwords where account have 'store passwords with reversible encryption' set (on | off | default). Default is 'off'"),
664         Option("--history-length",
665           help="The password history length (<integer> | default).  Default is 24.", type=str),
666         Option("--min-pwd-length",
667           help="The minimum password length (<integer> | default).  Default is 7.", type=str),
668         Option("--min-pwd-age",
669           help="The minimum password age (<integer in days> | default).  Default is 1.", type=str),
670         Option("--max-pwd-age",
671           help="The maximum password age (<integer in days> | default).  Default is 43.", type=str),
672           ]
673
674     takes_args = ["subcommand"]
675
676     def run(self, subcommand, H=None, min_pwd_age=None, max_pwd_age=None,
677             quiet=False, complexity=None, store_plaintext=None, history_length=None,
678             min_pwd_length=None, credopts=None, sambaopts=None,
679             versionopts=None):
680         lp = sambaopts.get_loadparm()
681         creds = credopts.get_credentials(lp)
682
683         samdb = SamDB(url=H, session_info=system_session(),
684             credentials=creds, lp=lp)
685
686         domain_dn = samdb.domain_dn()
687         res = samdb.search(domain_dn, scope=ldb.SCOPE_BASE,
688           attrs=["pwdProperties", "pwdHistoryLength", "minPwdLength",
689                  "minPwdAge", "maxPwdAge"])
690         assert(len(res) == 1)
691         try:
692             pwd_props = int(res[0]["pwdProperties"][0])
693             pwd_hist_len = int(res[0]["pwdHistoryLength"][0])
694             cur_min_pwd_len = int(res[0]["minPwdLength"][0])
695             # ticks -> days
696             cur_min_pwd_age = int(abs(int(res[0]["minPwdAge"][0])) / (1e7 * 60 * 60 * 24))
697             if int(res[0]["maxPwdAge"][0]) == -0x8000000000000000:
698                 cur_max_pwd_age = 0
699             else:
700                 cur_max_pwd_age = int(abs(int(res[0]["maxPwdAge"][0])) / (1e7 * 60 * 60 * 24))
701         except Exception, e:
702             raise CommandError("Could not retrieve password properties!", e)
703
704         if subcommand == "show":
705             self.message("Password informations for domain '%s'" % domain_dn)
706             self.message("")
707             if pwd_props & DOMAIN_PASSWORD_COMPLEX != 0:
708                 self.message("Password complexity: on")
709             else:
710                 self.message("Password complexity: off")
711             if pwd_props & DOMAIN_PASSWORD_STORE_CLEARTEXT != 0:
712                 self.message("Store plaintext passwords: on")
713             else:
714                 self.message("Store plaintext passwords: off")
715             self.message("Password history length: %d" % pwd_hist_len)
716             self.message("Minimum password length: %d" % cur_min_pwd_len)
717             self.message("Minimum password age (days): %d" % cur_min_pwd_age)
718             self.message("Maximum password age (days): %d" % cur_max_pwd_age)
719         elif subcommand == "set":
720             msgs = []
721             m = ldb.Message()
722             m.dn = ldb.Dn(samdb, domain_dn)
723
724             if complexity is not None:
725                 if complexity == "on" or complexity == "default":
726                     pwd_props = pwd_props | DOMAIN_PASSWORD_COMPLEX
727                     msgs.append("Password complexity activated!")
728                 elif complexity == "off":
729                     pwd_props = pwd_props & (~DOMAIN_PASSWORD_COMPLEX)
730                     msgs.append("Password complexity deactivated!")
731
732             if store_plaintext is not None:
733                 if store_plaintext == "on" or store_plaintext == "default":
734                     pwd_props = pwd_props | DOMAIN_PASSWORD_STORE_CLEARTEXT
735                     msgs.append("Plaintext password storage for changed passwords activated!")
736                 elif store_plaintext == "off":
737                     pwd_props = pwd_props & (~DOMAIN_PASSWORD_STORE_CLEARTEXT)
738                     msgs.append("Plaintext password storage for changed passwords deactivated!")
739
740             if complexity is not None or store_plaintext is not None:
741                 m["pwdProperties"] = ldb.MessageElement(str(pwd_props),
742                   ldb.FLAG_MOD_REPLACE, "pwdProperties")
743
744             if history_length is not None:
745                 if history_length == "default":
746                     pwd_hist_len = 24
747                 else:
748                     pwd_hist_len = int(history_length)
749
750                 if pwd_hist_len < 0 or pwd_hist_len > 24:
751                     raise CommandError("Password history length must be in the range of 0 to 24!")
752
753                 m["pwdHistoryLength"] = ldb.MessageElement(str(pwd_hist_len),
754                   ldb.FLAG_MOD_REPLACE, "pwdHistoryLength")
755                 msgs.append("Password history length changed!")
756
757             if min_pwd_length is not None:
758                 if min_pwd_length == "default":
759                     min_pwd_len = 7
760                 else:
761                     min_pwd_len = int(min_pwd_length)
762
763                 if min_pwd_len < 0 or min_pwd_len > 14:
764                     raise CommandError("Minimum password length must be in the range of 0 to 14!")
765
766                 m["minPwdLength"] = ldb.MessageElement(str(min_pwd_len),
767                   ldb.FLAG_MOD_REPLACE, "minPwdLength")
768                 msgs.append("Minimum password length changed!")
769
770             if min_pwd_age is not None:
771                 if min_pwd_age == "default":
772                     min_pwd_age = 1
773                 else:
774                     min_pwd_age = int(min_pwd_age)
775
776                 if min_pwd_age < 0 or min_pwd_age > 998:
777                     raise CommandError("Minimum password age must be in the range of 0 to 998!")
778
779                 # days -> ticks
780                 min_pwd_age_ticks = -int(min_pwd_age * (24 * 60 * 60 * 1e7))
781
782                 m["minPwdAge"] = ldb.MessageElement(str(min_pwd_age_ticks),
783                   ldb.FLAG_MOD_REPLACE, "minPwdAge")
784                 msgs.append("Minimum password age changed!")
785
786             if max_pwd_age is not None:
787                 if max_pwd_age == "default":
788                     max_pwd_age = 43
789                 else:
790                     max_pwd_age = int(max_pwd_age)
791
792                 if max_pwd_age < 0 or max_pwd_age > 999:
793                     raise CommandError("Maximum password age must be in the range of 0 to 999!")
794
795                 # days -> ticks
796                 if max_pwd_age == 0:
797                     max_pwd_age_ticks = -0x8000000000000000
798                 else:
799                     max_pwd_age_ticks = -int(max_pwd_age * (24 * 60 * 60 * 1e7))
800
801                 m["maxPwdAge"] = ldb.MessageElement(str(max_pwd_age_ticks),
802                   ldb.FLAG_MOD_REPLACE, "maxPwdAge")
803                 msgs.append("Maximum password age changed!")
804
805             if max_pwd_age > 0 and min_pwd_age >= max_pwd_age:
806                 raise CommandError("Maximum password age (%d) must be greater than minimum password age (%d)!" % (max_pwd_age, min_pwd_age))
807
808             if len(m) == 0:
809                 raise CommandError("You must specify at least one option to set. Try --help")
810             samdb.modify(m)
811             msgs.append("All changes applied successfully!")
812             self.message("\n".join(msgs))
813         else:
814             raise CommandError("Wrong argument '%s'!" % subcommand)
815
816
817 class cmd_domain_classicupgrade(Command):
818     """Upgrade from Samba classic (NT4-like) database to Samba AD DC database.
819
820     Specify either a directory with all Samba classic DC databases and state files (with --dbdir) or
821     the testparm utility from your classic installation (with --testparm).
822     """
823
824     synopsis = "%prog [options] <classic_smb_conf>"
825
826     takes_optiongroups = {
827         "sambaopts": options.SambaOptions,
828         "versionopts": options.VersionOptions
829     }
830
831     takes_options = [
832         Option("--dbdir", type="string", metavar="DIR",
833                   help="Path to samba classic DC database directory"),
834         Option("--testparm", type="string", metavar="PATH",
835                   help="Path to samba classic DC testparm utility from the previous installation.  This allows the default paths of the previous installation to be followed"),
836         Option("--targetdir", type="string", metavar="DIR",
837                   help="Path prefix where the new Samba 4.0 AD domain should be initialised"),
838         Option("--quiet", help="Be quiet", action="store_true"),
839         Option("--verbose", help="Be verbose", action="store_true"),
840         Option("--use-xattrs", type="choice", choices=["yes","no","auto"], metavar="[yes|no|auto]",
841                    help="Define if we should use the native fs capabilities or a tdb file for storing attributes likes ntacl, auto tries to make an inteligent guess based on the user rights and system capabilities", default="auto"),
842         Option("--dns-backend", type="choice", metavar="NAMESERVER-BACKEND",
843                choices=["SAMBA_INTERNAL", "BIND9_FLATFILE", "BIND9_DLZ", "NONE"],
844                help="The DNS server backend. SAMBA_INTERNAL is the builtin name server, " \
845                    "BIND9_FLATFILE uses bind9 text database to store zone information, " \
846                    "BIND9_DLZ uses samba4 AD to store zone information (default), " \
847                    "NONE skips the DNS setup entirely (this DC will not be a DNS server)",
848                default="BIND9_DLZ")
849     ]
850
851     takes_args = ["smbconf"]
852
853     def run(self, smbconf=None, targetdir=None, dbdir=None, testparm=None, 
854             quiet=False, verbose=False, use_xattrs=None, sambaopts=None, versionopts=None,
855             dns_backend=None):
856
857         if not os.path.exists(smbconf):
858             raise CommandError("File %s does not exist" % smbconf)
859
860         if testparm and not os.path.exists(testparm):
861             raise CommandError("Testparm utility %s does not exist" % testparm)
862
863         if dbdir and not os.path.exists(dbdir):
864             raise CommandError("Directory %s does not exist" % dbdir)
865
866         if not dbdir and not testparm:
867             raise CommandError("Please specify either dbdir or testparm")
868
869         logger = self.get_logger()
870         if verbose:
871             logger.setLevel(logging.DEBUG)
872         elif quiet:
873             logger.setLevel(logging.WARNING)
874         else:
875             logger.setLevel(logging.INFO)
876
877         if dbdir and testparm:
878             logger.warning("both dbdir and testparm specified, ignoring dbdir.")
879             dbdir = None
880
881         lp = sambaopts.get_loadparm()
882
883         s3conf = s3param.get_context()
884
885         if sambaopts.realm:
886             s3conf.set("realm", sambaopts.realm)
887
888         if targetdir is not None:
889             if not os.path.isdir(targetdir):
890                 os.mkdir(targetdir)
891
892         eadb = True
893         if use_xattrs == "yes":
894             eadb = False
895         elif use_xattrs == "auto" and not s3conf.get("posix:eadb"):
896             if targetdir:
897                 tmpfile = tempfile.NamedTemporaryFile(dir=os.path.abspath(targetdir))
898             else:
899                 tmpfile = tempfile.NamedTemporaryFile(dir=os.path.abspath(os.path.dirname(lp.get("private dir"))))
900             try:
901                 try:
902                     samba.ntacls.setntacl(lp, tmpfile.name,
903                                 "O:S-1-5-32G:S-1-5-32", "S-1-5-32", "native")
904                     eadb = False
905                 except Exception:
906                     # FIXME: Don't catch all exceptions here
907                     logger.info("You are not root or your system do not support xattr, using tdb backend for attributes. "
908                                 "If you intend to use this provision in production, rerun the script as root on a system supporting xattrs.")
909             finally:
910                 tmpfile.close()
911
912         # Set correct default values from dbdir or testparm
913         paths = {}
914         if dbdir:
915             paths["state directory"] = dbdir
916             paths["private dir"] = dbdir
917             paths["lock directory"] = dbdir
918             paths["smb passwd file"] = dbdir + "/smbpasswd"
919         else:
920             paths["state directory"] = get_testparm_var(testparm, smbconf, "state directory")
921             paths["private dir"] = get_testparm_var(testparm, smbconf, "private dir")
922             paths["smb passwd file"] = get_testparm_var(testparm, smbconf, "smb passwd file")
923             paths["lock directory"] = get_testparm_var(testparm, smbconf, "lock directory")
924             # "testparm" from Samba 3 < 3.4.x is not aware of the parameter
925             # "state directory", instead make use of "lock directory"
926             if len(paths["state directory"]) == 0:
927                 paths["state directory"] = paths["lock directory"]
928
929         for p in paths:
930             s3conf.set(p, paths[p])
931     
932         # load smb.conf parameters
933         logger.info("Reading smb.conf")
934         s3conf.load(smbconf)
935         samba3 = Samba3(smbconf, s3conf)
936     
937         logger.info("Provisioning")
938         upgrade_from_samba3(samba3, logger, targetdir, session_info=system_session(), 
939                             useeadb=eadb, dns_backend=dns_backend)
940
941 class cmd_domain(SuperCommand):
942     """Domain management"""
943
944     subcommands = {}
945     subcommands["demote"] = cmd_domain_demote()
946     if type(cmd_domain_export_keytab).__name__ != 'NoneType':
947         subcommands["exportkeytab"] = cmd_domain_export_keytab()
948     subcommands["info"] = cmd_domain_info()
949     subcommands["join"] = cmd_domain_join()
950     subcommands["level"] = cmd_domain_level()
951     subcommands["passwordsettings"] = cmd_domain_passwordsettings()
952     subcommands["classicupgrade"] = cmd_domain_classicupgrade()
953     subcommands["samba3upgrade"] = cmd_domain_classicupgrade()