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