samba-tool: add a function to cleanly demote a DC
[Samba/gebeck_regimport.git] / source4 / scripting / python / samba / netcmd / domain.py
bloba23785f945f21403e3276ed4b0ad8c6716f8ee98
1 #!/usr/bin/env python
3 # domain management
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
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.
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.
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/>.
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
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)
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
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()
74 class cmd_domain_export_keytab(Command):
75 """Dumps kerberos keys of the domain into a keytab"""
77 synopsis = "%prog <keytab> [options]"
79 takes_options = [
80 Option("--principal", help="extract only this principal", type=str),
83 takes_args = ["keytab"]
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)
90 class cmd_domain_info(Command):
91 """Print basic info about a domain and the DC passed as parameter"""
93 synopsis = "%prog domain info <ip_address> [options]"
95 takes_options = [
98 takes_args = ["address"]
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 + "'!")
116 class cmd_domain_join(Command):
117 """Joins domain as either member or backup domain controller"""
119 synopsis = "%prog <dnsdomain> [DC|RODC|MEMBER|SUBDOMAIN] [options]"
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)")
133 takes_args = ["domain", "role?"]
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)
142 if site is None:
143 site = "Default-First-Site-Name"
145 netbios_name = lp.get("netbios name")
147 if not role is None:
148 role = role.upper()
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)
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)
183 class cmd_domain_demote(Command):
184 """Demote ourselves from the role of Domain Controller"""
186 synopsis = "%prog [options]"
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),
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)
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")
207 if (len(res) == 1):
208 raise CommandError("You are the latest server in the domain")
210 server = None
211 for e in res:
212 if str(e["name"]).lower() != netbios_name.lower():
213 server = e["dnsHostName"]
214 break
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)
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)
227 dsa_options = int(str(msg[0]['options']))
230 print "Desactivating inbound replication"
232 nmsg = ldb.Message()
233 nmsg.dn = msg[0].dn
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)
239 if not (dsa_options & DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL) and not samdb.am_rodc():
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)
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"]))
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)
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())
281 olduac = uac
283 uac ^= (UF_SERVER_TRUST_ACCOUNT|UF_TRUSTED_FOR_DELEGATION)
284 uac |= UF_WORKSTATION_TRUST_ACCOUNT
286 msg = ldb.Message()
287 msg.dn = dc_dn
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)
300 raise CommandError("Error while changing account control", e)
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
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)
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)
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)
326 msg = ldb.Message()
327 msg.dn = dc_dn
329 msg["userAccountControl"] = ldb.MessageElement("%d" % uac,
330 ldb.FLAG_MOD_REPLACE,
331 "userAccountControl")
333 remote_samdb.modify(msg)
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))
339 newrdn = "%s-%d" % (rdn, i)
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)
350 msg = ldb.Message()
351 msg.dn = dc_dn
353 msg["userAccountControl"] = ldb.MessageElement("%d" % uac,
354 ldb.FLAG_MOD_REPLACE,
355 "userAccountControl")
357 remote_samdb.modify(msg)
358 raise CommandError("Error while renaming %s to %s" % (str(dc_dn), str(newdn)), e)
361 server_dsa_dn = samdb.get_serverName()
362 domain = remote_samdb.get_root_basedn()
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)
372 msg = ldb.Message()
373 msg.dn = newdn
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)
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
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
402 print "Demote successfull"
406 class cmd_domain_level(Command):
407 """Raises domain and forest function levels"""
409 synopsis = "%prog (show|raise <options>) [options]"
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)")
421 takes_args = ["subcommand"]
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)
428 samdb = SamDB(url=H, session_info=system_session(),
429 credentials=creds, lp=lp)
431 domain_dn = samdb.domain_dn()
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
437 res_domain = samdb.search(domain_dn, scope=ldb.SCOPE_BASE,
438 attrs=["msDS-Behavior-Version", "nTMixedDomain"])
439 assert len(res_domain) == 1
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
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])
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])
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!")
465 except KeyError:
466 raise CommandError("Could not retrieve the actual domain, forest level and/or lowest DC function level!")
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)!")
477 self.message("")
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)
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)
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)
521 elif subcommand == "raise":
522 msgs = []
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
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!")
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!")
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
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
577 level_domain = new_level_domain
578 msgs.append("Domain function level changed!")
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)
605 class cmd_domain_passwordsettings(Command):
606 """Sets password settings
608 Password complexity, history length, minimum password length, the minimum
609 and maximum password age) on a Samba4 server.
612 synopsis = "%prog (show|set <options>) [options]"
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),
632 takes_args = ["subcommand"]
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)
641 samdb = SamDB(url=H, session_info=system_session(),
642 credentials=creds, lp=lp)
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)
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)
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!")
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!")
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")
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)
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!")
711 m["pwdHistoryLength"] = ldb.MessageElement(str(pwd_hist_len),
712 ldb.FLAG_MOD_REPLACE, "pwdHistoryLength")
713 msgs.append("Password history length changed!")
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)
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!")
724 m["minPwdLength"] = ldb.MessageElement(str(min_pwd_len),
725 ldb.FLAG_MOD_REPLACE, "minPwdLength")
726 msgs.append("Minimum password length changed!")
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)
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!")
737 # days -> ticks
738 min_pwd_age_ticks = -int(min_pwd_age * (24 * 60 * 60 * 1e7))
740 m["minPwdAge"] = ldb.MessageElement(str(min_pwd_age_ticks),
741 ldb.FLAG_MOD_REPLACE, "minPwdAge")
742 msgs.append("Minimum password age changed!")
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)
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!")
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))
759 m["maxPwdAge"] = ldb.MessageElement(str(max_pwd_age_ticks),
760 ldb.FLAG_MOD_REPLACE, "maxPwdAge")
761 msgs.append("Maximum password age changed!")
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))
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)
775 class cmd_domain_samba3upgrade(Command):
776 """Upgrade from Samba3 database to Samba4 AD database.
778 Specify either a directory with all samba3 databases and state files (with --dbdir) or
779 samba3 testparm utility (with --testparm).
782 synopsis = "%prog [options] <samba3_smb_conf>"
784 takes_optiongroups = {
785 "sambaopts": options.SambaOptions,
786 "versionopts": options.VersionOptions
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"),
802 takes_args = ["smbconf"]
804 def run(self, smbconf=None, targetdir=None, dbdir=None, testparm=None,
805 quiet=False, verbose=False, use_xattrs=None, sambaopts=None, versionopts=None):
807 if not os.path.exists(smbconf):
808 raise CommandError("File %s does not exist" % smbconf)
810 if testparm and not os.path.exists(testparm):
811 raise CommandError("Testparm utility %s does not exist" % testparm)
813 if dbdir and not os.path.exists(dbdir):
814 raise CommandError("Directory %s does not exist" % dbdir)
816 if not dbdir and not testparm:
817 raise CommandError("Please specify either dbdir or testparm")
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)
827 if dbdir and testparm:
828 logger.warning("both dbdir and testparm specified, ignoring dbdir.")
829 dbdir = None
831 lp = sambaopts.get_loadparm()
833 s3conf = s3param.get_context()
835 if sambaopts.realm:
836 s3conf.set("realm", sambaopts.realm)
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()
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"]
871 for p in paths:
872 s3conf.set(p, paths[p])
874 # load smb.conf parameters
875 logger.info("Reading smb.conf")
876 s3conf.load(smbconf)
877 samba3 = Samba3(smbconf, s3conf)
879 logger.info("Provisioning")
880 upgrade_from_samba3(samba3, logger, targetdir, session_info=system_session(),
881 useeadb=eadb)
883 class cmd_domain(SuperCommand):
884 """Domain management"""
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()