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