samba-tool: Add new command 'samba-tool drs clone-dc-database'
authorAndrew Bartlett <abartlet@samba.org>
Mon, 17 Aug 2015 03:33:31 +0000 (15:33 +1200)
committerAndrew Bartlett <abartlet@samba.org>
Mon, 26 Oct 2015 04:11:21 +0000 (05:11 +0100)
This command makes a clone of an existing AD Domain, but does not
join the domain.  This allows us to test if the join would work
without adding objects to the target DC.

The server password will need to be reset for the clone to
be any use, see the source4/scripting/devel/chgtdcpass

(Based on patches written with Garming Sam)

Andrew Bartlett

Signed-off-by: Andrew Bartlett <abartlet@samba.org>
Signed-off-by: Garming Sam <garming@catalyst.net.nz>
Reviewed-by: Garming Sam <garming@catalyst.net.nz>
python/samba/join.py
python/samba/netcmd/drs.py
python/samba/tests/__init__.py
python/samba/tests/blackbox/samba_tool_drs.py

index c356145276560fd2b27109d7a1e4f554f56cc125..0f7dde237d166caf6c7f5e7a9180b53831680e16 100644 (file)
@@ -54,12 +54,13 @@ class dc_join(object):
     def __init__(ctx, logger=None, server=None, creds=None, lp=None, site=None,
                  netbios_name=None, targetdir=None, domain=None,
                  machinepass=None, use_ntvfs=False, dns_backend=None,
-                 promote_existing=False):
+                 promote_existing=False, clone_only=False):
+        ctx.clone_only=clone_only
+
         ctx.logger = logger
         ctx.creds = creds
         ctx.lp = lp
         ctx.site = site
-        ctx.netbios_name = netbios_name
         ctx.targetdir = targetdir
         ctx.use_ntvfs = use_ntvfs
 
@@ -89,8 +90,6 @@ class dc_join(object):
             raise DCJoinException(estr)
 
 
-        ctx.myname = netbios_name
-        ctx.samname = "%s$" % ctx.myname
         ctx.base_dn = str(ctx.samdb.get_default_basedn())
         ctx.root_dn = str(ctx.samdb.get_root_basedn())
         ctx.schema_dn = str(ctx.samdb.get_schema_basedn())
@@ -110,17 +109,34 @@ class dc_join(object):
         else:
             ctx.acct_pass = samba.generate_random_password(32, 40)
 
-        # work out the DNs of all the objects we will be adding
-        ctx.server_dn = "CN=%s,CN=Servers,CN=%s,CN=Sites,%s" % (ctx.myname, ctx.site, ctx.config_dn)
-        ctx.ntds_dn = "CN=NTDS Settings,%s" % ctx.server_dn
-        topology_base = "CN=Topology,CN=Domain System Volume,CN=DFSR-GlobalSettings,CN=System,%s" % ctx.base_dn
-        if ctx.dn_exists(topology_base):
-            ctx.topology_dn = "CN=%s,%s" % (ctx.myname, topology_base)
+        ctx.dnsdomain = ctx.samdb.domain_dns_name()
+        if clone_only:
+            # As we don't want to create or delete these DNs, we set them to None
+            ctx.server_dn = None
+            ctx.ntds_dn = None
+            ctx.acct_dn = None
+            ctx.myname = ctx.server.split('.')[0]
+            ctx.ntds_guid = None
         else:
-            ctx.topology_dn = None
+            # work out the DNs of all the objects we will be adding
+            ctx.myname = netbios_name
+            ctx.samname = "%s$" % ctx.myname
+            ctx.server_dn = "CN=%s,CN=Servers,CN=%s,CN=Sites,%s" % (ctx.myname, ctx.site, ctx.config_dn)
+            ctx.ntds_dn = "CN=NTDS Settings,%s" % ctx.server_dn
+            ctx.acct_dn = "CN=%s,OU=Domain Controllers,%s" % (ctx.myname, ctx.base_dn)
+            ctx.dnshostname = "%s.%s" % (ctx.myname.lower(), ctx.dnsdomain)
+            ctx.dnsforest = ctx.samdb.forest_dns_name()
+
+            topology_base = "CN=Topology,CN=Domain System Volume,CN=DFSR-GlobalSettings,CN=System,%s" % ctx.base_dn
+            if ctx.dn_exists(topology_base):
+                ctx.topology_dn = "CN=%s,%s" % (ctx.myname, topology_base)
+            else:
+                ctx.topology_dn = None
+
+            ctx.SPNs = [ "HOST/%s" % ctx.myname,
+                         "HOST/%s" % ctx.dnshostname,
+                         "GC/%s/%s" % (ctx.dnshostname, ctx.dnsforest) ]
 
-        ctx.dnsdomain = ctx.samdb.domain_dns_name()
-        ctx.dnsforest = ctx.samdb.forest_dns_name()
         ctx.domaindns_zone = 'DC=DomainDnsZones,%s' % ctx.base_dn
         ctx.forestdns_zone = 'DC=ForestDnsZones,%s' % ctx.root_dn
 
@@ -137,18 +153,10 @@ class dc_join(object):
             else:
                 ctx.dns_backend = dns_backend
 
-        ctx.dnshostname = "%s.%s" % (ctx.myname.lower(), ctx.dnsdomain)
-
         ctx.realm = ctx.dnsdomain
 
-        ctx.acct_dn = "CN=%s,OU=Domain Controllers,%s" % (ctx.myname, ctx.base_dn)
-
         ctx.tmp_samdb = None
 
-        ctx.SPNs = [ "HOST/%s" % ctx.myname,
-                     "HOST/%s" % ctx.dnshostname,
-                     "GC/%s/%s" % (ctx.dnshostname, ctx.dnsforest) ]
-
         # these elements are optional
         ctx.never_reveal_sid = None
         ctx.reveal_sid = None
@@ -538,28 +546,30 @@ class dc_join(object):
         if ctx.krbtgt_dn:
             ctx.add_krbtgt_account()
 
-        print "Adding %s" % ctx.server_dn
-        rec = {
-            "dn": ctx.server_dn,
-            "objectclass" : "server",
-            # windows uses 50000000 decimal for systemFlags. A windows hex/decimal mixup bug?
-            "systemFlags" : str(samba.dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
-                                samba.dsdb.SYSTEM_FLAG_CONFIG_ALLOW_LIMITED_MOVE |
-                                samba.dsdb.SYSTEM_FLAG_DISALLOW_MOVE_ON_DELETE),
-            # windows seems to add the dnsHostName later
-            "dnsHostName" : ctx.dnshostname}
-
-        if ctx.acct_dn:
-            rec["serverReference"] = ctx.acct_dn
+        if ctx.server_dn:
+            print "Adding %s" % ctx.server_dn
+            rec = {
+                "dn": ctx.server_dn,
+                "objectclass" : "server",
+                # windows uses 50000000 decimal for systemFlags. A windows hex/decimal mixup bug?
+                "systemFlags" : str(samba.dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
+                                    samba.dsdb.SYSTEM_FLAG_CONFIG_ALLOW_LIMITED_MOVE |
+                                    samba.dsdb.SYSTEM_FLAG_DISALLOW_MOVE_ON_DELETE),
+                # windows seems to add the dnsHostName later
+                "dnsHostName" : ctx.dnshostname}
+
+            if ctx.acct_dn:
+                rec["serverReference"] = ctx.acct_dn
 
-        ctx.samdb.add(rec)
+            ctx.samdb.add(rec)
 
         if ctx.subdomain:
             # the rest is done after replication
             ctx.ntds_guid = None
             return
 
-        ctx.join_add_ntdsdsa()
+        if ctx.ntds_dn:
+            ctx.join_add_ntdsdsa()
 
         if ctx.connection_dn is not None:
             print "Adding %s" % ctx.connection_dn
@@ -876,15 +886,17 @@ class dc_join(object):
         """Finalise the join, mark us synchronised and setup secrets db."""
 
         # FIXME we shouldn't do this in all cases
+
         # If for some reasons we joined in another site than the one of
         # DC we just replicated from then we don't need to send the updatereplicateref
         # as replication between sites is time based and on the initiative of the
         # requesting DC
-        ctx.logger.info("Sending DsReplicaUpdateRefs for all the replicated partitions")
-        for nc in ctx.nc_list:
-            ctx.send_DsReplicaUpdateRefs(nc)
+        if not ctx.clone_only:
+            ctx.logger.info("Sending DsReplicaUpdateRefs for all the replicated partitions")
+            for nc in ctx.nc_list:
+                ctx.send_DsReplicaUpdateRefs(nc)
 
-        if ctx.RODC:
+        if not ctx.clone_only and ctx.RODC:
             print "Setting RODC invocationId"
             ctx.local_samdb.set_invocation_id(str(ctx.invocation_id))
             ctx.local_samdb.set_opaque_integer("domainFunctionality",
@@ -914,11 +926,18 @@ class dc_join(object):
         m = ldb.Message()
         m.dn = ldb.Dn(ctx.local_samdb, '@ROOTDSE')
         m["isSynchronized"] = ldb.MessageElement("TRUE", ldb.FLAG_MOD_REPLACE, "isSynchronized")
-        m["dsServiceName"] = ldb.MessageElement("<GUID=%s>" % str(ctx.ntds_guid),
+
+        # We want to appear to be the server we just cloned
+        if ctx.clone_only:
+            guid = ctx.samdb.get_ntds_GUID()
+        else:
+            guid = ctx.ntds_guid
+
+        m["dsServiceName"] = ldb.MessageElement("<GUID=%s>" % str(guid),
                                                 ldb.FLAG_MOD_REPLACE, "dsServiceName")
         ctx.local_samdb.modify(m)
 
-        if ctx.subdomain:
+        if ctx.clone_only or ctx.subdomain:
             return
 
         secrets_ldb = Ldb(ctx.paths.secrets, session_info=system_session(), lp=ctx.lp)
@@ -1077,23 +1096,26 @@ class dc_join(object):
                 ctx.full_nc_list += [ctx.domaindns_zone]
                 ctx.full_nc_list += [ctx.forestdns_zone]
 
-        if ctx.promote_existing:
-            ctx.promote_possible()
-        else:
-            ctx.cleanup_old_join()
+        if not ctx.clone_only:
+            if ctx.promote_existing:
+                ctx.promote_possible()
+            else:
+                ctx.cleanup_old_join()
 
         try:
-            ctx.join_add_objects()
+            if not ctx.clone_only:
+                ctx.join_add_objects()
             ctx.join_provision()
             ctx.join_replicate()
-            if ctx.subdomain:
+            if (not ctx.clone_only and ctx.subdomain):
                 ctx.join_add_objects2()
                 ctx.join_provision_own_domain()
                 ctx.join_setup_trusts()
             ctx.join_finalise()
         except:
             print "Join failed - cleaning up"
-            ctx.cleanup_old_join()
+            if not ctx.clone_only:
+                ctx.cleanup_old_join()
             raise
 
 
@@ -1183,6 +1205,28 @@ def join_DC(logger=None, server=None, creds=None, lp=None, site=None, netbios_na
     ctx.do_join()
     logger.info("Joined domain %s (SID %s) as a DC" % (ctx.domain_name, ctx.domsid))
 
+def join_clone(logger=None, server=None, creds=None, lp=None,
+            targetdir=None, domain=None):
+    """Join as a DC."""
+    ctx = dc_join(logger, server, creds, lp, site=None, netbios_name=None, targetdir=targetdir, domain=domain,
+                  machinepass=None, use_ntvfs=False, dns_backend="NONE", promote_existing=False, clone_only=True)
+
+    lp.set("workgroup", ctx.domain_name)
+    logger.info("workgroup is %s" % ctx.domain_name)
+
+    lp.set("realm", ctx.realm)
+    logger.info("realm is %s" % ctx.realm)
+
+    ctx.replica_flags = (drsuapi.DRSUAPI_DRS_WRIT_REP |
+                         drsuapi.DRSUAPI_DRS_INIT_SYNC |
+                         drsuapi.DRSUAPI_DRS_PER_SYNC |
+                         drsuapi.DRSUAPI_DRS_FULL_SYNC_IN_PROGRESS |
+                         drsuapi.DRSUAPI_DRS_NEVER_SYNCED)
+    ctx.domain_replica_flags = ctx.replica_flags
+
+    ctx.do_join()
+    logger.info("Cloned domain %s (SID %s)" % (ctx.domain_name, ctx.domsid))
+
 def join_subdomain(logger=None, server=None, creds=None, lp=None, site=None,
         netbios_name=None, targetdir=None, parent_domain=None, dnsdomain=None,
         netbios_domain=None, machinepass=None, adminpass=None, use_ntvfs=False,
index e8e9ec879aac1f6c016a7809fa48d42fd3ac856d..f1d4970aecbec03bedb4201601b7dc49374c8bae 100644 (file)
@@ -20,6 +20,7 @@
 
 import samba.getopt as options
 import ldb
+import logging
 
 from samba.auth import system_session
 from samba.netcmd import (
@@ -32,6 +33,7 @@ from samba.samdb import SamDB
 from samba import drs_utils, nttime2string, dsdb
 from samba.dcerpc import drsuapi, misc
 import common
+from samba.join import join_clone
 
 def drsuapi_connect(ctx):
     '''make a DRSUAPI connection to the server'''
@@ -513,6 +515,44 @@ class cmd_drs_options(Command):
             self.message("New DSA options: " + ", ".join(cur_opts))
 
 
+class cmd_drs_clone_dc_database(Command):
+    """Replicate an initial clone of domain, but DO NOT JOIN it."""
+
+    synopsis = "%prog <dnsdomain> [options]"
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "versionopts": options.VersionOptions,
+        "credopts": options.CredentialsOptions,
+    }
+
+    takes_options = [
+        Option("--server", help="DC to join", type=str),
+        Option("--targetdir", help="where to store provision", type=str),
+        Option("--quiet", help="Be quiet", action="store_true"),
+        Option("--verbose", help="Be verbose", action="store_true")
+       ]
+
+    takes_args = ["domain"]
+
+    def run(self, domain, sambaopts=None, credopts=None,
+            versionopts=None, server=None, targetdir=None,
+            quiet=False, verbose=False):
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp)
+
+        logger = self.get_logger()
+        if verbose:
+            logger.setLevel(logging.DEBUG)
+        elif quiet:
+            logger.setLevel(logging.WARNING)
+        else:
+            logger.setLevel(logging.INFO)
+
+        join_clone(logger=logger, server=server, creds=creds, lp=lp, domain=domain,
+                   targetdir=targetdir)
+
+
 class cmd_drs(SuperCommand):
     """Directory Replication Services (DRS) management."""
 
@@ -522,3 +562,4 @@ class cmd_drs(SuperCommand):
     subcommands["replicate"] = cmd_drs_replicate()
     subcommands["showrepl"] = cmd_drs_showrepl()
     subcommands["options"] = cmd_drs_options()
+    subcommands["clone-dc-database"] = cmd_drs_clone_dc_database()
index b53c4ea027ffc394374852647d932308ce1b4a17..87b69435e378bbf6026009f12428ec258bccc32c 100644 (file)
@@ -253,7 +253,7 @@ class BlackboxProcessError(Exception):
         return "Command '%s'; exit status %d; stdout: '%s'; stderr: '%s'" % (self.cmd, self.returncode,
                                                                              self.stdout, self.stderr)
 
-class BlackboxTestCase(TestCase):
+class BlackboxTestCase(TestCaseInTempDir):
     """Base test case for blackbox tests."""
 
     def _make_cmdline(self, line):
index 9b7106ff03f702dd3ec3e51223973edd4fa7468a..0bfd65cac5c94aebe7e56016dc46db1e3bfb88d4 100644 (file)
@@ -18,7 +18,8 @@
 """Blackbox tests for samba-tool drs."""
 
 import samba.tests
-
+import shutil
+import os
 
 class SambaToolDrsTests(samba.tests.BlackboxTestCase):
     """Blackbox test case for samba-tool drs."""
@@ -33,10 +34,10 @@ class SambaToolDrsTests(samba.tests.BlackboxTestCase):
         self.cmdline_creds = "-U%s/%s%%%s" % (creds.get_domain(),
                                               creds.get_username(), creds.get_password())
 
-    def _get_rootDSE(self, dc):
+    def _get_rootDSE(self, dc, ldap_only=True):
         samdb = samba.tests.connect_samdb(dc, lp=self.get_loadparm(),
                                           credentials=self.get_credentials(),
-                                          ldap_only=True)
+                                          ldap_only=ldap_only)
         return samdb.search(base="", scope=samba.tests.ldb.SCOPE_BASE)[0]
 
     def test_samba_tool_bind(self):
@@ -100,3 +101,30 @@ class SambaToolDrsTests(samba.tests.BlackboxTestCase):
                                                                           self.cmdline_creds))
         self.assertTrue("Replicate from" in out)
         self.assertTrue("was successful" in out)
+
+    def test_samba_tool_drs_clone_dc(self):
+        """Tests 'samba-tool drs clone-dc-database' command."""
+        server_rootdse = self._get_rootDSE(self.dc1)
+        server_nc_name = server_rootdse["defaultNamingContext"]
+        server_ds_name = server_rootdse["dsServiceName"]
+        server_ldap_service_name = str(server_rootdse["ldapServiceName"][0])
+        server_realm = server_ldap_service_name.split(":")[0]
+        creds = self.get_credentials()
+        out = self.check_output("samba-tool drs clone-dc-database %s --server=%s %s --targetdir=%s"
+                                % (server_realm,
+                                   self.dc1,
+                                   self.cmdline_creds,
+                                   self.tempdir))
+        ldb_rootdse = self._get_rootDSE("tdb://" + os.path.join(self.tempdir, "private", "sam.ldb"), ldap_only=False)
+        nc_name = ldb_rootdse["defaultNamingContext"]
+        ds_name = ldb_rootdse["dsServiceName"]
+        ldap_service_name = str(server_rootdse["ldapServiceName"][0])
+        self.assertEqual(nc_name, server_nc_name)
+        # The clone should pretend to be the source server
+        self.assertEqual(ds_name, server_ds_name)
+        self.assertEqual(ldap_service_name, server_ldap_service_name)
+        shutil.rmtree(os.path.join(self.tempdir, "private"))
+        shutil.rmtree(os.path.join(self.tempdir, "etc"))
+        shutil.rmtree(os.path.join(self.tempdir, "msg.lock"))
+        os.remove(os.path.join(self.tempdir, "names.tdb"))
+        shutil.rmtree(os.path.join(self.tempdir, "state"))