libds:common Remove DS_DC_* domain functionality flags
[kai/samba-autobuild/.git] / source4 / scripting / python / samba / provision.py
index ec8dc100ef7ff5ff64314e9fea7a7c3370e8854b..1b7cfca64d4d84f84919c3d25846ebb53afb5b80 100644 (file)
@@ -27,6 +27,7 @@
 
 from base64 import b64encode
 import os
+import re
 import pwd
 import grp
 import logging
@@ -42,7 +43,7 @@ from samba.auth import system_session, admin_session
 import samba
 from samba import version, Ldb, substitute_var, valid_netbios_name
 from samba import check_all_substituted, read_and_sub_file, setup_file
-from samba.dsdb import DS_DOMAIN_FUNCTION_2003, DS_DC_FUNCTION_2008
+from samba.dsdb import DS_DOMAIN_FUNCTION_2003, DS_DOMAIN_FUNCTION_2008
 from samba.dcerpc import security
 from samba.dcerpc.misc import SEC_CHAN_BDC, SEC_CHAN_WKSTA
 from samba.idmap import IDmapDB
@@ -65,10 +66,12 @@ __docformat__ = "restructuredText"
 def find_setup_dir():
     """Find the setup directory used by provision."""
     import sys
-    for suffix in ["share/setup", "share/samba/setup", "setup"]:
-        ret = os.path.join(sys.prefix, suffix)
-        if os.path.isdir(ret):
-            return ret
+    for prefix in [sys.prefix,
+            os.path.join(os.path.dirname(__file__), "../../../..")]:
+        for suffix in ["share/setup", "share/samba/setup", "setup"]:
+            ret = os.path.join(prefix, suffix)
+            if os.path.isdir(ret):
+                return ret
     # In source tree
     dirname = os.path.dirname(__file__)
     ret = os.path.join(dirname, "../../../setup")
@@ -80,6 +83,7 @@ def find_setup_dir():
 # hard coded at this point, but will probably be changed when
 # we enable different fsmo roles
 
+
 def get_config_descriptor(domain_sid):
     sddl = "O:EAG:EAD:(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \
            "(OA;;CR;1131f6ab-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \
@@ -153,6 +157,7 @@ def get_domain_descriptor(domain_sid):
     return ndr_pack(sec)
 
 DEFAULTSITE = "Default-First-Site-Name"
+LAST_PROVISION_USN_ATTRIBUTE = "lastProvisionUSN"
 
 class ProvisionPaths(object):
 
@@ -189,7 +194,99 @@ class ProvisionNames(object):
         self.hostname = None
         self.sitename = None
         self.smbconf = None
+
+
+def update_provision_usn(samdb, low, high, replace=False):
+    """Update the field provisionUSN in sam.ldb
+
+    This field is used to track range of USN modified by provision and 
+    upgradeprovision.
+    This value is used afterward by next provision to figure out if 
+    the field have been modified since last provision.
+
+    :param samdb: An LDB object connect to sam.ldb
+    :param low: The lowest USN modified by this upgrade
+    :param high: The highest USN modified by this upgrade
+    :param replace: A boolean indicating if the range should replace any 
+                    existing one or appended (default)
+    """
+
+    tab = []
+    if not replace:
+        entry = samdb.search(expression="(&(dn=@PROVISION)(%s=*))" % \
+                                LAST_PROVISION_USN_ATTRIBUTE, base="", 
+                                scope=ldb.SCOPE_SUBTREE,
+                                attrs=[LAST_PROVISION_USN_ATTRIBUTE, "dn"])
+        for e in entry[0][LAST_PROVISION_USN_ATTRIBUTE]:
+            tab.append(str(e))
+
+    tab.append("%s-%s" % (low, high))
+    delta = ldb.Message()
+    delta.dn = ldb.Dn(samdb, "@PROVISION")
+    delta[LAST_PROVISION_USN_ATTRIBUTE] = ldb.MessageElement(tab,
+                                                    ldb.FLAG_MOD_REPLACE,
+                                                    LAST_PROVISION_USN_ATTRIBUTE)
+    samdb.modify(delta)
+
+
+def set_provision_usn(samdb, low, high):
+    """Set the field provisionUSN in sam.ldb
+    This field is used to track range of USN modified by provision and
+    upgradeprovision.
+    This value is used afterward by next provision to figure out if
+    the field have been modified since last provision.
+
+    :param samdb: An LDB object connect to sam.ldb
+    :param low: The lowest USN modified by this upgrade
+    :param high: The highest USN modified by this upgrade"""
+    tab = []
+    tab.append("%s-%s" % (low, high))
+    delta = ldb.Message()
+    delta.dn = ldb.Dn(samdb, "@PROVISION")
+    delta[LAST_PROVISION_USN_ATTRIBUTE] = ldb.MessageElement(tab,
+                                                  ldb.FLAG_MOD_ADD,
+                                                  LAST_PROVISION_USN_ATTRIBUTE)
+    samdb.add(delta)
+
+
+def get_max_usn(samdb,basedn):
+    """ This function return the biggest USN present in the provision
+
+    :param samdb: A LDB object pointing to the sam.ldb
+    :param basedn: A string containing the base DN of the provision
+                    (ie. DC=foo, DC=bar)
+    :return: The biggest USN in the provision"""
+
+    res = samdb.search(expression="objectClass=*",base=basedn,
+                         scope=ldb.SCOPE_SUBTREE,attrs=["uSNChanged"],
+                         controls=["search_options:1:2",
+                                   "server_sort:1:1:uSNChanged",
+                                   "paged_results:1:1"])
+    return res[0]["uSNChanged"]
     
+def get_last_provision_usn(sam):
+    """Get the lastest USN modified by a provision or an upgradeprovision
+
+    :param sam: An LDB object pointing to the sam.ldb
+    :return an integer corresponding to the highest USN modified by 
+            (upgrade)provision, 0 is this value is unknown"""
+
+    entry = sam.search(expression="(&(dn=@PROVISION)(%s=*))" % \
+                        LAST_PROVISION_USN_ATTRIBUTE,
+                        base="", scope=ldb.SCOPE_SUBTREE,
+                        attrs=[LAST_PROVISION_USN_ATTRIBUTE])
+    if len(entry):
+        range = []
+        idx = 0
+        p = re.compile(r'-')
+        for r in entry[0][LAST_PROVISION_USN_ATTRIBUTE]:
+            tab = p.split(str(r))
+            range.append(tab[0])
+            range.append(tab[1])
+            idx = idx + 1
+        return range
+    else:
+        return None
 
 class ProvisionResult(object):
 
@@ -452,7 +549,7 @@ def make_smbconf(smbconf, setup_path, hostname, domain, realm, serverrole,
             privdir = os.path.join(targetdir, "private")
         else:
             privdir = default_lp.get("private dir")
-        posixeadb_line = "posix:eadb = " + os.path.abspath(os.path.join(privdir,"eadb.tdb"))
+        posixeadb_line = "posix:eadb = " + os.path.abspath(os.path.join(privdir, "eadb.tdb"))
     else:
         posixeadb_line = ""
 
@@ -655,6 +752,9 @@ def secretsdb_setup_dns(secretsdb, setup_path, private_dir,
 def setup_secretsdb(path, setup_path, session_info, backend_credentials, lp):
     """Setup the secrets database.
 
+   :note: This function does not handle exceptions and transaction on purpose,
+   it's up to the caller to do this job.
+
     :param path: Path to the secrets database.
     :param setup_path: Get the path to a setup file.
     :param session_info: Session info.
@@ -671,22 +771,26 @@ def setup_secretsdb(path, setup_path, session_info, backend_credentials, lp):
     secrets_ldb = Ldb(path, session_info=session_info, 
                       lp=lp)
     secrets_ldb.transaction_start()
-    secrets_ldb.load_ldif_file_add(setup_path("secrets.ldif"))
-
-    if backend_credentials is not None and backend_credentials.authentication_requested():
-        if backend_credentials.get_bind_dn() is not None:
-            setup_add_ldif(secrets_ldb, setup_path("secrets_simple_ldap.ldif"), {
-                    "LDAPMANAGERDN": backend_credentials.get_bind_dn(),
-                    "LDAPMANAGERPASS_B64": b64encode(backend_credentials.get_password())
-                    })
-        else:
-            setup_add_ldif(secrets_ldb, setup_path("secrets_sasl_ldap.ldif"), {
-                    "LDAPADMINUSER": backend_credentials.get_username(),
-                    "LDAPADMINREALM": backend_credentials.get_realm(),
-                    "LDAPADMINPASS_B64": b64encode(backend_credentials.get_password())
-                    })
-
-    return secrets_ldb
+    try:
+        secrets_ldb.load_ldif_file_add(setup_path("secrets.ldif"))
+
+        if backend_credentials is not None and backend_credentials.authentication_requested():
+            if backend_credentials.get_bind_dn() is not None:
+                setup_add_ldif(secrets_ldb, setup_path("secrets_simple_ldap.ldif"), {
+                        "LDAPMANAGERDN": backend_credentials.get_bind_dn(),
+                        "LDAPMANAGERPASS_B64": b64encode(backend_credentials.get_password())
+                        })
+            else:
+                setup_add_ldif(secrets_ldb, setup_path("secrets_sasl_ldap.ldif"), {
+                        "LDAPADMINUSER": backend_credentials.get_username(),
+                        "LDAPADMINREALM": backend_credentials.get_realm(),
+                        "LDAPADMINPASS_B64": b64encode(backend_credentials.get_password())
+                        })
+
+        return secrets_ldb
+    except:
+        secrets_ldb.transaction_cancel()
+        raise
 
 def setup_privileges(path, setup_path, session_info, lp):
     """Setup the privileges database.
@@ -851,7 +955,7 @@ def setup_samdb(path, setup_path, session_info, provision_backend, lp, names,
 
     # ATTENTION: Do NOT change these default values without discussion with the
     # team and/or release manager. They have a big impact on the whole program!
-    domainControllerFunctionality = DS_DC_FUNCTION_2008
+    domainControllerFunctionality = DS_DOMAIN_FUNCTION_2008
 
     if dom_for_fun_level is None:
         dom_for_fun_level = DS_DOMAIN_FUNCTION_2003
@@ -879,7 +983,7 @@ def setup_samdb(path, setup_path, session_info, provision_backend, lp, names,
     logger.info("Pre-loading the Samba 4 and AD schema")
 
     # Load the schema from the one we computed earlier
-    samdb.set_schema_from_ldb(schema.ldb)
+    samdb.set_schema(schema)
 
     # And now we can connect to the DB - the schema won't be loaded from the DB
     samdb.connect(path)
@@ -1068,7 +1172,7 @@ def set_gpo_acl(sysvol, dnsdomain, domainsid, domaindn, samdb, lp):
     set_dir_acl(policy_path,dsacl2fsacl(POLICIES_ACL, str(domainsid)), 
         lp, str(domainsid))
     res = samdb.search(base="CN=Policies,CN=System,%s"%(domaindn),
-                        attrs=["cn","nTSecurityDescriptor"],
+                        attrs=["cn", "nTSecurityDescriptor"],
                         expression="", scope=ldb.SCOPE_ONELEVEL)
     for policy in res:
         acl = ndr_unpack(security.descriptor, 
@@ -1231,8 +1335,8 @@ def provision(setup_dir, logger, session_info,
 
     if not os.path.exists(paths.private_dir):
         os.mkdir(paths.private_dir)
-    if not os.path.exists(os.path.join(paths.private_dir,"tls")):
-        os.mkdir(os.path.join(paths.private_dir,"tls"))
+    if not os.path.exists(os.path.join(paths.private_dir, "tls")):
+        os.mkdir(os.path.join(paths.private_dir, "tls"))
 
     ldapi_url = "ldapi://%s" % urllib.quote(paths.s4_ldapi_path, safe="")
  
@@ -1301,111 +1405,122 @@ def provision(setup_dir, logger, session_info,
         session_info=session_info,
         backend_credentials=provision_backend.secrets_credentials, lp=lp)
 
-    logger.info("Setting up the registry")
-    setup_registry(paths.hklm, setup_path, session_info, 
-                   lp=lp)
-
-    logger.info("Setting up the privileges database")
-    setup_privileges(paths.privilege, setup_path, session_info, lp=lp)
-
-    logger.info("Setting up idmap db")
-    idmap = setup_idmapdb(paths.idmapdb, setup_path, session_info=session_info,
-                          lp=lp)
-
-    logger.info("Setting up SAM db")
-    samdb = setup_samdb(paths.samdb, setup_path, session_info, 
-                        provision_backend, lp, names,
-                        logger=logger, 
-                        domainsid=domainsid, 
-                        schema=schema, domainguid=domainguid,
-                        policyguid=policyguid, policyguid_dc=policyguid_dc,
-                        fill=samdb_fill, 
-                        adminpass=adminpass, krbtgtpass=krbtgtpass,
-                        invocationid=invocationid, 
-                        machinepass=machinepass, dnspass=dnspass, 
-                        ntdsguid=ntdsguid, serverrole=serverrole,
-                        dom_for_fun_level=dom_for_fun_level, am_rodc=am_rodc)
+    try:
+        logger.info("Setting up the registry")
+        setup_registry(paths.hklm, setup_path, session_info, 
+                       lp=lp)
 
-    if serverrole == "domain controller":
-        if paths.netlogon is None:
-            logger.info("Existing smb.conf does not have a [netlogon] share, but you are configuring a DC.")
-            logger.info("Please either remove %s or see the template at %s" % 
-                    (paths.smbconf, setup_path("provision.smb.conf.dc")))
-            assert paths.netlogon is not None
-
-        if paths.sysvol is None:
-            logger.info("Existing smb.conf does not have a [sysvol] share, but you"
-                    " are configuring a DC.")
-            logger.info("Please either remove %s or see the template at %s" % 
-                    (paths.smbconf, setup_path("provision.smb.conf.dc")))
-            assert paths.sysvol is not None
-
-        if not os.path.isdir(paths.netlogon):
-            os.makedirs(paths.netlogon, 0755)
+        logger.info("Setting up the privileges database")
+        setup_privileges(paths.privilege, setup_path, session_info, lp=lp)
 
-    if samdb_fill == FILL_FULL:
-        setup_name_mappings(samdb, idmap, str(domainsid), names.domaindn,
-                            root_uid=root_uid, nobody_uid=nobody_uid,
-                            users_gid=users_gid, wheel_gid=wheel_gid)
+        logger.info("Setting up idmap db")
+        idmap = setup_idmapdb(paths.idmapdb, setup_path, session_info=session_info,
+                              lp=lp)
 
-        if serverrole == "domain controller":
-            # Set up group policies (domain policy and domain controller policy)
-            setup_gpo(paths.sysvol, names.dnsdomain, policyguid, policyguid_dc)
-            setsysvolacl(samdb, paths.netlogon, paths.sysvol, wheel_gid, 
-                         domainsid, names.dnsdomain, names.domaindn, lp)
-
-        logger.info("Setting up sam.ldb rootDSE marking as synchronized")
-        setup_modify_ldif(samdb, setup_path("provision_rootdse_modify.ldif"))
-
-        secretsdb_self_join(secrets_ldb, domain=names.domain,
-                            realm=names.realm,
-                            dnsdomain=names.dnsdomain,
-                            netbiosname=names.netbiosname,
+        logger.info("Setting up SAM db")
+        samdb = setup_samdb(paths.samdb, setup_path, session_info, 
+                            provision_backend, lp, names,
+                            logger=logger, 
                             domainsid=domainsid, 
-                            machinepass=machinepass,
-                            secure_channel_type=SEC_CHAN_BDC)
+                            schema=schema, domainguid=domainguid,
+                            policyguid=policyguid, policyguid_dc=policyguid_dc,
+                            fill=samdb_fill, 
+                            adminpass=adminpass, krbtgtpass=krbtgtpass,
+                            invocationid=invocationid, 
+                            machinepass=machinepass, dnspass=dnspass, 
+                            ntdsguid=ntdsguid, serverrole=serverrole,
+                            dom_for_fun_level=dom_for_fun_level, am_rodc=am_rodc)
 
         if serverrole == "domain controller":
-            secretsdb_setup_dns(secrets_ldb, setup_path,
-                                paths.private_dir,
-                                realm=names.realm, dnsdomain=names.dnsdomain,
-                                dns_keytab_path=paths.dns_keytab,
-                                dnspass=dnspass)
-
-            domainguid = samdb.searchone(basedn=domaindn, attribute="objectGUID")
-            assert isinstance(domainguid, str)
-
-            # Only make a zone file on the first DC, it should be replicated
-            # with DNS replication
-            create_zone_file(lp, logger, paths, targetdir, setup_path,
-                dnsdomain=names.dnsdomain, hostip=hostip, hostip6=hostip6,
-                hostname=names.hostname, realm=names.realm, 
-                domainguid=domainguid, ntdsguid=names.ntdsguid)
-
-            create_named_conf(paths, setup_path, realm=names.realm,
-                              dnsdomain=names.dnsdomain, private_dir=paths.private_dir)
-
-            create_named_txt(paths.namedtxt, setup_path, realm=names.realm,
-                              dnsdomain=names.dnsdomain, private_dir=paths.private_dir,
-                              keytab_name=paths.dns_keytab)
-            logger.info("See %s for an example configuration include file for BIND", paths.namedconf)
-            logger.info("and %s for further documentation required for secure DNS "
-                    "updates", paths.namedtxt)
-
-            create_krb5_conf(paths.krb5conf, setup_path,
-                             dnsdomain=names.dnsdomain, hostname=names.hostname,
-                             realm=names.realm)
-            logger.info("A Kerberos configuration suitable for Samba 4 has been "
-                    "generated at %s", paths.krb5conf)
+            if paths.netlogon is None:
+                logger.info("Existing smb.conf does not have a [netlogon] share, but you are configuring a DC.")
+                logger.info("Please either remove %s or see the template at %s" % 
+                        (paths.smbconf, setup_path("provision.smb.conf.dc")))
+                assert paths.netlogon is not None
+
+            if paths.sysvol is None:
+                logger.info("Existing smb.conf does not have a [sysvol] share, but you"
+                        " are configuring a DC.")
+                logger.info("Please either remove %s or see the template at %s" % 
+                        (paths.smbconf, setup_path("provision.smb.conf.dc")))
+                assert paths.sysvol is not None
+
+            if not os.path.isdir(paths.netlogon):
+                os.makedirs(paths.netlogon, 0755)
+
+        if samdb_fill == FILL_FULL:
+            setup_name_mappings(samdb, idmap, str(domainsid), names.domaindn,
+                                root_uid=root_uid, nobody_uid=nobody_uid,
+                                users_gid=users_gid, wheel_gid=wheel_gid)
+
+            if serverrole == "domain controller":
+                # Set up group policies (domain policy and domain controller policy)
+                setup_gpo(paths.sysvol, names.dnsdomain, policyguid, policyguid_dc)
+                setsysvolacl(samdb, paths.netlogon, paths.sysvol, wheel_gid, 
+                             domainsid, names.dnsdomain, names.domaindn, lp)
+
+            logger.info("Setting up sam.ldb rootDSE marking as synchronized")
+            setup_modify_ldif(samdb, setup_path("provision_rootdse_modify.ldif"))
+
+            secretsdb_self_join(secrets_ldb, domain=names.domain,
+                                realm=names.realm,
+                                dnsdomain=names.dnsdomain,
+                                netbiosname=names.netbiosname,
+                                domainsid=domainsid, 
+                                machinepass=machinepass,
+                                secure_channel_type=SEC_CHAN_BDC)
+
+            if serverrole == "domain controller":
+                secretsdb_setup_dns(secrets_ldb, setup_path,
+                                    paths.private_dir,
+                                    realm=names.realm, dnsdomain=names.dnsdomain,
+                                    dns_keytab_path=paths.dns_keytab,
+                                    dnspass=dnspass)
+
+                domainguid = samdb.searchone(basedn=domaindn, attribute="objectGUID")
+                assert isinstance(domainguid, str)
+
+                # Only make a zone file on the first DC, it should be replicated
+                # with DNS replication
+                create_zone_file(lp, logger, paths, targetdir, setup_path,
+                    dnsdomain=names.dnsdomain, hostip=hostip, hostip6=hostip6,
+                    hostname=names.hostname, realm=names.realm, 
+                    domainguid=domainguid, ntdsguid=names.ntdsguid)
+
+                create_named_conf(paths, setup_path, realm=names.realm,
+                                  dnsdomain=names.dnsdomain, private_dir=paths.private_dir)
+
+                create_named_txt(paths.namedtxt, setup_path, realm=names.realm,
+                                  dnsdomain=names.dnsdomain, private_dir=paths.private_dir,
+                                  keytab_name=paths.dns_keytab)
+                logger.info("See %s for an example configuration include file for BIND", paths.namedconf)
+                logger.info("and %s for further documentation required for secure DNS "
+                        "updates", paths.namedtxt)
+
+                create_krb5_conf(paths.krb5conf, setup_path,
+                                 dnsdomain=names.dnsdomain, hostname=names.hostname,
+                                 realm=names.realm)
+                logger.info("A Kerberos configuration suitable for Samba 4 has been "
+                        "generated at %s", paths.krb5conf)
+
+            lastProvisionUSNs = get_last_provision_usn(samdb)
+            maxUSN = get_max_usn(samdb, str(names.rootdn))
+            if lastProvisionUSNs is not None:
+                update_provision_usn(samdb, 0, maxUSN, 1)
+            else:
+                set_provision_usn(samdb, 0, maxUSN)
 
-    if serverrole == "domain controller":
-        create_dns_update_list(lp, logger, paths, setup_path)
+        if serverrole == "domain controller":
+            create_dns_update_list(lp, logger, paths, setup_path)
 
-    provision_backend.post_setup()
-    provision_backend.shutdown()
-    
-    create_phpldapadmin_config(paths.phpldapadminconfig, setup_path, 
-                               ldapi_url)
+        provision_backend.post_setup()
+        provision_backend.shutdown()
+        
+        create_phpldapadmin_config(paths.phpldapadminconfig, setup_path, 
+                                   ldapi_url)
+    except:
+        secrets_ldb.transaction_cancel()
+        raise
 
     #Now commit the secrets.ldb to disk
     secrets_ldb.transaction_commit()
@@ -1447,7 +1562,6 @@ def provision(setup_dir, logger, session_info,
             logger.info("This slapd-Commandline is also stored under: %s/ldap_backend_startup.sh", 
                     provision_backend.ldapdir)
 
-
     result = ProvisionResult()
     result.domaindn = domaindn
     result.paths = paths
@@ -1610,6 +1724,7 @@ def create_named_conf(paths, setup_path, realm, dnsdomain,
 
     setup_file(setup_path("named.conf.update"), paths.namedconf_update)
 
+
 def create_named_txt(path, setup_path, realm, dnsdomain,
                       private_dir, keytab_name):
     """Write out a file containing zone statements suitable for inclusion in a
@@ -1631,6 +1746,7 @@ def create_named_txt(path, setup_path, realm, dnsdomain,
             "PRIVATE_DIR": private_dir
         })
 
+
 def create_krb5_conf(path, setup_path, dnsdomain, hostname, realm):
     """Write out a file containing zone statements suitable for inclusion in a
     named.conf file (including GSS-TSIG configuration).