netcmd: Split 'domain passwordsettings' into a super-command
[samba.git] / python / samba / netcmd / domain.py
index b3965747655fe0403429b8f34fb5bcb2b39098cc..cb2b1ccecb363ea8d9839848efdc75d75b1cb26d 100644 (file)
@@ -22,6 +22,8 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 
+from __future__ import print_function
+from __future__ import division
 import samba.getopt as options
 import ldb
 import string
@@ -33,6 +35,7 @@ import tempfile
 import logging
 import subprocess
 import time
+import shutil
 from samba import ntstatus
 from samba import NTSTATUSError
 from samba import werror
@@ -41,7 +44,7 @@ from samba.net import Net, LIBNET_JOIN_AUTOMATIC
 import samba.ntacls
 from samba.join import join_RODC, join_DC, join_subdomain
 from samba.auth import system_session
-from samba.samdb import SamDB
+from samba.samdb import SamDB, get_default_backend_store
 from samba.ndr import ndr_unpack, ndr_pack, ndr_print
 from samba.dcerpc import drsuapi
 from samba.dcerpc import drsblobs
@@ -57,6 +60,7 @@ from samba.netcmd import (
     SuperCommand,
     Option
     )
+from samba.netcmd.fsmo import get_fsmo_roleowner
 from samba.netcmd.common import netcmd_get_domain_infos_via_cldap
 from samba.samba3 import Samba3
 from samba.samba3 import param as s3param
@@ -95,6 +99,12 @@ from samba.provision.common import (
     FILL_DRS
 )
 
+string_version_to_constant = {
+    "2008_R2" : DS_DOMAIN_FUNCTION_2008_R2,
+    "2012": DS_DOMAIN_FUNCTION_2012,
+    "2012_R2": DS_DOMAIN_FUNCTION_2012_R2,
+}
+
 def get_testparm_var(testparm, smbconf, varname):
     errfile = open(os.devnull, 'w')
     p = subprocess.Popen([testparm, '-s', '-l',
@@ -234,7 +244,7 @@ class cmd_domain_provision(Command):
                 help="The domain and forest function level (2000 | 2003 | 2008 | 2008_R2 - always native). Default is (Windows) 2008_R2 Native.",
                 default="2008_R2"),
          Option("--base-schema", type="choice", metavar="BASE-SCHEMA",
-                choices=["2008_R2", "2012", "2012_R2"],
+                choices=["2008_R2", "2008_R2_old", "2012", "2012_R2"],
                 help="The base schema files to use. Default is (Windows) 2008_R2.",
                 default="2008_R2"),
          Option("--next-rid", type="int", metavar="NEXTRID", default=1000,
@@ -246,6 +256,13 @@ class cmd_domain_provision(Command):
          Option("--ol-mmr-urls", type="string", metavar="LDAPSERVER",
                 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\""),
          Option("--use-rfc2307", action="store_true", help="Use AD to store posix attributes (default = no)"),
+         Option("--plaintext-secrets", action="store_true",
+                help="Store secret/sensitive values as plain text on disk" +
+                     "(default is to encrypt secret/ensitive values)"),
+         Option("--backend-store", type="choice", metavar="BACKENDSTORE",
+                choices=["tdb", "mdb"],
+                help="Specify the database backend to be used "
+                     "(default is %s)" % get_default_backend_store()),
         ]
 
     openldap_options = [
@@ -315,7 +332,9 @@ class cmd_domain_provision(Command):
             ldap_backend_extra_port=None,
             ldap_backend_forced_uri=None,
             ldap_dryrun_mode=None,
-            base_schema=None):
+            base_schema=None,
+            plaintext_secrets=False,
+            backend_store=None):
 
         self.logger = self.get_logger("provision")
         if quiet:
@@ -342,9 +361,9 @@ class cmd_domain_provision(Command):
 
             def ask(prompt, default=None):
                 if default is not None:
-                    print "%s [%s]: " % (prompt, default),
+                    print("%s [%s]: " % (prompt, default), end=' ')
                 else:
-                    print "%s: " % (prompt,),
+                    print("%s: " % (prompt,), end=' ')
                 return sys.stdin.readline().rstrip("\n") or default
 
             try:
@@ -463,6 +482,8 @@ class cmd_domain_provision(Command):
             domain_sid = security.dom_sid(domain_sid)
 
         session = system_session()
+        if backend_store is None:
+            backend_store = get_default_backend_store()
         try:
             result = provision(self.logger,
                   session, smbconf=smbconf, targetdir=targetdir,
@@ -484,9 +505,11 @@ class cmd_domain_provision(Command):
                   ldap_backend_extra_port=ldap_backend_extra_port,
                   ldap_backend_forced_uri=ldap_backend_forced_uri,
                   nosync=ldap_backend_nosync, ldap_dryrun_mode=ldap_dryrun_mode,
-                  base_schema=base_schema)
+                  base_schema=base_schema,
+                  plaintext_secrets=plaintext_secrets,
+                  backend_store=backend_store)
 
-        except ProvisioningError, e:
+        except ProvisioningError as e:
             raise CommandError("Provision failed", e)
 
         result.report_logger(self.logger)
@@ -637,6 +660,9 @@ class cmd_domain_join(Command):
                    "BIND9_DLZ uses samba4 AD to store zone information, "
                    "NONE skips the DNS setup entirely (this DC will not be a DNS server)",
                default="SAMBA_INTERNAL"),
+        Option("--plaintext-secrets", action="store_true",
+               help="Store secret/sensitive values as plain text on disk" +
+                    "(default is to encrypt secret/ensitive values)"),
         Option("--quiet", help="Be quiet", action="store_true"),
         Option("--verbose", help="Be verbose", action="store_true")
        ]
@@ -654,7 +680,7 @@ class cmd_domain_join(Command):
             versionopts=None, server=None, site=None, targetdir=None,
             domain_critical_only=False, parent_domain=None, machinepass=None,
             use_ntvfs=False, dns_backend=None, adminpass=None,
-            quiet=False, verbose=False):
+            quiet=False, verbose=False, plaintext_secrets=False):
         lp = sambaopts.get_loadparm()
         creds = credopts.get_credentials(lp)
         net = Net(creds, lp, server=credopts.ipaddress)
@@ -685,13 +711,16 @@ class cmd_domain_join(Command):
             join_DC(logger=logger, server=server, creds=creds, lp=lp, domain=domain,
                     site=site, netbios_name=netbios_name, targetdir=targetdir,
                     domain_critical_only=domain_critical_only,
-                    machinepass=machinepass, use_ntvfs=use_ntvfs, dns_backend=dns_backend)
+                    machinepass=machinepass, use_ntvfs=use_ntvfs,
+                    dns_backend=dns_backend,
+                    plaintext_secrets=plaintext_secrets)
         elif role == "RODC":
             join_RODC(logger=logger, server=server, creds=creds, lp=lp, domain=domain,
                       site=site, netbios_name=netbios_name, targetdir=targetdir,
                       domain_critical_only=domain_critical_only,
                       machinepass=machinepass, use_ntvfs=use_ntvfs,
-                      dns_backend=dns_backend)
+                      dns_backend=dns_backend,
+                      plaintext_secrets=plaintext_secrets)
         elif role == "SUBDOMAIN":
             if not adminpass:
                 logger.info("Administrator password will be set randomly!")
@@ -704,7 +733,8 @@ class cmd_domain_join(Command):
                            netbios_name=netbios_name, netbios_domain=netbios_domain,
                            targetdir=targetdir, machinepass=machinepass,
                            use_ntvfs=use_ntvfs, dns_backend=dns_backend,
-                           adminpass=adminpass)
+                           adminpass=adminpass,
+                           plaintext_secrets=plaintext_secrets)
         else:
             raise CommandError("Invalid role '%s' (possible values: MEMBER, DC, RODC, SUBDOMAIN)" % role)
 
@@ -821,7 +851,8 @@ class cmd_domain_demote(Command):
 
                 try:
                     drsuapiBind.DsReplicaSync(drsuapi_handle, 1, req1)
-                except RuntimeError as (werr, string):
+                except RuntimeError as e1:
+                    (werr, string) = e1.args
                     if werr == werror.WERR_DS_DRA_NO_REPLICA:
                         pass
                     else:
@@ -845,7 +876,7 @@ class cmd_domain_demote(Command):
             dc_dn = res[0].dn
             uac = int(str(res[0]["userAccountControl"]))
 
-        except Exception, e:
+        except Exception as e:
             if not (dsa_options & DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL) and not samdb.am_rodc():
                 self.errf.write(
                     "Error while demoting, re-enabling inbound replication\n")
@@ -877,7 +908,7 @@ class cmd_domain_demote(Command):
                                                         "userAccountControl")
         try:
             remote_samdb.modify(msg)
-        except Exception, e:
+        except Exception as e:
             if not (dsa_options & DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL) and not samdb.am_rodc():
                 self.errf.write(
                     "Error while demoting, re-enabling inbound replication")
@@ -932,7 +963,7 @@ class cmd_domain_demote(Command):
         try:
             newdn = ldb.Dn(remote_samdb, "%s,%s" % (newrdn, str(computer_dn)))
             remote_samdb.rename(dc_dn, newdn)
-        except Exception, e:
+        except Exception as e:
             if not (dsa_options & DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL) and not samdb.am_rodc():
                 self.errf.write(
                     "Error while demoting, re-enabling inbound replication\n")
@@ -961,7 +992,8 @@ class cmd_domain_demote(Command):
             req1.commit = 1
 
             drsuapiBind.DsRemoveDSServer(drsuapi_handle, 1, req1)
-        except RuntimeError as (werr, string):
+        except RuntimeError as e3:
+            (werr, string) = e3.args
             if not (dsa_options & DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL) and not samdb.am_rodc():
                 self.errf.write(
                     "Error while demoting, re-enabling inbound replication\n")
@@ -992,7 +1024,7 @@ class cmd_domain_demote(Command):
             try:
                 remote_samdb.delete(ldb.Dn(remote_samdb,
                                     "%s,%s" % (s, str(newdn))))
-            except ldb.LdbError, l:
+            except ldb.LdbError as l:
                 pass
 
         self.errf.write("Demote successful\n")
@@ -1173,7 +1205,8 @@ class cmd_domain_level(Command):
                       ldb.FLAG_MOD_REPLACE, "nTMixedDomain")
                     try:
                         samdb.modify(m)
-                    except ldb.LdbError, (enum, emsg):
+                    except ldb.LdbError as e:
+                        (enum, emsg) = e.args
                         if enum != ldb.ERR_UNWILLING_TO_PERFORM:
                             raise
 
@@ -1193,7 +1226,8 @@ class cmd_domain_level(Command):
                           "msDS-Behavior-Version")
                 try:
                     samdb.modify(m)
-                except ldb.LdbError, (enum, emsg):
+                except ldb.LdbError as e2:
+                    (enum, emsg) = e2.args
                     if enum != ldb.ERR_UNWILLING_TO_PERFORM:
                         raise
 
@@ -1229,8 +1263,74 @@ class cmd_domain_level(Command):
         else:
             raise CommandError("invalid argument: '%s' (choose from 'show', 'raise')" % subcommand)
 
+class cmd_domain_passwordsettings_show(Command):
+    """Display current password settings for the domain."""
+
+    synopsis = "%prog [options]"
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "versionopts": options.VersionOptions,
+        "credopts": options.CredentialsOptions,
+        }
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server", type=str,
+               metavar="URL", dest="H"),
+          ]
+
+    def run(self, H=None, credopts=None, sambaopts=None, versionopts=None):
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp)
+
+        samdb = SamDB(url=H, session_info=system_session(),
+            credentials=creds, lp=lp)
+
+        domain_dn = samdb.domain_dn()
+        res = samdb.search(domain_dn, scope=ldb.SCOPE_BASE,
+          attrs=["pwdProperties", "pwdHistoryLength", "minPwdLength",
+                 "minPwdAge", "maxPwdAge", "lockoutDuration", "lockoutThreshold",
+                 "lockOutObservationWindow"])
+        assert(len(res) == 1)
+        try:
+            pwd_props = int(res[0]["pwdProperties"][0])
+            pwd_hist_len = int(res[0]["pwdHistoryLength"][0])
+            cur_min_pwd_len = int(res[0]["minPwdLength"][0])
+            # ticks -> days
+            cur_min_pwd_age = int(abs(int(res[0]["minPwdAge"][0])) / (1e7 * 60 * 60 * 24))
+            if int(res[0]["maxPwdAge"][0]) == -0x8000000000000000:
+                cur_max_pwd_age = 0
+            else:
+                cur_max_pwd_age = int(abs(int(res[0]["maxPwdAge"][0])) / (1e7 * 60 * 60 * 24))
+            cur_account_lockout_threshold = int(res[0]["lockoutThreshold"][0])
+            # ticks -> mins
+            if int(res[0]["lockoutDuration"][0]) == -0x8000000000000000:
+                cur_account_lockout_duration = 0
+            else:
+                cur_account_lockout_duration = abs(int(res[0]["lockoutDuration"][0])) / (1e7 * 60)
+            cur_reset_account_lockout_after = abs(int(res[0]["lockOutObservationWindow"][0])) / (1e7 * 60)
+        except Exception as e:
+            raise CommandError("Could not retrieve password properties!", e)
 
-class cmd_domain_passwordsettings(Command):
+        self.message("Password informations for domain '%s'" % domain_dn)
+        self.message("")
+        if pwd_props & DOMAIN_PASSWORD_COMPLEX != 0:
+            self.message("Password complexity: on")
+        else:
+            self.message("Password complexity: off")
+        if pwd_props & DOMAIN_PASSWORD_STORE_CLEARTEXT != 0:
+            self.message("Store plaintext passwords: on")
+        else:
+            self.message("Store plaintext passwords: off")
+        self.message("Password history length: %d" % pwd_hist_len)
+        self.message("Minimum password length: %d" % cur_min_pwd_len)
+        self.message("Minimum password age (days): %d" % cur_min_pwd_age)
+        self.message("Maximum password age (days): %d" % cur_max_pwd_age)
+        self.message("Account lockout duration (mins): %d" % cur_account_lockout_duration)
+        self.message("Account lockout threshold (attempts): %d" % cur_account_lockout_threshold)
+        self.message("Reset account lockout after (mins): %d" % cur_reset_account_lockout_after)
+
+class cmd_domain_passwordsettings_set(Command):
     """Set password settings.
 
     Password complexity, password lockout policy, history length,
@@ -1240,7 +1340,7 @@ class cmd_domain_passwordsettings(Command):
     Use against a Windows DC is possible, but group policy will override it.
     """
 
-    synopsis = "%prog (show|set <options>) [options]"
+    synopsis = "%prog <options> [options]"
 
     takes_optiongroups = {
         "sambaopts": options.SambaOptions,
@@ -1272,9 +1372,7 @@ class cmd_domain_passwordsettings(Command):
           help="After this time is elapsed, the recorded number of attempts restarts from zero (<integer> | default).  Default is 30.", type=str),
           ]
 
-    takes_args = ["subcommand"]
-
-    def run(self, subcommand, H=None, min_pwd_age=None, max_pwd_age=None,
+    def run(self, H=None, min_pwd_age=None, max_pwd_age=None,
             quiet=False, complexity=None, store_plaintext=None, history_length=None,
             min_pwd_length=None, account_lockout_duration=None, account_lockout_threshold=None,
             reset_account_lockout_after=None, credopts=None, sambaopts=None,
@@ -1286,194 +1384,155 @@ class cmd_domain_passwordsettings(Command):
             credentials=creds, lp=lp)
 
         domain_dn = samdb.domain_dn()
-        res = samdb.search(domain_dn, scope=ldb.SCOPE_BASE,
-          attrs=["pwdProperties", "pwdHistoryLength", "minPwdLength",
-                 "minPwdAge", "maxPwdAge", "lockoutDuration", "lockoutThreshold",
-                 "lockOutObservationWindow"])
-        assert(len(res) == 1)
-        try:
-            pwd_props = int(res[0]["pwdProperties"][0])
-            pwd_hist_len = int(res[0]["pwdHistoryLength"][0])
-            cur_min_pwd_len = int(res[0]["minPwdLength"][0])
-            # ticks -> days
-            cur_min_pwd_age = int(abs(int(res[0]["minPwdAge"][0])) / (1e7 * 60 * 60 * 24))
-            if int(res[0]["maxPwdAge"][0]) == -0x8000000000000000:
-                cur_max_pwd_age = 0
+        msgs = []
+        m = ldb.Message()
+        m.dn = ldb.Dn(samdb, domain_dn)
+        pwd_props = int(samdb.get_pwdProperties())
+
+        if complexity is not None:
+            if complexity == "on" or complexity == "default":
+                pwd_props = pwd_props | DOMAIN_PASSWORD_COMPLEX
+                msgs.append("Password complexity activated!")
+            elif complexity == "off":
+                pwd_props = pwd_props & (~DOMAIN_PASSWORD_COMPLEX)
+                msgs.append("Password complexity deactivated!")
+
+        if store_plaintext is not None:
+            if store_plaintext == "on" or store_plaintext == "default":
+                pwd_props = pwd_props | DOMAIN_PASSWORD_STORE_CLEARTEXT
+                msgs.append("Plaintext password storage for changed passwords activated!")
+            elif store_plaintext == "off":
+                pwd_props = pwd_props & (~DOMAIN_PASSWORD_STORE_CLEARTEXT)
+                msgs.append("Plaintext password storage for changed passwords deactivated!")
+
+        if complexity is not None or store_plaintext is not None:
+            m["pwdProperties"] = ldb.MessageElement(str(pwd_props),
+              ldb.FLAG_MOD_REPLACE, "pwdProperties")
+
+        if history_length is not None:
+            if history_length == "default":
+                pwd_hist_len = 24
             else:
-                cur_max_pwd_age = int(abs(int(res[0]["maxPwdAge"][0])) / (1e7 * 60 * 60 * 24))
-            cur_account_lockout_threshold = int(res[0]["lockoutThreshold"][0])
-            # ticks -> mins
-            if int(res[0]["lockoutDuration"][0]) == -0x8000000000000000:
-                cur_account_lockout_duration = 0
-            else:
-                cur_account_lockout_duration = abs(int(res[0]["lockoutDuration"][0])) / (1e7 * 60)
-            cur_reset_account_lockout_after = abs(int(res[0]["lockOutObservationWindow"][0])) / (1e7 * 60)
-        except Exception, e:
-            raise CommandError("Could not retrieve password properties!", e)
+                pwd_hist_len = int(history_length)
 
-        if subcommand == "show":
-            self.message("Password informations for domain '%s'" % domain_dn)
-            self.message("")
-            if pwd_props & DOMAIN_PASSWORD_COMPLEX != 0:
-                self.message("Password complexity: on")
-            else:
-                self.message("Password complexity: off")
-            if pwd_props & DOMAIN_PASSWORD_STORE_CLEARTEXT != 0:
-                self.message("Store plaintext passwords: on")
-            else:
-                self.message("Store plaintext passwords: off")
-            self.message("Password history length: %d" % pwd_hist_len)
-            self.message("Minimum password length: %d" % cur_min_pwd_len)
-            self.message("Minimum password age (days): %d" % cur_min_pwd_age)
-            self.message("Maximum password age (days): %d" % cur_max_pwd_age)
-            self.message("Account lockout duration (mins): %d" % cur_account_lockout_duration)
-            self.message("Account lockout threshold (attempts): %d" % cur_account_lockout_threshold)
-            self.message("Reset account lockout after (mins): %d" % cur_reset_account_lockout_after)
-        elif subcommand == "set":
-            msgs = []
-            m = ldb.Message()
-            m.dn = ldb.Dn(samdb, domain_dn)
-
-            if complexity is not None:
-                if complexity == "on" or complexity == "default":
-                    pwd_props = pwd_props | DOMAIN_PASSWORD_COMPLEX
-                    msgs.append("Password complexity activated!")
-                elif complexity == "off":
-                    pwd_props = pwd_props & (~DOMAIN_PASSWORD_COMPLEX)
-                    msgs.append("Password complexity deactivated!")
-
-            if store_plaintext is not None:
-                if store_plaintext == "on" or store_plaintext == "default":
-                    pwd_props = pwd_props | DOMAIN_PASSWORD_STORE_CLEARTEXT
-                    msgs.append("Plaintext password storage for changed passwords activated!")
-                elif store_plaintext == "off":
-                    pwd_props = pwd_props & (~DOMAIN_PASSWORD_STORE_CLEARTEXT)
-                    msgs.append("Plaintext password storage for changed passwords deactivated!")
-
-            if complexity is not None or store_plaintext is not None:
-                m["pwdProperties"] = ldb.MessageElement(str(pwd_props),
-                  ldb.FLAG_MOD_REPLACE, "pwdProperties")
-
-            if history_length is not None:
-                if history_length == "default":
-                    pwd_hist_len = 24
-                else:
-                    pwd_hist_len = int(history_length)
+            if pwd_hist_len < 0 or pwd_hist_len > 24:
+                raise CommandError("Password history length must be in the range of 0 to 24!")
 
-                if pwd_hist_len < 0 or pwd_hist_len > 24:
-                    raise CommandError("Password history length must be in the range of 0 to 24!")
+            m["pwdHistoryLength"] = ldb.MessageElement(str(pwd_hist_len),
+              ldb.FLAG_MOD_REPLACE, "pwdHistoryLength")
+            msgs.append("Password history length changed!")
 
-                m["pwdHistoryLength"] = ldb.MessageElement(str(pwd_hist_len),
-                  ldb.FLAG_MOD_REPLACE, "pwdHistoryLength")
-                msgs.append("Password history length changed!")
+        if min_pwd_length is not None:
+            if min_pwd_length == "default":
+                min_pwd_len = 7
+            else:
+                min_pwd_len = int(min_pwd_length)
 
-            if min_pwd_length is not None:
-                if min_pwd_length == "default":
-                    min_pwd_len = 7
-                else:
-                    min_pwd_len = int(min_pwd_length)
+            if min_pwd_len < 0 or min_pwd_len > 14:
+                raise CommandError("Minimum password length must be in the range of 0 to 14!")
 
-                if min_pwd_len < 0 or min_pwd_len > 14:
-                    raise CommandError("Minimum password length must be in the range of 0 to 14!")
+            m["minPwdLength"] = ldb.MessageElement(str(min_pwd_len),
+              ldb.FLAG_MOD_REPLACE, "minPwdLength")
+            msgs.append("Minimum password length changed!")
 
-                m["minPwdLength"] = ldb.MessageElement(str(min_pwd_len),
-                  ldb.FLAG_MOD_REPLACE, "minPwdLength")
-                msgs.append("Minimum password length changed!")
+        if min_pwd_age is not None:
+            if min_pwd_age == "default":
+                min_pwd_age = 1
+            else:
+                min_pwd_age = int(min_pwd_age)
 
-            if min_pwd_age is not None:
-                if min_pwd_age == "default":
-                    min_pwd_age = 1
-                else:
-                    min_pwd_age = int(min_pwd_age)
+            if min_pwd_age < 0 or min_pwd_age > 998:
+                raise CommandError("Minimum password age must be in the range of 0 to 998!")
 
-                if min_pwd_age < 0 or min_pwd_age > 998:
-                    raise CommandError("Minimum password age must be in the range of 0 to 998!")
+            # days -> ticks
+            min_pwd_age_ticks = -int(min_pwd_age * (24 * 60 * 60 * 1e7))
 
-                # days -> ticks
-                min_pwd_age_ticks = -int(min_pwd_age * (24 * 60 * 60 * 1e7))
+            m["minPwdAge"] = ldb.MessageElement(str(min_pwd_age_ticks),
+              ldb.FLAG_MOD_REPLACE, "minPwdAge")
+            msgs.append("Minimum password age changed!")
 
-                m["minPwdAge"] = ldb.MessageElement(str(min_pwd_age_ticks),
-                  ldb.FLAG_MOD_REPLACE, "minPwdAge")
-                msgs.append("Minimum password age changed!")
+        if max_pwd_age is not None:
+            if max_pwd_age == "default":
+                max_pwd_age = 43
+            else:
+                max_pwd_age = int(max_pwd_age)
 
-            if max_pwd_age is not None:
-                if max_pwd_age == "default":
-                    max_pwd_age = 43
-                else:
-                    max_pwd_age = int(max_pwd_age)
+            if max_pwd_age < 0 or max_pwd_age > 999:
+                raise CommandError("Maximum password age must be in the range of 0 to 999!")
 
-                if max_pwd_age < 0 or max_pwd_age > 999:
-                    raise CommandError("Maximum password age must be in the range of 0 to 999!")
+            # days -> ticks
+            if max_pwd_age == 0:
+                max_pwd_age_ticks = -0x8000000000000000
+            else:
+                max_pwd_age_ticks = -int(max_pwd_age * (24 * 60 * 60 * 1e7))
 
-                # days -> ticks
-                if max_pwd_age == 0:
-                    max_pwd_age_ticks = -0x8000000000000000
-                else:
-                    max_pwd_age_ticks = -int(max_pwd_age * (24 * 60 * 60 * 1e7))
+            m["maxPwdAge"] = ldb.MessageElement(str(max_pwd_age_ticks),
+              ldb.FLAG_MOD_REPLACE, "maxPwdAge")
+            msgs.append("Maximum password age changed!")
 
-                m["maxPwdAge"] = ldb.MessageElement(str(max_pwd_age_ticks),
-                  ldb.FLAG_MOD_REPLACE, "maxPwdAge")
-                msgs.append("Maximum password age changed!")
+        if account_lockout_duration is not None:
+            if account_lockout_duration == "default":
+                account_lockout_duration = 30
+            else:
+                account_lockout_duration = int(account_lockout_duration)
 
-            if account_lockout_duration is not None:
-                if account_lockout_duration == "default":
-                    account_lockout_duration = 30
-                else:
-                    account_lockout_duration = int(account_lockout_duration)
+            if account_lockout_duration < 0 or account_lockout_duration > 99999:
+                raise CommandError("Maximum password age must be in the range of 0 to 99999!")
 
-                if account_lockout_duration < 0 or account_lockout_duration > 99999:
-                    raise CommandError("Maximum password age must be in the range of 0 to 99999!")
+            # minutes -> ticks
+            if account_lockout_duration == 0:
+                account_lockout_duration_ticks = -0x8000000000000000
+            else:
+                account_lockout_duration_ticks = -int(account_lockout_duration * (60 * 1e7))
 
-                # days -> ticks
-                if account_lockout_duration == 0:
-                    account_lockout_duration_ticks = -0x8000000000000000
-                else:
-                    account_lockout_duration_ticks = -int(account_lockout_duration * (60 * 1e7))
+            m["lockoutDuration"] = ldb.MessageElement(str(account_lockout_duration_ticks),
+              ldb.FLAG_MOD_REPLACE, "lockoutDuration")
+            msgs.append("Account lockout duration changed!")
 
-                m["lockoutDuration"] = ldb.MessageElement(str(account_lockout_duration_ticks),
-                  ldb.FLAG_MOD_REPLACE, "lockoutDuration")
-                msgs.append("Account lockout duration changed!")
+        if account_lockout_threshold is not None:
+            if account_lockout_threshold == "default":
+                account_lockout_threshold = 0
+            else:
+                account_lockout_threshold = int(account_lockout_threshold)
 
-            if account_lockout_threshold is not None:
-                if account_lockout_threshold == "default":
-                    account_lockout_threshold = 0
-                else:
-                    account_lockout_threshold = int(account_lockout_threshold)
+            m["lockoutThreshold"] = ldb.MessageElement(str(account_lockout_threshold),
+              ldb.FLAG_MOD_REPLACE, "lockoutThreshold")
+            msgs.append("Account lockout threshold changed!")
 
-                m["lockoutThreshold"] = ldb.MessageElement(str(account_lockout_threshold),
-                  ldb.FLAG_MOD_REPLACE, "lockoutThreshold")
-                msgs.append("Account lockout threshold changed!")
+        if reset_account_lockout_after is not None:
+            if reset_account_lockout_after == "default":
+                reset_account_lockout_after = 30
+            else:
+                reset_account_lockout_after = int(reset_account_lockout_after)
 
-            if reset_account_lockout_after is not None:
-                if reset_account_lockout_after == "default":
-                    reset_account_lockout_after = 30
-                else:
-                    reset_account_lockout_after = int(reset_account_lockout_after)
+            if reset_account_lockout_after < 0 or reset_account_lockout_after > 99999:
+                raise CommandError("Maximum password age must be in the range of 0 to 99999!")
 
-                if reset_account_lockout_after < 0 or reset_account_lockout_after > 99999:
-                    raise CommandError("Maximum password age must be in the range of 0 to 99999!")
+            # minutes -> ticks
+            if reset_account_lockout_after == 0:
+                reset_account_lockout_after_ticks = -0x8000000000000000
+            else:
+                reset_account_lockout_after_ticks = -int(reset_account_lockout_after * (60 * 1e7))
 
-                # days -> ticks
-                if reset_account_lockout_after == 0:
-                    reset_account_lockout_after_ticks = -0x8000000000000000
-                else:
-                    reset_account_lockout_after_ticks = -int(reset_account_lockout_after * (60 * 1e7))
+            m["lockOutObservationWindow"] = ldb.MessageElement(str(reset_account_lockout_after_ticks),
+              ldb.FLAG_MOD_REPLACE, "lockOutObservationWindow")
+            msgs.append("Duration to reset account lockout after changed!")
 
-                m["lockOutObservationWindow"] = ldb.MessageElement(str(reset_account_lockout_after_ticks),
-                  ldb.FLAG_MOD_REPLACE, "lockOutObservationWindow")
-                msgs.append("Duration to reset account lockout after changed!")
+        if max_pwd_age > 0 and min_pwd_age >= max_pwd_age:
+            raise CommandError("Maximum password age (%d) must be greater than minimum password age (%d)!" % (max_pwd_age, min_pwd_age))
 
-            if max_pwd_age > 0 and min_pwd_age >= max_pwd_age:
-                raise CommandError("Maximum password age (%d) must be greater than minimum password age (%d)!" % (max_pwd_age, min_pwd_age))
+        if len(m) == 0:
+            raise CommandError("You must specify at least one option to set. Try --help")
+        samdb.modify(m)
+        msgs.append("All changes applied successfully!")
+        self.message("\n".join(msgs))
 
-            if len(m) == 0:
-                raise CommandError("You must specify at least one option to set. Try --help")
-            samdb.modify(m)
-            msgs.append("All changes applied successfully!")
-            self.message("\n".join(msgs))
-        else:
-            raise CommandError("Wrong argument '%s'!" % subcommand)
+class cmd_domain_passwordsettings(SuperCommand):
+    """Manage password policy settings."""
 
+    subcommands = {}
+    subcommands["show"] = cmd_domain_passwordsettings_show()
+    subcommands["set"] = cmd_domain_passwordsettings_set()
 
 class cmd_domain_classicupgrade(Command):
     """Upgrade from Samba classic (NT4-like) database to Samba AD DC database.
@@ -1751,6 +1810,9 @@ class DomainTrustCommand(Command):
             if require_pdc:
                 remote_flags |= nbt.NBT_SERVER_PDC
             remote_info = remote_net.finddc(flags=remote_flags, domain=domain, address=remote_server)
+        except NTSTATUSError as error:
+            raise CommandError("Failed to find a writeable DC for domain '%s': %s" %
+                               (domain, error[1]))
         except Exception:
             raise CommandError("Failed to find a writeable DC for domain '%s'" % domain)
         flag_map = {
@@ -2129,7 +2191,7 @@ class cmd_domain_trust_show(DomainTrustCommand):
             local_tdo_forest.count = 0
             local_tdo_forest.entries = []
 
-        self.outf.write("TrusteDomain:\n\n");
+        self.outf.write("TrustedDomain:\n\n");
         self.outf.write("NetbiosName:    %s\n" % local_tdo_info.netbios_name.string)
         if local_tdo_info.netbios_name.string != local_tdo_info.domain_name.string:
             self.outf.write("DnsName:        %s\n" % local_tdo_info.domain_name.string)
@@ -2295,7 +2357,7 @@ class cmd_domain_trust_create(DomainTrustCommand):
             # 512 bytes and a 2 bytes confounder is required.
             #
             def random_trust_secret(length):
-                pw = samba.generate_random_machine_password(length/2, length/2)
+                pw = samba.generate_random_machine_password(length//2, length//2)
                 return string_to_byte_array(pw.encode('utf-16-le'))
 
             if local_trust_info.trust_direction & lsa.LSA_TRUST_DIRECTION_INBOUND:
@@ -3830,7 +3892,7 @@ This command expunges tombstones from the database."""
                                                                current_time=current_time,
                                                                tombstone_lifetime=tombstone_lifetime)
 
-        except Exception, err:
+        except Exception as err:
             if started_transaction:
                 samdb.transaction_cancel()
             raise CommandError("Failed to expunge / garbage collect tombstones", err)
@@ -3895,7 +3957,21 @@ schemaUpdateNow: 1
         """Applies a single LDIF update to the schema"""
 
         try:
-            samdb.modify_ldif(self.ldif, controls=['relax:0'])
+            try:
+                samdb.modify_ldif(self.ldif, controls=['relax:0'])
+            except ldb.LdbError as e:
+                if e.args[0] == ldb.ERR_INVALID_ATTRIBUTE_SYNTAX:
+
+                    # REFRESH after a failed change
+
+                    # Otherwise the OID-to-attribute mapping in
+                    # _apply_updates_in_file() won't work, because it
+                    # can't lookup the new OID in the schema
+                    self._ldap_schemaUpdateNow(samdb)
+
+                    samdb.modify_ldif(self.ldif, controls=['relax:0'])
+                else:
+                    raise
         except ldb.LdbError as e:
             if self.can_ignore_failure(e):
                 return 0
@@ -3907,11 +3983,6 @@ schemaUpdateNow: 1
 
                 raise
 
-        # REFRESH AFTER EVERY CHANGE
-        # Otherwise the OID-to-attribute mapping in _apply_updates_in_file()
-        # won't work, because it can't lookup the new OID in the schema
-        self._ldap_schemaUpdateNow(samdb)
-
         return 1
 
 class cmd_domain_schema_upgrade(Command):
@@ -3936,7 +4007,7 @@ class cmd_domain_schema_upgrade(Command):
                default="2012_R2"),
         Option("--ldf-file", type=str, default=None,
                 help="Just apply the schema updates in the adprep/.LDF file(s) specified"),
-        Option("--base-dir", type=str, default=setup_path('adprep'),
+        Option("--base-dir", type=str, default=None,
                help="Location of ldf files Default is ${SETUPDIR}/adprep.")
     ]
 
@@ -4044,6 +4115,7 @@ class cmd_domain_schema_upgrade(Command):
         return count
 
     def run(self, **kwargs):
+        from samba.ms_schema_markdown import read_ms_markdown
         from samba.schema import Schema
 
         updates_allowed_overriden = False
@@ -4057,6 +4129,8 @@ class cmd_domain_schema_upgrade(Command):
         ldf_files = kwargs.get("ldf_file")
         base_dir = kwargs.get("base_dir")
 
+        temp_folder = None
+
         samdb = SamDB(url=H, session_info=system_session(), credentials=creds, lp=lp)
 
         # we're not going to get far if the config doesn't allow schema updates
@@ -4065,6 +4139,12 @@ class cmd_domain_schema_upgrade(Command):
             print("Temporarily overriding 'dsdb:schema update allowed' setting")
             updates_allowed_overriden = True
 
+        own_dn = ldb.Dn(samdb, samdb.get_dsServiceName())
+        master = get_fsmo_roleowner(samdb, str(samdb.get_schema_basedn()),
+                                    'schema')
+        if own_dn != master:
+            raise CommandError("This server is not the schema master.")
+
         # if specific LDIF files were specified, just apply them
         if ldf_files:
             schema_updates = ldf_files.split(",")
@@ -4082,8 +4162,48 @@ class cmd_domain_schema_upgrade(Command):
                 raise CommandError('Could not determine current schema version')
             start = int(res[0]['objectVersion'][0]) + 1
 
+            diff_dir = setup_path("adprep/WindowsServerDocs")
+            if base_dir is None:
+                # Read from the Schema-Updates.md file
+                temp_folder = tempfile.mkdtemp()
+
+                update_file = setup_path("adprep/WindowsServerDocs/Schema-Updates.md")
+
+                try:
+                    read_ms_markdown(update_file, temp_folder)
+                except Exception as e:
+                    print("Exception in markdown parsing: %s" % e)
+                    shutil.rmtree(temp_folder)
+                    raise CommandError('Failed to upgrade schema')
+
+                base_dir = temp_folder
+
             for version in range(start, end + 1):
-                schema_updates.append('Sch%d.ldf' % version)
+                update = 'Sch%d.ldf' % version
+                schema_updates.append(update)
+
+                # Apply patches if we parsed the Schema-Updates.md file
+                diff = os.path.abspath(os.path.join(diff_dir, update + '.diff'))
+                if temp_folder and os.path.exists(diff):
+                    try:
+                        p = subprocess.Popen(['patch', update, '-i', diff],
+                                             stdout=subprocess.PIPE,
+                                             stderr=subprocess.PIPE, cwd=temp_folder)
+                    except (OSError, IOError):
+                        shutil.rmtree(temp_folder)
+                        raise CommandError("Failed to upgrade schema. Check if 'patch' is installed.")
+
+                    stdout, stderr = p.communicate()
+
+                    if p.returncode:
+                        print("Exception in patch: %s\n%s" % (stdout, stderr))
+                        shutil.rmtree(temp_folder)
+                        raise CommandError('Failed to upgrade schema')
+
+                    print("Patched %s using %s" % (update, diff))
+
+        if base_dir is None:
+            base_dir = setup_path("adprep")
 
         samdb.transaction_start()
         count = 0
@@ -4109,9 +4229,118 @@ class cmd_domain_schema_upgrade(Command):
         if updates_allowed_overriden:
             lp.set("dsdb:schema update allowed", "no")
 
+        if temp_folder:
+            shutil.rmtree(temp_folder)
+
         if error_encountered:
             raise CommandError('Failed to upgrade schema')
 
+class cmd_domain_functional_prep(Command):
+    """Domain functional level preparation"""
+
+    synopsis = "%prog [options]"
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "versionopts": options.VersionOptions,
+        "credopts": options.CredentialsOptions,
+    }
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server", type=str,
+               metavar="URL", dest="H"),
+        Option("--quiet", help="Be quiet", action="store_true"),
+        Option("--verbose", help="Be verbose", action="store_true"),
+        Option("--function-level", type="choice", metavar="FUNCTION_LEVEL",
+               choices=["2008_R2", "2012", "2012_R2"],
+               help="The schema file to upgrade to. Default is (Windows) 2012_R2.",
+               default="2012_R2"),
+        Option("--forest-prep", action="store_true",
+               help="Run the forest prep (by default, both the domain and forest prep are run)."),
+        Option("--domain-prep", action="store_true",
+               help="Run the domain prep (by default, both the domain and forest prep are run).")
+    ]
+
+    def run(self, **kwargs):
+        updates_allowed_overriden = False
+        sambaopts = kwargs.get("sambaopts")
+        credopts = kwargs.get("credopts")
+        versionpts = kwargs.get("versionopts")
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp)
+        H = kwargs.get("H")
+        target_level = string_version_to_constant[kwargs.get("function_level")]
+        forest_prep = kwargs.get("forest_prep")
+        domain_prep = kwargs.get("domain_prep")
+
+        samdb = SamDB(url=H, session_info=system_session(), credentials=creds, lp=lp)
+
+        # we're not going to get far if the config doesn't allow schema updates
+        if lp.get("dsdb:schema update allowed") is None:
+            lp.set("dsdb:schema update allowed", "yes")
+            print("Temporarily overriding 'dsdb:schema update allowed' setting")
+            updates_allowed_overriden = True
+
+        if forest_prep is None and domain_prep is None:
+            forest_prep = True
+            domain_prep = True
+
+        own_dn = ldb.Dn(samdb, samdb.get_dsServiceName())
+        if forest_prep:
+            master = get_fsmo_roleowner(samdb, str(samdb.get_schema_basedn()),
+                                        'schema')
+            if own_dn != master:
+                raise CommandError("This server is not the schema master.")
+
+        if domain_prep:
+            domain_dn = samdb.domain_dn()
+            infrastructure_dn = "CN=Infrastructure," + domain_dn
+            master = get_fsmo_roleowner(samdb, infrastructure_dn,
+                                       'infrastructure')
+            if own_dn != master:
+                raise CommandError("This server is not the infrastructure master.")
+
+        if forest_prep:
+            samdb.transaction_start()
+            error_encountered = False
+            try:
+                from samba.forest_update import ForestUpdate
+                forest = ForestUpdate(samdb, fix=True)
+
+                forest.check_updates_iterator([53, 79, 80, 81, 82, 83])
+                forest.check_updates_functional_level(target_level,
+                                                      DS_DOMAIN_FUNCTION_2008_R2,
+                                                      update_revision=True)
+
+                samdb.transaction_commit()
+            except Exception as e:
+                print("Exception: %s" % e)
+                samdb.transaction_cancel()
+                error_encountered = True
+
+        if domain_prep:
+            samdb.transaction_start()
+            error_encountered = False
+            try:
+                from samba.domain_update import DomainUpdate
+
+                domain = DomainUpdate(samdb, fix=True)
+                domain.check_updates_functional_level(target_level,
+                                                      DS_DOMAIN_FUNCTION_2008,
+                                                      update_revision=True)
+
+                samdb.transaction_commit()
+            except Exception as e:
+                print("Exception: %s" % e)
+                samdb.transaction_cancel()
+                error_encountered = True
+
+        if updates_allowed_overriden:
+            lp.set("dsdb:schema update allowed", "no")
+
+        if error_encountered:
+            raise CommandError('Failed to perform functional prep')
+
 class cmd_domain(SuperCommand):
     """Domain management."""
 
@@ -4130,3 +4359,4 @@ class cmd_domain(SuperCommand):
     subcommands["trust"] = cmd_domain_trust()
     subcommands["tombstones"] = cmd_domain_tombstones()
     subcommands["schemaupgrade"] = cmd_domain_schema_upgrade()
+    subcommands["functionalprep"] = cmd_domain_functional_prep()