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