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