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