samba.netcmd.domain: Just catch ImportError, not any parsing errors in cmd_domain_exp...
[samba.git] / python / samba / netcmd / domain.py
1 # domain management
2 #
3 # Copyright Matthias Dieter Wallnoefer 2009
4 # Copyright Andrew Kroeger 2009
5 # Copyright Jelmer Vernooij 2007-2012
6 # Copyright Giampaolo Lauria 2011
7 # Copyright Matthieu Patou <mat@matws.net> 2011
8 # Copyright Andrew Bartlett 2008
9 # Copyright Stefan Metzmacher 2012
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 import samba.getopt as options
26 import ldb
27 import string
28 import os
29 import sys
30 import tempfile
31 import logging
32 from samba.net import Net, LIBNET_JOIN_AUTOMATIC
33 import samba.ntacls
34 from samba.join import join_RODC, join_DC, join_subdomain
35 from samba.auth import system_session
36 from samba.samdb import SamDB
37 from samba.dcerpc import drsuapi
38 from samba.dcerpc.samr import DOMAIN_PASSWORD_COMPLEX, DOMAIN_PASSWORD_STORE_CLEARTEXT
39 from samba.netcmd import (
40     Command,
41     CommandError,
42     SuperCommand,
43     Option
44     )
45 from samba.netcmd.common import netcmd_get_domain_infos_via_cldap
46 from samba.samba3 import Samba3
47 from samba.samba3 import param as s3param
48 from samba.upgrade import upgrade_from_samba3
49 from samba.drs_utils import (
50                             sendDsReplicaSync, drsuapi_connect, drsException,
51                             sendRemoveDsServer)
52
53
54 from samba.dsdb import (
55     DS_DOMAIN_FUNCTION_2000,
56     DS_DOMAIN_FUNCTION_2003,
57     DS_DOMAIN_FUNCTION_2003_MIXED,
58     DS_DOMAIN_FUNCTION_2008,
59     DS_DOMAIN_FUNCTION_2008_R2,
60     DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL,
61     DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL,
62     UF_WORKSTATION_TRUST_ACCOUNT,
63     UF_SERVER_TRUST_ACCOUNT,
64     UF_TRUSTED_FOR_DELEGATION
65     )
66
67 from samba.credentials import DONT_USE_KERBEROS
68 from samba.provision import (
69     provision,
70     ProvisioningError
71     )
72
73 from samba.provision.common import (
74     FILL_FULL,
75     FILL_NT4SYNC,
76     FILL_DRS
77 )
78
79 def get_testparm_var(testparm, smbconf, varname):
80     cmd = "%s -s -l --parameter-name='%s' %s 2>/dev/null" % (testparm, varname, smbconf)
81     output = os.popen(cmd, 'r').readline()
82     return output.strip()
83
84 try:
85    import samba.dckeytab
86 except ImportError:
87    cmd_domain_export_keytab = None
88 else:
89    class cmd_domain_export_keytab(Command):
90        """Dump Kerberos keys of the domain into a keytab."""
91
92        synopsis = "%prog <keytab> [options]"
93
94        takes_optiongroups = {
95            "sambaopts": options.SambaOptions,
96            "credopts": options.CredentialsOptions,
97            "versionopts": options.VersionOptions,
98            }
99
100        takes_options = [
101            Option("--principal", help="extract only this principal", type=str),
102            ]
103
104        takes_args = ["keytab"]
105
106        def run(self, keytab, credopts=None, sambaopts=None, versionopts=None, principal=None):
107            lp = sambaopts.get_loadparm()
108            net = Net(None, lp)
109            net.export_keytab(keytab=keytab, principal=principal)
110
111
112 class cmd_domain_info(Command):
113     """Print basic info about a domain and the DC passed as parameter."""
114
115     synopsis = "%prog <ip_address> [options]"
116
117     takes_options = [
118         ]
119
120     takes_optiongroups = {
121         "sambaopts": options.SambaOptions,
122         "credopts": options.CredentialsOptions,
123         "versionopts": options.VersionOptions,
124         }
125
126     takes_args = ["address"]
127
128     def run(self, address, credopts=None, sambaopts=None, versionopts=None):
129         lp = sambaopts.get_loadparm()
130         try:
131             res = netcmd_get_domain_infos_via_cldap(lp, None, address)
132         except RuntimeError:
133             raise CommandError("Invalid IP address '" + address + "'!")
134         self.outf.write("Forest           : %s\n" % res.forest)
135         self.outf.write("Domain           : %s\n" % res.dns_domain)
136         self.outf.write("Netbios domain   : %s\n" % res.domain_name)
137         self.outf.write("DC name          : %s\n" % res.pdc_dns_name)
138         self.outf.write("DC netbios name  : %s\n" % res.pdc_name)
139         self.outf.write("Server site      : %s\n" % res.server_site)
140         self.outf.write("Client site      : %s\n" % res.client_site)
141
142
143 class cmd_domain_provision(Command):
144     """Provision a domain."""
145
146     synopsis = "%prog [options]"
147
148     takes_optiongroups = {
149         "sambaopts": options.SambaOptions,
150         "versionopts": options.VersionOptions,
151     }
152
153     takes_options = [
154          Option("--interactive", help="Ask for names", action="store_true"),
155          Option("--domain", type="string", metavar="DOMAIN",
156                 help="set domain"),
157          Option("--domain-guid", type="string", metavar="GUID",
158                 help="set domainguid (otherwise random)"),
159          Option("--domain-sid", type="string", metavar="SID",
160                 help="set domainsid (otherwise random)"),
161          Option("--ntds-guid", type="string", metavar="GUID",
162                 help="set NTDS object GUID (otherwise random)"),
163          Option("--invocationid", type="string", metavar="GUID",
164                 help="set invocationid (otherwise random)"),
165          Option("--host-name", type="string", metavar="HOSTNAME",
166                 help="set hostname"),
167          Option("--host-ip", type="string", metavar="IPADDRESS",
168                 help="set IPv4 ipaddress"),
169          Option("--host-ip6", type="string", metavar="IP6ADDRESS",
170                 help="set IPv6 ipaddress"),
171          Option("--site", type="string", metavar="SITENAME",
172                 help="set site name"),
173          Option("--adminpass", type="string", metavar="PASSWORD",
174                 help="choose admin password (otherwise random)"),
175          Option("--krbtgtpass", type="string", metavar="PASSWORD",
176                 help="choose krbtgt password (otherwise random)"),
177          Option("--machinepass", type="string", metavar="PASSWORD",
178                 help="choose machine password (otherwise random)"),
179          Option("--dns-backend", type="choice", metavar="NAMESERVER-BACKEND",
180                 choices=["SAMBA_INTERNAL", "BIND9_FLATFILE", "BIND9_DLZ", "NONE"],
181                 help="The DNS server backend. SAMBA_INTERNAL is the builtin name server (default), "
182                      "BIND9_FLATFILE uses bind9 text database to store zone information, "
183                      "BIND9_DLZ uses samba4 AD to store zone information, "
184                      "NONE skips the DNS setup entirely (not recommended)",
185                 default="SAMBA_INTERNAL"),
186          Option("--dnspass", type="string", metavar="PASSWORD",
187                 help="choose dns password (otherwise random)"),
188          Option("--ldapadminpass", type="string", metavar="PASSWORD",
189                 help="choose password to set between Samba and it's LDAP backend (otherwise random)"),
190          Option("--root", type="string", metavar="USERNAME",
191                 help="choose 'root' unix username"),
192          Option("--nobody", type="string", metavar="USERNAME",
193                 help="choose 'nobody' user"),
194          Option("--users", type="string", metavar="GROUPNAME",
195                 help="choose 'users' group"),
196          Option("--quiet", help="Be quiet", action="store_true"),
197          Option("--blank", action="store_true",
198                 help="do not add users or groups, just the structure"),
199          Option("--ldap-backend-type", type="choice", metavar="LDAP-BACKEND-TYPE",
200                 help="Test initialisation support for unsupported LDAP backend type (fedora-ds or openldap) DO NOT USE",
201                 choices=["fedora-ds", "openldap"]),
202          Option("--server-role", type="choice", metavar="ROLE",
203                 choices=["domain controller", "dc", "member server", "member", "standalone"],
204                 help="The server role (domain controller | dc | member server | member | standalone). Default is dc.",
205                 default="domain controller"),
206          Option("--function-level", type="choice", metavar="FOR-FUN-LEVEL",
207                 choices=["2000", "2003", "2008", "2008_R2"],
208                 help="The domain and forest function level (2000 | 2003 | 2008 | 2008_R2 - always native). Default is (Windows) 2003 Native.",
209                 default="2003"),
210          Option("--next-rid", type="int", metavar="NEXTRID", default=1000,
211                 help="The initial nextRid value (only needed for upgrades).  Default is 1000."),
212          Option("--partitions-only",
213                 help="Configure Samba's partitions, but do not modify them (ie, join a BDC)", action="store_true"),
214          Option("--targetdir", type="string", metavar="DIR",
215                 help="Set target directory"),
216          Option("--ol-mmr-urls", type="string", metavar="LDAPSERVER",
217                 help="List of LDAP-URLS [ ldap://<FQHN>:<PORT>/  (where <PORT> has to be different than 389!) ] separated with comma (\",\") for use with OpenLDAP-MMR (Multi-Master-Replication), e.g.: \"ldap://s4dc1:9000,ldap://s4dc2:9000\""),
218          Option("--use-xattrs", type="choice", choices=["yes", "no", "auto"], 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"),
219          Option("--use-ntvfs", action="store_true", help="Use NTVFS for the fileserver (default = no)"),
220          Option("--use-rfc2307", action="store_true", help="Use AD to store posix attributes (default = no)"),
221         ]
222
223     openldap_options = [
224         Option("--ldap-dryrun-mode", help="Configure LDAP backend, but do not run any binaries and exit early.  Used only for the test environment.  DO NOT USE",
225                action="store_true"),
226         Option("--slapd-path", type="string", metavar="SLAPD-PATH",
227                help="Path to slapd for LDAP backend [e.g.:'/usr/local/libexec/slapd']. Required for Setup with LDAP-Backend. OpenLDAP Version >= 2.4.17 should be used."),
228         Option("--ldap-backend-extra-port", type="int", metavar="LDAP-BACKEND-EXTRA-PORT", help="Additional TCP port for LDAP backend server (to use for replication)"),
229         Option("--ldap-backend-forced-uri", type="string", metavar="LDAP-BACKEND-FORCED-URI",
230                help="Force the LDAP backend connection to be to a particular URI.  Use this ONLY for 'existing' backends, or when debugging the interaction with the LDAP backend and you need to intercept the LDA"),
231         Option("--ldap-backend-nosync", help="Configure LDAP backend not to call fsync() (for performance in test environments)", action="store_true"),
232         ]
233
234     if os.getenv('TEST_LDAP', "no") == "yes":
235         takes_options.extend(openldap_options)
236
237     takes_args = []
238
239     def run(self, sambaopts=None, versionopts=None,
240             interactive=None,
241             domain=None,
242             domain_guid=None,
243             domain_sid=None,
244             ntds_guid=None,
245             invocationid=None,
246             host_name=None,
247             host_ip=None,
248             host_ip6=None,
249             adminpass=None,
250             site=None,
251             krbtgtpass=None,
252             machinepass=None,
253             dns_backend=None,
254             dns_forwarder=None,
255             dnspass=None,
256             ldapadminpass=None,
257             root=None,
258             nobody=None,
259             users=None,
260             quiet=None,
261             blank=None,
262             ldap_backend_type=None,
263             server_role=None,
264             function_level=None,
265             next_rid=None,
266             partitions_only=None,
267             targetdir=None,
268             ol_mmr_urls=None,
269             use_xattrs=None,
270             slapd_path=None,
271             use_ntvfs=None,
272             use_rfc2307=None,
273             ldap_backend_nosync=None,
274             ldap_backend_extra_port=None,
275             ldap_backend_forced_uri=None,
276             ldap_dryrun_mode=None):
277
278         self.logger = self.get_logger("provision")
279         if quiet:
280             self.logger.setLevel(logging.WARNING)
281         else:
282             self.logger.setLevel(logging.INFO)
283
284         lp = sambaopts.get_loadparm()
285         smbconf = lp.configfile
286
287         if dns_forwarder is not None:
288             suggested_forwarder = dns_forwarder
289         else:
290             suggested_forwarder = self._get_nameserver_ip()
291             if suggested_forwarder is None:
292                 suggested_forwarder = "none"
293
294         if len(self.raw_argv) == 1:
295             interactive = True
296
297         if interactive:
298             from getpass import getpass
299             import socket
300
301             def ask(prompt, default=None):
302                 if default is not None:
303                     print "%s [%s]: " % (prompt, default),
304                 else:
305                     print "%s: " % (prompt,),
306                 return sys.stdin.readline().rstrip("\n") or default
307
308             try:
309                 default = socket.getfqdn().split(".", 1)[1].upper()
310             except IndexError:
311                 default = None
312             realm = ask("Realm", default)
313             if realm in (None, ""):
314                 raise CommandError("No realm set!")
315
316             try:
317                 default = realm.split(".")[0]
318             except IndexError:
319                 default = None
320             domain = ask("Domain", default)
321             if domain is None:
322                 raise CommandError("No domain set!")
323
324             server_role = ask("Server Role (dc, member, standalone)", "dc")
325
326             dns_backend = ask("DNS backend (SAMBA_INTERNAL, BIND9_FLATFILE, BIND9_DLZ, NONE)", "SAMBA_INTERNAL")
327             if dns_backend in (None, ''):
328                 raise CommandError("No DNS backend set!")
329
330             if dns_backend == "SAMBA_INTERNAL":
331                 dns_forwarder = ask("DNS forwarder IP address (write 'none' to disable forwarding)", suggested_forwarder)
332                 if dns_forwarder.lower() in (None, 'none'):
333                     suggested_forwarder = None
334                     dns_forwarder = None
335
336             while True:
337                 adminpassplain = getpass("Administrator password: ")
338                 if not adminpassplain:
339                     self.errf.write("Invalid administrator password.\n")
340                 else:
341                     adminpassverify = getpass("Retype password: ")
342                     if not adminpassplain == adminpassverify:
343                         self.errf.write("Sorry, passwords do not match.\n")
344                     else:
345                         adminpass = adminpassplain
346                         break
347
348         else:
349             realm = sambaopts._lp.get('realm')
350             if realm is None:
351                 raise CommandError("No realm set!")
352             if domain is None:
353                 raise CommandError("No domain set!")
354
355         if not adminpass:
356             self.logger.info("Administrator password will be set randomly!")
357
358         if function_level == "2000":
359             dom_for_fun_level = DS_DOMAIN_FUNCTION_2000
360         elif function_level == "2003":
361             dom_for_fun_level = DS_DOMAIN_FUNCTION_2003
362         elif function_level == "2008":
363             dom_for_fun_level = DS_DOMAIN_FUNCTION_2008
364         elif function_level == "2008_R2":
365             dom_for_fun_level = DS_DOMAIN_FUNCTION_2008_R2
366
367         if dns_backend == "SAMBA_INTERNAL" and dns_forwarder is None:
368             dns_forwarder = suggested_forwarder
369
370         samdb_fill = FILL_FULL
371         if blank:
372             samdb_fill = FILL_NT4SYNC
373         elif partitions_only:
374             samdb_fill = FILL_DRS
375
376         if targetdir is not None:
377             if not os.path.isdir(targetdir):
378                 os.mkdir(targetdir)
379
380         eadb = True
381
382         if use_xattrs == "yes":
383             eadb = False
384         elif use_xattrs == "auto" and not lp.get("posix:eadb"):
385             if targetdir:
386                 file = tempfile.NamedTemporaryFile(dir=os.path.abspath(targetdir))
387             else:
388                 file = tempfile.NamedTemporaryFile(dir=os.path.abspath(os.path.dirname(lp.get("private dir"))))
389             try:
390                 try:
391                     samba.ntacls.setntacl(lp, file.name,
392                                           "O:S-1-5-32G:S-1-5-32", "S-1-5-32", "native")
393                     eadb = False
394                 except Exception:
395                     self.logger.info("You are not root or your system do not support xattr, using tdb backend for attributes. ")
396             finally:
397                 file.close()
398
399         if eadb:
400             self.logger.info("not using extended attributes to store ACLs and other metadata. If you intend to use this provision in production, rerun the script as root on a system supporting xattrs.")
401         if ldap_backend_type == "existing":
402             if dap_backend_forced_uri is not None:
403                 logger.warn("You have specified to use an existing LDAP server as the backend, please make sure an LDAP server is running at %s" % ldap_backend_forced_uri)
404             else:
405                 logger.info("You have specified to use an existing LDAP server as the backend, please make sure an LDAP server is running at the default location")
406         else:
407             if ldap_backend_forced_uri is not None:
408                 logger.warn("You have specified to use an fixed URI %s for connecting to your LDAP server backend.  This is NOT RECOMMENDED, as our default communiation over ldapi:// is more secure and much less")
409
410         session = system_session()
411         try:
412             result = provision(self.logger,
413                   session, smbconf=smbconf, targetdir=targetdir,
414                   samdb_fill=samdb_fill, realm=realm, domain=domain,
415                   domainguid=domain_guid, domainsid=domain_sid,
416                   hostname=host_name,
417                   hostip=host_ip, hostip6=host_ip6,
418                   sitename=site, ntdsguid=ntds_guid,
419                   invocationid=invocationid, adminpass=adminpass,
420                   krbtgtpass=krbtgtpass, machinepass=machinepass,
421                   dns_backend=dns_backend, dns_forwarder=dns_forwarder,
422                   dnspass=dnspass, root=root, nobody=nobody,
423                   users=users,
424                   serverrole=server_role, dom_for_fun_level=dom_for_fun_level,
425                   backend_type=ldap_backend_type,
426                   ldapadminpass=ldapadminpass, ol_mmr_urls=ol_mmr_urls, slapd_path=slapd_path,
427                   useeadb=eadb, next_rid=next_rid, lp=lp, use_ntvfs=use_ntvfs,
428                   use_rfc2307=use_rfc2307, skip_sysvolacl=False,
429                   ldap_backend_extra_port=ldap_backend_extra_port,
430                   ldap_backend_forced_uri=ldap_backend_forced_uri,
431                   nosync=ldap_backend_nosync, ldap_dryrun_mode=ldap_dryrun_mode)
432
433         except ProvisioningError, e:
434             raise CommandError("Provision failed", e)
435
436         result.report_logger(self.logger)
437
438     def _get_nameserver_ip(self):
439         """Grab the nameserver IP address from /etc/resolv.conf."""
440         from os import path
441         RESOLV_CONF="/etc/resolv.conf"
442
443         if not path.isfile(RESOLV_CONF):
444             self.logger.warning("Failed to locate %s" % RESOLV_CONF)
445             return None
446
447         handle = None
448         try:
449             handle = open(RESOLV_CONF, 'r')
450             for line in handle:
451                 if not line.startswith('nameserver'):
452                     continue
453                 # we want the last non-space continuous string of the line
454                 return line.strip().split()[-1]
455         finally:
456             if handle is not None:
457                 handle.close()
458
459         self.logger.warning("No nameserver found in %s" % RESOLV_CONF)
460
461
462 class cmd_domain_dcpromo(Command):
463     """Promote an existing domain member or NT4 PDC to an AD DC."""
464
465     synopsis = "%prog <dnsdomain> [DC|RODC] [options]"
466
467     takes_optiongroups = {
468         "sambaopts": options.SambaOptions,
469         "versionopts": options.VersionOptions,
470         "credopts": options.CredentialsOptions,
471     }
472
473     takes_options = [
474         Option("--server", help="DC to join", type=str),
475         Option("--site", help="site to join", type=str),
476         Option("--targetdir", help="where to store provision", type=str),
477         Option("--domain-critical-only",
478                help="only replicate critical domain objects",
479                action="store_true"),
480         Option("--machinepass", type=str, metavar="PASSWORD",
481                help="choose machine password (otherwise random)"),
482         Option("--use-ntvfs", help="Use NTVFS for the fileserver (default = no)",
483                action="store_true"),
484         Option("--dns-backend", type="choice", metavar="NAMESERVER-BACKEND",
485                choices=["SAMBA_INTERNAL", "BIND9_DLZ", "NONE"],
486                help="The DNS server backend. SAMBA_INTERNAL is the builtin name server (default), "
487                    "BIND9_DLZ uses samba4 AD to store zone information, "
488                    "NONE skips the DNS setup entirely (this DC will not be a DNS server)",
489                default="SAMBA_INTERNAL"),
490         Option("--quiet", help="Be quiet", action="store_true"),
491         Option("--verbose", help="Be verbose", action="store_true")
492         ]
493
494     takes_args = ["domain", "role?"]
495
496     def run(self, domain, role=None, sambaopts=None, credopts=None,
497             versionopts=None, server=None, site=None, targetdir=None,
498             domain_critical_only=False, parent_domain=None, machinepass=None,
499             use_ntvfs=False, dns_backend=None,
500             quiet=False, verbose=False):
501         lp = sambaopts.get_loadparm()
502         creds = credopts.get_credentials(lp)
503         net = Net(creds, lp, server=credopts.ipaddress)
504
505         if site is None:
506             site = "Default-First-Site-Name"
507
508         logger = self.get_logger()
509         if verbose:
510             logger.setLevel(logging.DEBUG)
511         elif quiet:
512             logger.setLevel(logging.WARNING)
513         else:
514             logger.setLevel(logging.INFO)
515
516         netbios_name = lp.get("netbios name")
517
518         if not role is None:
519             role = role.upper()
520
521         if role == "DC":
522             join_DC(logger=logger, server=server, creds=creds, lp=lp, domain=domain,
523                     site=site, netbios_name=netbios_name, targetdir=targetdir,
524                     domain_critical_only=domain_critical_only,
525                     machinepass=machinepass, use_ntvfs=use_ntvfs,
526                     dns_backend=dns_backend,
527                     promote_existing=True)
528         elif role == "RODC":
529             join_RODC(logger=logger, server=server, creds=creds, lp=lp, domain=domain,
530                       site=site, netbios_name=netbios_name, targetdir=targetdir,
531                       domain_critical_only=domain_critical_only,
532                       machinepass=machinepass, use_ntvfs=use_ntvfs, dns_backend=dns_backend,
533                       promote_existing=True)
534         else:
535             raise CommandError("Invalid role '%s' (possible values: DC, RODC)" % role)
536
537
538 class cmd_domain_join(Command):
539     """Join domain as either member or backup domain controller."""
540
541     synopsis = "%prog <dnsdomain> [DC|RODC|MEMBER|SUBDOMAIN] [options]"
542
543     takes_optiongroups = {
544         "sambaopts": options.SambaOptions,
545         "versionopts": options.VersionOptions,
546         "credopts": options.CredentialsOptions,
547     }
548
549     takes_options = [
550         Option("--server", help="DC to join", type=str),
551         Option("--site", help="site to join", type=str),
552         Option("--targetdir", help="where to store provision", type=str),
553         Option("--parent-domain", help="parent domain to create subdomain under", type=str),
554         Option("--domain-critical-only",
555                help="only replicate critical domain objects",
556                action="store_true"),
557         Option("--machinepass", type=str, metavar="PASSWORD",
558                help="choose machine password (otherwise random)"),
559         Option("--adminpass", type="string", metavar="PASSWORD",
560                help="choose adminstrator password when joining as a subdomain (otherwise random)"),
561         Option("--use-ntvfs", help="Use NTVFS for the fileserver (default = no)",
562                action="store_true"),
563         Option("--dns-backend", type="choice", metavar="NAMESERVER-BACKEND",
564                choices=["SAMBA_INTERNAL", "BIND9_DLZ", "NONE"],
565                help="The DNS server backend. SAMBA_INTERNAL is the builtin name server (default), "
566                    "BIND9_DLZ uses samba4 AD to store zone information, "
567                    "NONE skips the DNS setup entirely (this DC will not be a DNS server)",
568                default="SAMBA_INTERNAL"),
569         Option("--quiet", help="Be quiet", action="store_true"),
570         Option("--verbose", help="Be verbose", action="store_true")
571        ]
572
573     takes_args = ["domain", "role?"]
574
575     def run(self, domain, role=None, sambaopts=None, credopts=None,
576             versionopts=None, server=None, site=None, targetdir=None,
577             domain_critical_only=False, parent_domain=None, machinepass=None,
578             use_ntvfs=False, dns_backend=None, adminpass=None,
579             quiet=False, verbose=False):
580         lp = sambaopts.get_loadparm()
581         creds = credopts.get_credentials(lp)
582         net = Net(creds, lp, server=credopts.ipaddress)
583
584         if site is None:
585             site = "Default-First-Site-Name"
586
587         logger = self.get_logger()
588         if verbose:
589             logger.setLevel(logging.DEBUG)
590         elif quiet:
591             logger.setLevel(logging.WARNING)
592         else:
593             logger.setLevel(logging.INFO)
594
595         netbios_name = lp.get("netbios name")
596
597         if not role is None:
598             role = role.upper()
599
600         if role is None or role == "MEMBER":
601             (join_password, sid, domain_name) = net.join_member(
602                 domain, netbios_name, LIBNET_JOIN_AUTOMATIC,
603                 machinepass=machinepass)
604
605             self.errf.write("Joined domain %s (%s)\n" % (domain_name, sid))
606         elif role == "DC":
607             join_DC(logger=logger, server=server, creds=creds, lp=lp, domain=domain,
608                     site=site, netbios_name=netbios_name, targetdir=targetdir,
609                     domain_critical_only=domain_critical_only,
610                     machinepass=machinepass, use_ntvfs=use_ntvfs, dns_backend=dns_backend)
611         elif role == "RODC":
612             join_RODC(logger=logger, server=server, creds=creds, lp=lp, domain=domain,
613                       site=site, netbios_name=netbios_name, targetdir=targetdir,
614                       domain_critical_only=domain_critical_only,
615                       machinepass=machinepass, use_ntvfs=use_ntvfs,
616                       dns_backend=dns_backend)
617         elif role == "SUBDOMAIN":
618             if not adminpass:
619                 logger.info("Administrator password will be set randomly!")
620
621             netbios_domain = lp.get("workgroup")
622             if parent_domain is None:
623                 parent_domain = ".".join(domain.split(".")[1:])
624             join_subdomain(logger=logger, server=server, creds=creds, lp=lp, dnsdomain=domain,
625                            parent_domain=parent_domain, site=site,
626                            netbios_name=netbios_name, netbios_domain=netbios_domain,
627                            targetdir=targetdir, machinepass=machinepass,
628                            use_ntvfs=use_ntvfs, dns_backend=dns_backend,
629                            adminpass=adminpass)
630         else:
631             raise CommandError("Invalid role '%s' (possible values: MEMBER, DC, RODC, SUBDOMAIN)" % role)
632
633
634 class cmd_domain_demote(Command):
635     """Demote ourselves from the role of Domain Controller."""
636
637     synopsis = "%prog [options]"
638
639     takes_options = [
640         Option("--server", help="DC to force replication before demote", type=str),
641         Option("--targetdir", help="where provision is stored", type=str),
642         ]
643
644     takes_optiongroups = {
645         "sambaopts": options.SambaOptions,
646         "credopts": options.CredentialsOptions,
647         "versionopts": options.VersionOptions,
648         }
649
650     def run(self, sambaopts=None, credopts=None,
651             versionopts=None, server=None, targetdir=None):
652         lp = sambaopts.get_loadparm()
653         creds = credopts.get_credentials(lp)
654         net = Net(creds, lp, server=credopts.ipaddress)
655
656         netbios_name = lp.get("netbios name")
657         samdb = SamDB(session_info=system_session(), credentials=creds, lp=lp)
658         if not server:
659             res = samdb.search(expression='(&(objectClass=computer)(serverReferenceBL=*))', attrs=["dnsHostName", "name"])
660             if (len(res) == 0):
661                 raise CommandError("Unable to search for servers")
662
663             if (len(res) == 1):
664                 raise CommandError("You are the latest server in the domain")
665
666             server = None
667             for e in res:
668                 if str(e["name"]).lower() != netbios_name.lower():
669                     server = e["dnsHostName"]
670                     break
671
672         ntds_guid = samdb.get_ntds_GUID()
673         msg = samdb.search(base=str(samdb.get_config_basedn()),
674             scope=ldb.SCOPE_SUBTREE, expression="(objectGUID=%s)" % ntds_guid,
675             attrs=['options'])
676         if len(msg) == 0 or "options" not in msg[0]:
677             raise CommandError("Failed to find options on %s" % ntds_guid)
678
679         ntds_dn = msg[0].dn
680         dsa_options = int(str(msg[0]['options']))
681
682         res = samdb.search(expression="(fSMORoleOwner=%s)" % str(ntds_dn),
683                             controls=["search_options:1:2"])
684
685         if len(res) != 0:
686             raise CommandError("Current DC is still the owner of %d role(s), use the role command to transfer roles to another DC" % len(res))
687
688         self.errf.write("Using %s as partner server for the demotion\n" %
689                         server)
690         (drsuapiBind, drsuapi_handle, supportedExtensions) = drsuapi_connect(server, lp, creds)
691
692         self.errf.write("Desactivating inbound replication\n")
693
694         nmsg = ldb.Message()
695         nmsg.dn = msg[0].dn
696
697         dsa_options |= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
698         nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
699         samdb.modify(nmsg)
700
701         if not (dsa_options & DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL) and not samdb.am_rodc():
702
703             self.errf.write("Asking partner server %s to synchronize from us\n"
704                             % server)
705             for part in (samdb.get_schema_basedn(),
706                             samdb.get_config_basedn(),
707                             samdb.get_root_basedn()):
708                 try:
709                     sendDsReplicaSync(drsuapiBind, drsuapi_handle, ntds_guid, str(part), drsuapi.DRSUAPI_DRS_WRIT_REP)
710                 except drsException, e:
711                     self.errf.write(
712                         "Error while demoting, "
713                         "re-enabling inbound replication\n")
714                     dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
715                     nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
716                     samdb.modify(nmsg)
717                     raise CommandError("Error while sending a DsReplicaSync for partion %s" % str(part), e)
718         try:
719             remote_samdb = SamDB(url="ldap://%s" % server,
720                                 session_info=system_session(),
721                                 credentials=creds, lp=lp)
722
723             self.errf.write("Changing userControl and container\n")
724             res = remote_samdb.search(base=str(remote_samdb.get_root_basedn()),
725                                 expression="(&(objectClass=user)(sAMAccountName=%s$))" %
726                                             netbios_name.upper(),
727                                 attrs=["userAccountControl"])
728             dc_dn = res[0].dn
729             uac = int(str(res[0]["userAccountControl"]))
730
731         except Exception, e:
732             self.errf.write(
733                 "Error while demoting, re-enabling inbound replication\n")
734             dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
735             nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
736             samdb.modify(nmsg)
737             raise CommandError("Error while changing account control", e)
738
739         if (len(res) != 1):
740             self.errf.write(
741                 "Error while demoting, re-enabling inbound replication")
742             dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
743             nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
744             samdb.modify(nmsg)
745             raise CommandError("Unable to find object with samaccountName = %s$"
746                                " in the remote dc" % netbios_name.upper())
747
748         olduac = uac
749
750         uac ^= (UF_SERVER_TRUST_ACCOUNT|UF_TRUSTED_FOR_DELEGATION)
751         uac |= UF_WORKSTATION_TRUST_ACCOUNT
752
753         msg = ldb.Message()
754         msg.dn = dc_dn
755
756         msg["userAccountControl"] = ldb.MessageElement("%d" % uac,
757                                                         ldb.FLAG_MOD_REPLACE,
758                                                         "userAccountControl")
759         try:
760             remote_samdb.modify(msg)
761         except Exception, e:
762             self.errf.write(
763                 "Error while demoting, re-enabling inbound replication")
764             dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
765             nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
766             samdb.modify(nmsg)
767
768             raise CommandError("Error while changing account control", e)
769
770         parent = msg.dn.parent()
771         rdn = str(res[0].dn)
772         rdn = string.replace(rdn, ",%s" % str(parent), "")
773         # Let's move to the Computer container
774         i = 0
775         newrdn = rdn
776
777         computer_dn = ldb.Dn(remote_samdb, "CN=Computers,%s" % str(remote_samdb.get_root_basedn()))
778         res = remote_samdb.search(base=computer_dn, expression=rdn, scope=ldb.SCOPE_ONELEVEL)
779
780         if (len(res) != 0):
781             res = remote_samdb.search(base=computer_dn, expression="%s-%d" % (rdn, i),
782                                         scope=ldb.SCOPE_ONELEVEL)
783             while(len(res) != 0 and i < 100):
784                 i = i + 1
785                 res = remote_samdb.search(base=computer_dn, expression="%s-%d" % (rdn, i),
786                                             scope=ldb.SCOPE_ONELEVEL)
787
788             if i == 100:
789                 self.errf.write(
790                     "Error while demoting, re-enabling inbound replication\n")
791                 dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
792                 nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
793                 samdb.modify(nmsg)
794
795                 msg = ldb.Message()
796                 msg.dn = dc_dn
797
798                 msg["userAccountControl"] = ldb.MessageElement("%d" % uac,
799                                                         ldb.FLAG_MOD_REPLACE,
800                                                         "userAccountControl")
801
802                 remote_samdb.modify(msg)
803
804                 raise CommandError("Unable to find a slot for renaming %s,"
805                                     " all names from %s-1 to %s-%d seemed used" %
806                                     (str(dc_dn), rdn, rdn, i - 9))
807
808             newrdn = "%s-%d" % (rdn, i)
809
810         try:
811             newdn = ldb.Dn(remote_samdb, "%s,%s" % (newrdn, str(computer_dn)))
812             remote_samdb.rename(dc_dn, newdn)
813         except Exception, e:
814             self.errf.write(
815                 "Error while demoting, re-enabling inbound replication\n")
816             dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
817             nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
818             samdb.modify(nmsg)
819
820             msg = ldb.Message()
821             msg.dn = dc_dn
822
823             msg["userAccountControl"] = ldb.MessageElement("%d" % uac,
824                                                     ldb.FLAG_MOD_REPLACE,
825                                                     "userAccountControl")
826
827             remote_samdb.modify(msg)
828             raise CommandError("Error while renaming %s to %s" % (str(dc_dn), str(newdn)), e)
829
830
831         server_dsa_dn = samdb.get_serverName()
832         domain = remote_samdb.get_root_basedn()
833
834         try:
835             sendRemoveDsServer(drsuapiBind, drsuapi_handle, server_dsa_dn, domain)
836         except drsException, e:
837             self.errf.write(
838                 "Error while demoting, re-enabling inbound replication\n")
839             dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
840             nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
841             samdb.modify(nmsg)
842
843             msg = ldb.Message()
844             msg.dn = newdn
845
846             msg["userAccountControl"] = ldb.MessageElement("%d" % uac,
847                                                     ldb.FLAG_MOD_REPLACE,
848                                                     "userAccountControl")
849             print str(dc_dn)
850             remote_samdb.modify(msg)
851             remote_samdb.rename(newdn, dc_dn)
852             raise CommandError("Error while sending a removeDsServer", e)
853
854         for s in ("CN=Enterprise,CN=Microsoft System Volumes,CN=System,CN=Configuration",
855                   "CN=%s,CN=Microsoft System Volumes,CN=System,CN=Configuration" % lp.get("realm"),
856                   "CN=Domain System Volumes (SYSVOL share),CN=File Replication Service,CN=System"):
857             try:
858                 remote_samdb.delete(ldb.Dn(remote_samdb,
859                                     "%s,%s,%s" % (str(rdn), s, str(remote_samdb.get_root_basedn()))))
860             except ldb.LdbError, l:
861                 pass
862
863         for s in ("CN=Enterprise,CN=NTFRS Subscriptions",
864                   "CN=%s, CN=NTFRS Subscriptions" % lp.get("realm"),
865                   "CN=Domain system Volumes (SYSVOL Share), CN=NTFRS Subscriptions",
866                   "CN=NTFRS Subscriptions"):
867             try:
868                 remote_samdb.delete(ldb.Dn(remote_samdb,
869                                     "%s,%s" % (s, str(newdn))))
870             except ldb.LdbError, l:
871                 pass
872
873         self.errf.write("Demote successfull\n")
874
875
876 class cmd_domain_level(Command):
877     """Raise domain and forest function levels."""
878
879     synopsis = "%prog (show|raise <options>) [options]"
880
881     takes_optiongroups = {
882         "sambaopts": options.SambaOptions,
883         "credopts": options.CredentialsOptions,
884         "versionopts": options.VersionOptions,
885         }
886
887     takes_options = [
888         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
889                metavar="URL", dest="H"),
890         Option("--quiet", help="Be quiet", action="store_true"),
891         Option("--forest-level", type="choice", choices=["2003", "2008", "2008_R2"],
892             help="The forest function level (2003 | 2008 | 2008_R2)"),
893         Option("--domain-level", type="choice", choices=["2003", "2008", "2008_R2"],
894             help="The domain function level (2003 | 2008 | 2008_R2)")
895             ]
896
897     takes_args = ["subcommand"]
898
899     def run(self, subcommand, H=None, forest_level=None, domain_level=None,
900             quiet=False, credopts=None, sambaopts=None, versionopts=None):
901         lp = sambaopts.get_loadparm()
902         creds = credopts.get_credentials(lp, fallback_machine=True)
903
904         samdb = SamDB(url=H, session_info=system_session(),
905             credentials=creds, lp=lp)
906
907         domain_dn = samdb.domain_dn()
908
909         res_forest = samdb.search("CN=Partitions,%s" % samdb.get_config_basedn(),
910           scope=ldb.SCOPE_BASE, attrs=["msDS-Behavior-Version"])
911         assert len(res_forest) == 1
912
913         res_domain = samdb.search(domain_dn, scope=ldb.SCOPE_BASE,
914           attrs=["msDS-Behavior-Version", "nTMixedDomain"])
915         assert len(res_domain) == 1
916
917         res_dc_s = samdb.search("CN=Sites,%s" % samdb.get_config_basedn(),
918           scope=ldb.SCOPE_SUBTREE, expression="(objectClass=nTDSDSA)",
919           attrs=["msDS-Behavior-Version"])
920         assert len(res_dc_s) >= 1
921
922         try:
923             level_forest = int(res_forest[0]["msDS-Behavior-Version"][0])
924             level_domain = int(res_domain[0]["msDS-Behavior-Version"][0])
925             level_domain_mixed = int(res_domain[0]["nTMixedDomain"][0])
926
927             min_level_dc = int(res_dc_s[0]["msDS-Behavior-Version"][0]) # Init value
928             for msg in res_dc_s:
929                 if int(msg["msDS-Behavior-Version"][0]) < min_level_dc:
930                     min_level_dc = int(msg["msDS-Behavior-Version"][0])
931
932             if level_forest < 0 or level_domain < 0:
933                 raise CommandError("Domain and/or forest function level(s) is/are invalid. Correct them or reprovision!")
934             if min_level_dc < 0:
935                 raise CommandError("Lowest function level of a DC is invalid. Correct this or reprovision!")
936             if level_forest > level_domain:
937                 raise CommandError("Forest function level is higher than the domain level(s). Correct this or reprovision!")
938             if level_domain > min_level_dc:
939                 raise CommandError("Domain function level is higher than the lowest function level of a DC. Correct this or reprovision!")
940
941         except KeyError:
942             raise CommandError("Could not retrieve the actual domain, forest level and/or lowest DC function level!")
943
944         if subcommand == "show":
945             self.message("Domain and forest function level for domain '%s'" % domain_dn)
946             if level_forest == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0:
947                 self.message("\nATTENTION: You run SAMBA 4 on a forest function level lower than Windows 2000 (Native). This isn't supported! Please raise!")
948             if level_domain == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0:
949                 self.message("\nATTENTION: You run SAMBA 4 on a domain function level lower than Windows 2000 (Native). This isn't supported! Please raise!")
950             if min_level_dc == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0:
951                 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)!")
952
953             self.message("")
954
955             if level_forest == DS_DOMAIN_FUNCTION_2000:
956                 outstr = "2000"
957             elif level_forest == DS_DOMAIN_FUNCTION_2003_MIXED:
958                 outstr = "2003 with mixed domains/interim (NT4 DC support)"
959             elif level_forest == DS_DOMAIN_FUNCTION_2003:
960                 outstr = "2003"
961             elif level_forest == DS_DOMAIN_FUNCTION_2008:
962                 outstr = "2008"
963             elif level_forest == DS_DOMAIN_FUNCTION_2008_R2:
964                 outstr = "2008 R2"
965             else:
966                 outstr = "higher than 2008 R2"
967             self.message("Forest function level: (Windows) " + outstr)
968
969             if level_domain == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0:
970                 outstr = "2000 mixed (NT4 DC support)"
971             elif level_domain == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed == 0:
972                 outstr = "2000"
973             elif level_domain == DS_DOMAIN_FUNCTION_2003_MIXED:
974                 outstr = "2003 with mixed domains/interim (NT4 DC support)"
975             elif level_domain == DS_DOMAIN_FUNCTION_2003:
976                 outstr = "2003"
977             elif level_domain == DS_DOMAIN_FUNCTION_2008:
978                 outstr = "2008"
979             elif level_domain == DS_DOMAIN_FUNCTION_2008_R2:
980                 outstr = "2008 R2"
981             else:
982                 outstr = "higher than 2008 R2"
983             self.message("Domain function level: (Windows) " + outstr)
984
985             if min_level_dc == DS_DOMAIN_FUNCTION_2000:
986                 outstr = "2000"
987             elif min_level_dc == DS_DOMAIN_FUNCTION_2003:
988                 outstr = "2003"
989             elif min_level_dc == DS_DOMAIN_FUNCTION_2008:
990                 outstr = "2008"
991             elif min_level_dc == DS_DOMAIN_FUNCTION_2008_R2:
992                 outstr = "2008 R2"
993             else:
994                 outstr = "higher than 2008 R2"
995             self.message("Lowest function level of a DC: (Windows) " + outstr)
996
997         elif subcommand == "raise":
998             msgs = []
999
1000             if domain_level is not None:
1001                 if domain_level == "2003":
1002                     new_level_domain = DS_DOMAIN_FUNCTION_2003
1003                 elif domain_level == "2008":
1004                     new_level_domain = DS_DOMAIN_FUNCTION_2008
1005                 elif domain_level == "2008_R2":
1006                     new_level_domain = DS_DOMAIN_FUNCTION_2008_R2
1007
1008                 if new_level_domain <= level_domain and level_domain_mixed == 0:
1009                     raise CommandError("Domain function level can't be smaller than or equal to the actual one!")
1010
1011                 if new_level_domain > min_level_dc:
1012                     raise CommandError("Domain function level can't be higher than the lowest function level of a DC!")
1013
1014                 # Deactivate mixed/interim domain support
1015                 if level_domain_mixed != 0:
1016                     # Directly on the base DN
1017                     m = ldb.Message()
1018                     m.dn = ldb.Dn(samdb, domain_dn)
1019                     m["nTMixedDomain"] = ldb.MessageElement("0",
1020                       ldb.FLAG_MOD_REPLACE, "nTMixedDomain")
1021                     samdb.modify(m)
1022                     # Under partitions
1023                     m = ldb.Message()
1024                     m.dn = ldb.Dn(samdb, "CN=" + lp.get("workgroup") + ",CN=Partitions,%s" % samdb.get_config_basedn())
1025                     m["nTMixedDomain"] = ldb.MessageElement("0",
1026                       ldb.FLAG_MOD_REPLACE, "nTMixedDomain")
1027                     try:
1028                         samdb.modify(m)
1029                     except ldb.LdbError, (enum, emsg):
1030                         if enum != ldb.ERR_UNWILLING_TO_PERFORM:
1031                             raise
1032
1033                 # Directly on the base DN
1034                 m = ldb.Message()
1035                 m.dn = ldb.Dn(samdb, domain_dn)
1036                 m["msDS-Behavior-Version"]= ldb.MessageElement(
1037                   str(new_level_domain), ldb.FLAG_MOD_REPLACE,
1038                             "msDS-Behavior-Version")
1039                 samdb.modify(m)
1040                 # Under partitions
1041                 m = ldb.Message()
1042                 m.dn = ldb.Dn(samdb, "CN=" + lp.get("workgroup")
1043                   + ",CN=Partitions,%s" % samdb.get_config_basedn())
1044                 m["msDS-Behavior-Version"]= ldb.MessageElement(
1045                   str(new_level_domain), ldb.FLAG_MOD_REPLACE,
1046                           "msDS-Behavior-Version")
1047                 try:
1048                     samdb.modify(m)
1049                 except ldb.LdbError, (enum, emsg):
1050                     if enum != ldb.ERR_UNWILLING_TO_PERFORM:
1051                         raise
1052
1053                 level_domain = new_level_domain
1054                 msgs.append("Domain function level changed!")
1055
1056             if forest_level is not None:
1057                 if forest_level == "2003":
1058                     new_level_forest = DS_DOMAIN_FUNCTION_2003
1059                 elif forest_level == "2008":
1060                     new_level_forest = DS_DOMAIN_FUNCTION_2008
1061                 elif forest_level == "2008_R2":
1062                     new_level_forest = DS_DOMAIN_FUNCTION_2008_R2
1063                 if new_level_forest <= level_forest:
1064                     raise CommandError("Forest function level can't be smaller than or equal to the actual one!")
1065                 if new_level_forest > level_domain:
1066                     raise CommandError("Forest function level can't be higher than the domain function level(s). Please raise it/them first!")
1067                 m = ldb.Message()
1068                 m.dn = ldb.Dn(samdb, "CN=Partitions,%s" % samdb.get_config_basedn())
1069                 m["msDS-Behavior-Version"]= ldb.MessageElement(
1070                   str(new_level_forest), ldb.FLAG_MOD_REPLACE,
1071                           "msDS-Behavior-Version")
1072                 samdb.modify(m)
1073                 msgs.append("Forest function level changed!")
1074             msgs.append("All changes applied successfully!")
1075             self.message("\n".join(msgs))
1076         else:
1077             raise CommandError("invalid argument: '%s' (choose from 'show', 'raise')" % subcommand)
1078
1079
1080 class cmd_domain_passwordsettings(Command):
1081     """Set password settings.
1082
1083     Password complexity, password lockout policy, history length,
1084     minimum password length, the minimum and maximum password age) on
1085     a Samba AD DC server.
1086
1087     Use against a Windows DC is possible, but group policy will override it.
1088     """
1089
1090     synopsis = "%prog (show|set <options>) [options]"
1091
1092     takes_optiongroups = {
1093         "sambaopts": options.SambaOptions,
1094         "versionopts": options.VersionOptions,
1095         "credopts": options.CredentialsOptions,
1096         }
1097
1098     takes_options = [
1099         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
1100                metavar="URL", dest="H"),
1101         Option("--quiet", help="Be quiet", action="store_true"),
1102         Option("--complexity", type="choice", choices=["on","off","default"],
1103           help="The password complexity (on | off | default). Default is 'on'"),
1104         Option("--store-plaintext", type="choice", choices=["on","off","default"],
1105           help="Store plaintext passwords where account have 'store passwords with reversible encryption' set (on | off | default). Default is 'off'"),
1106         Option("--history-length",
1107           help="The password history length (<integer> | default).  Default is 24.", type=str),
1108         Option("--min-pwd-length",
1109           help="The minimum password length (<integer> | default).  Default is 7.", type=str),
1110         Option("--min-pwd-age",
1111           help="The minimum password age (<integer in days> | default).  Default is 1.", type=str),
1112         Option("--max-pwd-age",
1113           help="The maximum password age (<integer in days> | default).  Default is 43.", type=str),
1114         Option("--account-lockout-duration",
1115           help="The the length of time an account is locked out after exeeding the limit on bad password attempts (<integer in mins> | default).  Default is 30 mins.", type=str),
1116         Option("--account-lockout-threshold",
1117           help="The number of bad password attempts allowed before locking out the account (<integer> | default).  Default is 0 (never lock out).", type=str),
1118         Option("--reset-account-lockout-after",
1119           help="After this time is elapsed, the recorded number of attempts restarts from zero (<integer> | default).  Default is 30.", type=str),
1120           ]
1121
1122     takes_args = ["subcommand"]
1123
1124     def run(self, subcommand, H=None, min_pwd_age=None, max_pwd_age=None,
1125             quiet=False, complexity=None, store_plaintext=None, history_length=None,
1126             min_pwd_length=None, account_lockout_duration=None, account_lockout_threshold=None,
1127             reset_account_lockout_after=None, credopts=None, sambaopts=None,
1128             versionopts=None):
1129         lp = sambaopts.get_loadparm()
1130         creds = credopts.get_credentials(lp)
1131
1132         samdb = SamDB(url=H, session_info=system_session(),
1133             credentials=creds, lp=lp)
1134
1135         domain_dn = samdb.domain_dn()
1136         res = samdb.search(domain_dn, scope=ldb.SCOPE_BASE,
1137           attrs=["pwdProperties", "pwdHistoryLength", "minPwdLength",
1138                  "minPwdAge", "maxPwdAge", "lockoutDuration", "lockoutThreshold",
1139                  "lockOutObservationWindow"])
1140         assert(len(res) == 1)
1141         try:
1142             pwd_props = int(res[0]["pwdProperties"][0])
1143             pwd_hist_len = int(res[0]["pwdHistoryLength"][0])
1144             cur_min_pwd_len = int(res[0]["minPwdLength"][0])
1145             # ticks -> days
1146             cur_min_pwd_age = int(abs(int(res[0]["minPwdAge"][0])) / (1e7 * 60 * 60 * 24))
1147             if int(res[0]["maxPwdAge"][0]) == -0x8000000000000000:
1148                 cur_max_pwd_age = 0
1149             else:
1150                 cur_max_pwd_age = int(abs(int(res[0]["maxPwdAge"][0])) / (1e7 * 60 * 60 * 24))
1151             cur_account_lockout_threshold = int(res[0]["lockoutThreshold"][0])
1152             # ticks -> mins
1153             if int(res[0]["lockoutDuration"][0]) == -0x8000000000000000:
1154                 cur_account_lockout_duration = 0
1155             else:
1156                 cur_account_lockout_duration = abs(int(res[0]["lockoutDuration"][0])) / (1e7 * 60)
1157             cur_reset_account_lockout_after = abs(int(res[0]["lockOutObservationWindow"][0])) / (1e7 * 60)
1158         except Exception, e:
1159             raise CommandError("Could not retrieve password properties!", e)
1160
1161         if subcommand == "show":
1162             self.message("Password informations for domain '%s'" % domain_dn)
1163             self.message("")
1164             if pwd_props & DOMAIN_PASSWORD_COMPLEX != 0:
1165                 self.message("Password complexity: on")
1166             else:
1167                 self.message("Password complexity: off")
1168             if pwd_props & DOMAIN_PASSWORD_STORE_CLEARTEXT != 0:
1169                 self.message("Store plaintext passwords: on")
1170             else:
1171                 self.message("Store plaintext passwords: off")
1172             self.message("Password history length: %d" % pwd_hist_len)
1173             self.message("Minimum password length: %d" % cur_min_pwd_len)
1174             self.message("Minimum password age (days): %d" % cur_min_pwd_age)
1175             self.message("Maximum password age (days): %d" % cur_max_pwd_age)
1176             self.message("Account lockout duration (mins): %d" % cur_account_lockout_duration)
1177             self.message("Account lockout threshold (attempts): %d" % cur_account_lockout_threshold)
1178             self.message("Reset account lockout after (mins): %d" % cur_reset_account_lockout_after)
1179         elif subcommand == "set":
1180             msgs = []
1181             m = ldb.Message()
1182             m.dn = ldb.Dn(samdb, domain_dn)
1183
1184             if complexity is not None:
1185                 if complexity == "on" or complexity == "default":
1186                     pwd_props = pwd_props | DOMAIN_PASSWORD_COMPLEX
1187                     msgs.append("Password complexity activated!")
1188                 elif complexity == "off":
1189                     pwd_props = pwd_props & (~DOMAIN_PASSWORD_COMPLEX)
1190                     msgs.append("Password complexity deactivated!")
1191
1192             if store_plaintext is not None:
1193                 if store_plaintext == "on" or store_plaintext == "default":
1194                     pwd_props = pwd_props | DOMAIN_PASSWORD_STORE_CLEARTEXT
1195                     msgs.append("Plaintext password storage for changed passwords activated!")
1196                 elif store_plaintext == "off":
1197                     pwd_props = pwd_props & (~DOMAIN_PASSWORD_STORE_CLEARTEXT)
1198                     msgs.append("Plaintext password storage for changed passwords deactivated!")
1199
1200             if complexity is not None or store_plaintext is not None:
1201                 m["pwdProperties"] = ldb.MessageElement(str(pwd_props),
1202                   ldb.FLAG_MOD_REPLACE, "pwdProperties")
1203
1204             if history_length is not None:
1205                 if history_length == "default":
1206                     pwd_hist_len = 24
1207                 else:
1208                     pwd_hist_len = int(history_length)
1209
1210                 if pwd_hist_len < 0 or pwd_hist_len > 24:
1211                     raise CommandError("Password history length must be in the range of 0 to 24!")
1212
1213                 m["pwdHistoryLength"] = ldb.MessageElement(str(pwd_hist_len),
1214                   ldb.FLAG_MOD_REPLACE, "pwdHistoryLength")
1215                 msgs.append("Password history length changed!")
1216
1217             if min_pwd_length is not None:
1218                 if min_pwd_length == "default":
1219                     min_pwd_len = 7
1220                 else:
1221                     min_pwd_len = int(min_pwd_length)
1222
1223                 if min_pwd_len < 0 or min_pwd_len > 14:
1224                     raise CommandError("Minimum password length must be in the range of 0 to 14!")
1225
1226                 m["minPwdLength"] = ldb.MessageElement(str(min_pwd_len),
1227                   ldb.FLAG_MOD_REPLACE, "minPwdLength")
1228                 msgs.append("Minimum password length changed!")
1229
1230             if min_pwd_age is not None:
1231                 if min_pwd_age == "default":
1232                     min_pwd_age = 1
1233                 else:
1234                     min_pwd_age = int(min_pwd_age)
1235
1236                 if min_pwd_age < 0 or min_pwd_age > 998:
1237                     raise CommandError("Minimum password age must be in the range of 0 to 998!")
1238
1239                 # days -> ticks
1240                 min_pwd_age_ticks = -int(min_pwd_age * (24 * 60 * 60 * 1e7))
1241
1242                 m["minPwdAge"] = ldb.MessageElement(str(min_pwd_age_ticks),
1243                   ldb.FLAG_MOD_REPLACE, "minPwdAge")
1244                 msgs.append("Minimum password age changed!")
1245
1246             if max_pwd_age is not None:
1247                 if max_pwd_age == "default":
1248                     max_pwd_age = 43
1249                 else:
1250                     max_pwd_age = int(max_pwd_age)
1251
1252                 if max_pwd_age < 0 or max_pwd_age > 999:
1253                     raise CommandError("Maximum password age must be in the range of 0 to 999!")
1254
1255                 # days -> ticks
1256                 if max_pwd_age == 0:
1257                     max_pwd_age_ticks = -0x8000000000000000
1258                 else:
1259                     max_pwd_age_ticks = -int(max_pwd_age * (24 * 60 * 60 * 1e7))
1260
1261                 m["maxPwdAge"] = ldb.MessageElement(str(max_pwd_age_ticks),
1262                   ldb.FLAG_MOD_REPLACE, "maxPwdAge")
1263                 msgs.append("Maximum password age changed!")
1264
1265             if account_lockout_duration is not None:
1266                 if account_lockout_duration == "default":
1267                     account_lockout_duration = 30
1268                 else:
1269                     account_lockout_duration = int(account_lockout_duration)
1270
1271                 if account_lockout_duration < 0 or account_lockout_duration > 99999:
1272                     raise CommandError("Maximum password age must be in the range of 0 to 99999!")
1273
1274                 # days -> ticks
1275                 if account_lockout_duration == 0:
1276                     account_lockout_duration_ticks = -0x8000000000000000
1277                 else:
1278                     account_lockout_duration_ticks = -int(account_lockout_duration * (60 * 1e7))
1279
1280                 m["lockoutDuration"] = ldb.MessageElement(str(account_lockout_duration_ticks),
1281                   ldb.FLAG_MOD_REPLACE, "lockoutDuration")
1282                 msgs.append("Account lockout duration changed!")
1283
1284             if account_lockout_threshold is not None:
1285                 if account_lockout_threshold == "default":
1286                     account_lockout_threshold = 0
1287                 else:
1288                     account_lockout_threshold = int(account_lockout_threshold)
1289
1290                 m["lockoutThreshold"] = ldb.MessageElement(str(account_lockout_threshold),
1291                   ldb.FLAG_MOD_REPLACE, "lockoutThreshold")
1292                 msgs.append("Account lockout threshold changed!")
1293
1294             if reset_account_lockout_after is not None:
1295                 if reset_account_lockout_after == "default":
1296                     reset_account_lockout_after = 30
1297                 else:
1298                     reset_account_lockout_after = int(reset_account_lockout_after)
1299
1300                 if reset_account_lockout_after < 0 or reset_account_lockout_after > 99999:
1301                     raise CommandError("Maximum password age must be in the range of 0 to 99999!")
1302
1303                 # days -> ticks
1304                 if reset_account_lockout_after == 0:
1305                     reset_account_lockout_after_ticks = -0x8000000000000000
1306                 else:
1307                     reset_account_lockout_after_ticks = -int(reset_account_lockout_after * (60 * 1e7))
1308
1309                 m["lockOutObservationWindow"] = ldb.MessageElement(str(reset_account_lockout_after_ticks),
1310                   ldb.FLAG_MOD_REPLACE, "lockOutObservationWindow")
1311                 msgs.append("Duration to reset account lockout after changed!")
1312
1313             if max_pwd_age > 0 and min_pwd_age >= max_pwd_age:
1314                 raise CommandError("Maximum password age (%d) must be greater than minimum password age (%d)!" % (max_pwd_age, min_pwd_age))
1315
1316             if len(m) == 0:
1317                 raise CommandError("You must specify at least one option to set. Try --help")
1318             samdb.modify(m)
1319             msgs.append("All changes applied successfully!")
1320             self.message("\n".join(msgs))
1321         else:
1322             raise CommandError("Wrong argument '%s'!" % subcommand)
1323
1324
1325 class cmd_domain_classicupgrade(Command):
1326     """Upgrade from Samba classic (NT4-like) database to Samba AD DC database.
1327
1328     Specify either a directory with all Samba classic DC databases and state files (with --dbdir) or
1329     the testparm utility from your classic installation (with --testparm).
1330     """
1331
1332     synopsis = "%prog [options] <classic_smb_conf>"
1333
1334     takes_optiongroups = {
1335         "sambaopts": options.SambaOptions,
1336         "versionopts": options.VersionOptions
1337     }
1338
1339     takes_options = [
1340         Option("--dbdir", type="string", metavar="DIR",
1341                   help="Path to samba classic DC database directory"),
1342         Option("--testparm", type="string", metavar="PATH",
1343                   help="Path to samba classic DC testparm utility from the previous installation.  This allows the default paths of the previous installation to be followed"),
1344         Option("--targetdir", type="string", metavar="DIR",
1345                   help="Path prefix where the new Samba 4.0 AD domain should be initialised"),
1346         Option("--quiet", help="Be quiet", action="store_true"),
1347         Option("--verbose", help="Be verbose", action="store_true"),
1348         Option("--use-xattrs", type="choice", choices=["yes","no","auto"], metavar="[yes|no|auto]",
1349                    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"),
1350         Option("--use-ntvfs", help="Use NTVFS for the fileserver (default = no)",
1351                action="store_true"),
1352         Option("--dns-backend", type="choice", metavar="NAMESERVER-BACKEND",
1353                choices=["SAMBA_INTERNAL", "BIND9_FLATFILE", "BIND9_DLZ", "NONE"],
1354                help="The DNS server backend. SAMBA_INTERNAL is the builtin name server (default), "
1355                    "BIND9_FLATFILE uses bind9 text database to store zone information, "
1356                    "BIND9_DLZ uses samba4 AD to store zone information, "
1357                    "NONE skips the DNS setup entirely (this DC will not be a DNS server)",
1358                default="SAMBA_INTERNAL")
1359     ]
1360
1361     takes_args = ["smbconf"]
1362
1363     def run(self, smbconf=None, targetdir=None, dbdir=None, testparm=None,
1364             quiet=False, verbose=False, use_xattrs=None, sambaopts=None, versionopts=None,
1365             dns_backend=None, use_ntvfs=False):
1366
1367         if not os.path.exists(smbconf):
1368             raise CommandError("File %s does not exist" % smbconf)
1369
1370         if testparm and not os.path.exists(testparm):
1371             raise CommandError("Testparm utility %s does not exist" % testparm)
1372
1373         if dbdir and not os.path.exists(dbdir):
1374             raise CommandError("Directory %s does not exist" % dbdir)
1375
1376         if not dbdir and not testparm:
1377             raise CommandError("Please specify either dbdir or testparm")
1378
1379         logger = self.get_logger()
1380         if verbose:
1381             logger.setLevel(logging.DEBUG)
1382         elif quiet:
1383             logger.setLevel(logging.WARNING)
1384         else:
1385             logger.setLevel(logging.INFO)
1386
1387         if dbdir and testparm:
1388             logger.warning("both dbdir and testparm specified, ignoring dbdir.")
1389             dbdir = None
1390
1391         lp = sambaopts.get_loadparm()
1392
1393         s3conf = s3param.get_context()
1394
1395         if sambaopts.realm:
1396             s3conf.set("realm", sambaopts.realm)
1397
1398         if targetdir is not None:
1399             if not os.path.isdir(targetdir):
1400                 os.mkdir(targetdir)
1401
1402         eadb = True
1403         if use_xattrs == "yes":
1404             eadb = False
1405         elif use_xattrs == "auto" and not s3conf.get("posix:eadb"):
1406             if targetdir:
1407                 tmpfile = tempfile.NamedTemporaryFile(dir=os.path.abspath(targetdir))
1408             else:
1409                 tmpfile = tempfile.NamedTemporaryFile(dir=os.path.abspath(os.path.dirname(lp.get("private dir"))))
1410             try:
1411                 try:
1412                     samba.ntacls.setntacl(lp, tmpfile.name,
1413                                 "O:S-1-5-32G:S-1-5-32", "S-1-5-32", "native")
1414                     eadb = False
1415                 except Exception:
1416                     # FIXME: Don't catch all exceptions here
1417                     logger.info("You are not root or your system do not support xattr, using tdb backend for attributes. "
1418                                 "If you intend to use this provision in production, rerun the script as root on a system supporting xattrs.")
1419             finally:
1420                 tmpfile.close()
1421
1422         # Set correct default values from dbdir or testparm
1423         paths = {}
1424         if dbdir:
1425             paths["state directory"] = dbdir
1426             paths["private dir"] = dbdir
1427             paths["lock directory"] = dbdir
1428             paths["smb passwd file"] = dbdir + "/smbpasswd"
1429         else:
1430             paths["state directory"] = get_testparm_var(testparm, smbconf, "state directory")
1431             paths["private dir"] = get_testparm_var(testparm, smbconf, "private dir")
1432             paths["smb passwd file"] = get_testparm_var(testparm, smbconf, "smb passwd file")
1433             paths["lock directory"] = get_testparm_var(testparm, smbconf, "lock directory")
1434             # "testparm" from Samba 3 < 3.4.x is not aware of the parameter
1435             # "state directory", instead make use of "lock directory"
1436             if len(paths["state directory"]) == 0:
1437                 paths["state directory"] = paths["lock directory"]
1438
1439         for p in paths:
1440             s3conf.set(p, paths[p])
1441
1442         # load smb.conf parameters
1443         logger.info("Reading smb.conf")
1444         s3conf.load(smbconf)
1445         samba3 = Samba3(smbconf, s3conf)
1446
1447         logger.info("Provisioning")
1448         upgrade_from_samba3(samba3, logger, targetdir, session_info=system_session(),
1449                             useeadb=eadb, dns_backend=dns_backend, use_ntvfs=use_ntvfs)
1450
1451
1452 class cmd_domain_samba3upgrade(cmd_domain_classicupgrade):
1453     __doc__ = cmd_domain_classicupgrade.__doc__
1454
1455     # This command is present for backwards compatibility only,
1456     # and should not be shown.
1457
1458     hidden = True
1459
1460
1461 class cmd_domain(SuperCommand):
1462     """Domain management."""
1463
1464     subcommands = {}
1465     subcommands["demote"] = cmd_domain_demote()
1466     if cmd_domain_export_keytab is not None:
1467         subcommands["exportkeytab"] = cmd_domain_export_keytab()
1468     subcommands["info"] = cmd_domain_info()
1469     subcommands["provision"] = cmd_domain_provision()
1470     subcommands["join"] = cmd_domain_join()
1471     subcommands["dcpromo"] = cmd_domain_dcpromo()
1472     subcommands["level"] = cmd_domain_level()
1473     subcommands["passwordsettings"] = cmd_domain_passwordsettings()
1474     subcommands["classicupgrade"] = cmd_domain_classicupgrade()
1475     subcommands["samba3upgrade"] = cmd_domain_samba3upgrade()