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