[PATCH] do a partial replication with drs replicate --local

Bob Campbell bobcampbell at catalyst.net.nz
Thu Feb 9 02:10:36 UTC 2017


Hi Metze -

I've changed the attached patches by adding python bindings for
dsdb_load_udv_v2, and using that in replicate --local.

Thanks,
Bob

On 08/02/17 23:41, Stefan Metzmacher wrote:
> Hi Bob,
>
> no the logic is not ok yet.
>
> Am 08.02.2017 um 02:01 schrieb Bob Campbell:
>>  
>> -
>>      source_dsa_invocation_id = misc.GUID(self.samdb.get_invocation_id())
>>      dest_dsa_invocation_id = misc.GUID(self.local_samdb.get_invocation_id())
>>      destination_dsa_guid = self.ntds_guid
>>  
>> +    # If we can't find an upToDateVector, replicate fully
>> +    hwm = drsuapi.DsReplicaHighWaterMark()
>> +    hwm.tmp_highest_usn = 0
>> +    hwm.reserved_usn = 0
>> +    hwm.highest_usn = 0
>> +
>> +    udv = None
>> +    if not full_sync:
>> +        res = self.local_samdb.search(base=NC, scope=ldb.SCOPE_BASE,
>> +                                      attrs=["replUpToDateVector", "repsFrom"])
>> +        if "repsFrom" in res[0]:
>> +            for reps_from_packed in res[0]["repsFrom"]:
>> +                reps_from_obj = ndr_unpack(drsblobs.repsFromToBlob, reps_from_packed)
>> +                if reps_from_obj.ctr.source_dsa_invocation_id == source_dsa_invocation_id:
>> +                    hwm = reps_from_obj.ctr.highwatermark
>> +
>> +        if "replUpToDateVector" in res[0]:
>> +            udv_packed = res[0]["replUpToDateVector"][0]
>> +            udv_obj = ndr_unpack(drsblobs.replUpToDateVectorBlob, udv_packed)
>> +            udv_ctr = udv_obj.ctr
>> +
>> +            udv = drsuapi.DsReplicaCursorCtrEx()
>> +            udv.version = 2
>> +            udv.reserved1 = 0
>> +            udv.reserved2 = 0
>> +            cursors = []
>> +
>> +            highest_usn = self.local_samdb.sequence_number(ldb.SEQ_NEXT)
>> +            found = False
>> +            for cursor in udv_ctr.cursors:
>> +                udv_ex_cursor = drsuapi.DsReplicaCursor()
>> +                udv_ex_cursor.source_dsa_invocation_id = cursor.source_dsa_invocation_id
>> +                udv_ex_cursor.highest_usn = cursor.highest_usn
>> +                # Update the cursor if it's ours
>> +                if cursor.source_dsa_invocation_id == source_dsa_invocation_id:
>> +                    udv_ex_cursor.highest_usn = highest_usn
>> +                    found = True
>> +                cursors.append(udv_ex_cursor)
>> +
>> +            # If we didn't find our cursor, add a new one for us
>> +            if not found:
>> +                our_udv_cursor = drsuapi.DsReplicaCursor()
>> +                our_udv_cursor.source_dsa_invocation_id = source_dsa_invocation_id
>> +                our_udv_cursor.highest_usn = highest_usn
>> +                cursors.append(our_udv_cursor)
>> +
>> +            udv.cursors = cursors
>> +            udv.count = len(cursors)
>> +
> It needs to be something like this:
>
>
>     highest_usn = self.local_samdb.sequence_number(ldb.SEQ_NEXT)
>
>     # If we can't find an upToDateVector, replicate fully
>     hwm = drsuapi.DsReplicaHighWaterMark()
>     hwm.tmp_highest_usn = 0
>     hwm.reserved_usn = 0
>     hwm.highest_usn = 0
>
>     udv = None
>     if not full_sync:
>         res = self.local_samdb.search(base=NC, scope=ldb.SCOPE_BASE,
>                                       attrs=["replUpToDateVector",
> "repsFrom"])
>         if "repsFrom" in res[0]:
>             for reps_from_packed in res[0]["repsFrom"]:
>                 reps_from_obj = ndr_unpack(drsblobs.repsFromToBlob,
> reps_from_packed)
>                 if reps_from_obj.ctr.source_dsa_invocation_id ==
> source_dsa_invocation_id:
>                     hwm = reps_from_obj.ctr.highwatermark
>
>         # The following logic should match dsdb_load_udv_v2()!
>         cursors = []
>         found = False
>
>         if "replUpToDateVector" in res[0]:
>             udv_packed = res[0]["replUpToDateVector"][0]
>             udv_obj = ndr_unpack(drsblobs.replUpToDateVectorBlob,
> udv_packed)
>             udv_ctr = udv_obj.ctr
>
>             for cursor in udv_ctr.cursors:
>                 udv_ex_cursor = drsuapi.DsReplicaCursor()
>                 udv_ex_cursor.source_dsa_invocation_id =
> cursor.source_dsa_invocation_id
>                 # Update the cursor if it's ours
>                 if cursor.source_dsa_invocation_id ==
> destinatoin_dsa_invocation_id:
>                     udv_ex_cursor.highest_usn = highest_usn
>                     found = True
>                 else:
>                     udv_ex_cursor.highest_usn = cursor.highest_usn
>                 cursors.append(udv_ex_cursor)
>
>         # If we didn't find our cursor, add a new one for us
>         if not found:
>             our_udv_cursor = drsuapi.DsReplicaCursor()
>             our_udv_cursor.source_dsa_invocation_id =
> destination_dsa_invocation_id
>             our_udv_cursor.highest_usn = highest_usn
>             cursors.append(our_udv_cursor)
>
>         todo_sort_by_guid(cursors)
>
>         udv = drsuapi.DsReplicaCursorCtrEx()
>         udv.version = 2
>         udv.reserved1 = 0
>         udv.reserved2 = 0
>         udv.cursors = cursors
>         udv.count = len(cursors)
>
> The main points are that our highest_usn belongs to our
> destination_dsa_invocation_id not to the source_dsa_invocation_id variable.
> and we need to create the udv even without "replUpToDateVector" in that
> case we just send our own cursor.
>
> And you should to sort the cursors at the end see:
> TYPESAFE_QSORT(*cursors, *count, drsuapi_DsReplicaCursor2_compare);
>
> Another approach would be to add python bindings for dsdb_load_udv_v2().
>
> metze
>

-------------- next part --------------
From a8eaadbea11dfd4fe2f3959bb905ff2ad8cadb13 Mon Sep 17 00:00:00 2001
From: Bob Campbell <bobcampbell at catalyst.net.nz>
Date: Fri, 27 Jan 2017 10:18:21 +1300
Subject: [PATCH 1/6] drs_utils: return number of replicated objects and links
 in replicate()

Signed-off-by: Bob Campbell <bobcampbell at catalyst.net.nz>
Pair-programmed-with: Andrew Bartlett <abartlet at samba.org>
---
 python/samba/drs_utils.py | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/python/samba/drs_utils.py b/python/samba/drs_utils.py
index 07fc05a..645d146 100644
--- a/python/samba/drs_utils.py
+++ b/python/samba/drs_utils.py
@@ -1,7 +1,7 @@
 # DRS utility code
 #
 # Copyright Andrew Tridgell 2010
-# Copyright Andrew Bartlett 2010
+# Copyright Andrew Bartlett 2017
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -212,6 +212,7 @@ class drs_Replicate(object):
         req8.highwatermark.reserved_usn = 0
         req8.highwatermark.highest_usn = 0
         req8.uptodateness_vector = None
+
         if replica_flags is not None:
             req8.replica_flags = replica_flags
         elif exop == drsuapi.DRSUAPI_EXOP_REPL_SECRET:
@@ -250,12 +251,25 @@ class drs_Replicate(object):
                     setattr(req5, a, getattr(req8, a))
             req = req5
 
+        num_objects = 0
+        num_links = 0
         while True:
             (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, req_level, req)
             if ctr.first_object is None and ctr.object_count != 0:
                 raise RuntimeError("DsGetNCChanges: NULL first_object with object_count=%u" % (ctr.object_count))
             self.net.replicate_chunk(self.replication_state, level, ctr,
                 schema=schema, req_level=req_level, req=req)
+
+            num_objects += ctr.object_count
+
+            # Cope with servers that do not return level 6, so do not return any links
+            try:
+                num_links += ctr.linked_attributes_count
+            except AttributeError:
+                pass
+
             if ctr.more_data == 0:
                 break
             req.highwatermark = ctr.new_highwatermark
+
+        return (num_objects, num_links)
-- 
1.9.1


From 2c2a5910273117a7404b3b0fb8b81f316fa2a6b1 Mon Sep 17 00:00:00 2001
From: Bob Campbell <bobcampbell at catalyst.net.nz>
Date: Fri, 27 Jan 2017 10:40:19 +1300
Subject: [PATCH 2/6] drs_utils: use a given highwatermark and
 uptodateness_vector in replicate()

Signed-off-by: Bob Campbell <bobcampbell at catalyst.net.nz>
Pair-programmed-with: Andrew Bartlett <abartlet at samba.org>
---
 python/samba/drs_utils.py | 17 +++++++++++------
 1 file changed, 11 insertions(+), 6 deletions(-)

diff --git a/python/samba/drs_utils.py b/python/samba/drs_utils.py
index 645d146..8c7a86b 100644
--- a/python/samba/drs_utils.py
+++ b/python/samba/drs_utils.py
@@ -197,7 +197,7 @@ class drs_Replicate(object):
 
     def replicate(self, dn, source_dsa_invocation_id, destination_dsa_guid,
                   schema=False, exop=drsuapi.DRSUAPI_EXOP_NONE, rodc=False,
-                  replica_flags=None):
+                  replica_flags=None, highwatermark=None, udv=None):
         '''replicate a single DN'''
 
         # setup for a GetNCChanges call
@@ -207,11 +207,16 @@ class drs_Replicate(object):
         req8.source_dsa_invocation_id = source_dsa_invocation_id
         req8.naming_context = drsuapi.DsReplicaObjectIdentifier()
         req8.naming_context.dn = dn
-        req8.highwatermark = drsuapi.DsReplicaHighWaterMark()
-        req8.highwatermark.tmp_highest_usn = 0
-        req8.highwatermark.reserved_usn = 0
-        req8.highwatermark.highest_usn = 0
-        req8.uptodateness_vector = None
+
+        if highwatermark is not None:
+            req8.highwatermark = highwatermark
+        else:
+            req8.highwatermark = drsuapi.DsReplicaHighWaterMark()
+            req8.highwatermark.tmp_highest_usn = 0
+            req8.highwatermark.reserved_usn = 0
+            req8.highwatermark.highest_usn = 0
+
+        req8.uptodateness_vector = udv
 
         if replica_flags is not None:
             req8.replica_flags = replica_flags
-- 
1.9.1


From d426ea9bb4736e2c3b68c94bea9a980c70838afb Mon Sep 17 00:00:00 2001
From: Bob Campbell <bobcampbell at catalyst.net.nz>
Date: Thu, 9 Feb 2017 11:22:08 +1300
Subject: [PATCH 3/6] pydsdb: Add _dsdb_load_udv_v2 to python bindings

Signed-off-by: Bob Campbell <bobcampbell at catalyst.net.nz>
---
 source4/dsdb/pydsdb.c | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 73 insertions(+)

diff --git a/source4/dsdb/pydsdb.c b/source4/dsdb/pydsdb.c
index ab1d0d2..3348d64 100644
--- a/source4/dsdb/pydsdb.c
+++ b/source4/dsdb/pydsdb.c
@@ -1244,6 +1244,78 @@ static PyObject *py_dsdb_garbage_collect_tombstones(PyObject *self, PyObject *ar
 			    num_links_removed);
 }
 
+static PyObject *py_dsdb_load_udv_v2(PyObject *self, PyObject *args)
+{
+	PyObject *py_ldb, *py_dn, *pylist;
+	struct ldb_context *samdb;
+	struct ldb_dn *dn;
+	struct drsuapi_DsReplicaCursor2 *cursors;
+	uint32_t *count;
+	struct drsuapi_DsReplicaCursor2CtrEx *cursor_ctr;
+	int ret, i;
+	TALLOC_CTX *tmp_ctx;
+
+	if (!PyArg_ParseTuple(args, "OO", &py_ldb, &py_dn)) {
+		return NULL;
+	}
+
+	PyErr_LDB_OR_RAISE(py_ldb, samdb);
+
+	tmp_ctx = talloc_new(samdb);
+	if (tmp_ctx == NULL) {
+		return PyErr_NoMemory();
+	}
+
+	if (!pyldb_Object_AsDn(tmp_ctx, py_dn, samdb, &dn)) {
+		talloc_free(tmp_ctx);
+		return NULL;
+	}
+
+	cursor_ctr = talloc_zero(tmp_ctx,
+				 struct drsuapi_DsReplicaCursor2CtrEx);
+	count = talloc_zero(tmp_ctx, uint32_t);
+	if (cursor_ctr == NULL || count == NULL) {
+		return PyErr_NoMemory();
+	}
+
+	ret = dsdb_load_udv_v2(samdb, dn, tmp_ctx, &cursors, count);
+	if (ret != LDB_SUCCESS) {
+		TALLOC_FREE(tmp_ctx);
+		PyErr_SetString(PyExc_RuntimeError,
+				"Failed to load udv from ldb");
+		return NULL;
+	}
+
+	pylist = PyList_New(*count);
+	if (pylist == NULL) {
+		TALLOC_FREE(tmp_ctx);
+		return PyErr_NoMemory();
+	}
+
+	for (i = 0; i < *count; i++) {
+		PyObject *py_cursor;
+		struct drsuapi_DsReplicaCursor2 *cursor;
+		cursor = talloc(tmp_ctx, struct drsuapi_DsReplicaCursor2);
+		if (cursor == NULL) {
+			TALLOC_FREE(tmp_ctx);
+			return PyErr_NoMemory();
+		}
+		*cursor = cursors[i];
+
+		py_cursor = py_return_ndr_struct("samba.dcerpc.drsuapi",
+						 "DsReplicaCursor2",
+						 cursor, cursor);
+		if (!py_cursor) {
+			TALLOC_FREE(tmp_ctx);
+			return PyErr_NoMemory();
+		}
+
+		PyList_SetItem(pylist, i, py_cursor);
+	}
+
+	TALLOC_FREE(tmp_ctx);
+	return pylist;
+}
 
 static PyMethodDef py_dsdb_methods[] = {
 	{ "_samdb_server_site_name", (PyCFunction)py_samdb_server_site_name,
@@ -1319,6 +1391,7 @@ static PyMethodDef py_dsdb_methods[] = {
 	{ "_dsdb_allocate_rid", (PyCFunction)py_dsdb_allocate_rid, METH_VARARGS,
 		"_dsdb_allocate_rid(samdb)"
 		" -> RID" },
+	{ "_dsdb_load_udv_v2", (PyCFunction)py_dsdb_load_udv_v2, METH_VARARGS, NULL },
 	{ NULL }
 };
 
-- 
1.9.1


From 6e6cad818cd8081c0359c06c4d59a6b1c8c04375 Mon Sep 17 00:00:00 2001
From: Bob Campbell <bobcampbell at catalyst.net.nz>
Date: Fri, 27 Jan 2017 10:40:59 +1300
Subject: [PATCH 4/6] samba-tool/drs: do partial replication when --local is
 given by default

The samba-tool drs replicate --local command would previously always do
a full replication. This changes it to only replicate changes it doesn't
have according to appropriate highwatermark if the appropriate repsFrom
attribute exists in the local database, or an uptodateness_vector if one
exists.

Signed-off-by: Bob Campbell <bobcampbell at catalyst.net.nz>
Pair-programmed-with: Andrew Bartlett <abartlet at samba.org>
---
 python/samba/netcmd/drs.py                    | 61 +++++++++++++++++++++++----
 python/samba/tests/blackbox/samba_tool_drs.py |  4 +-
 2 files changed, 54 insertions(+), 11 deletions(-)

diff --git a/python/samba/netcmd/drs.py b/python/samba/netcmd/drs.py
index 67733ac..aadfff4 100644
--- a/python/samba/netcmd/drs.py
+++ b/python/samba/netcmd/drs.py
@@ -1,6 +1,7 @@
 # implement samba_tool drs commands
 #
 # Copyright Andrew Tridgell 2010
+# Copyright Andrew Bartlett 2017
 #
 # based on C implementation by Kamen Mazdrashki <kamen.mazdrashki at postpath.com>
 #
@@ -21,6 +22,7 @@
 import samba.getopt as options
 import ldb
 import logging
+import common
 
 from samba.auth import system_session
 from samba.netcmd import (
@@ -32,8 +34,9 @@ from samba.netcmd import (
 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
+from samba.ndr import ndr_unpack
+from samba.dcerpc import drsblobs
 
 def drsuapi_connect(ctx):
     '''make a DRSUAPI connection to the server'''
@@ -238,7 +241,7 @@ class cmd_drs_kcc(Command):
 
 
 
-def drs_local_replicate(self, SOURCE_DC, NC):
+def drs_local_replicate(self, SOURCE_DC, NC, full_sync=False):
     '''replicate from a source DC to the local SAM'''
 
     self.server = SOURCE_DC
@@ -252,17 +255,51 @@ def drs_local_replicate(self, SOURCE_DC, NC):
                        credentials=self.creds, lp=self.lp)
 
     # work out the source and destination GUIDs
-    res = self.local_samdb.search(base="", scope=ldb.SCOPE_BASE, attrs=["dsServiceName"])
+    res = self.local_samdb.search(base="", scope=ldb.SCOPE_BASE,
+                                  attrs=["dsServiceName"])
     self.ntds_dn = res[0]["dsServiceName"][0]
 
-    res = self.local_samdb.search(base=self.ntds_dn, scope=ldb.SCOPE_BASE, attrs=["objectGUID"])
+    res = self.local_samdb.search(base=self.ntds_dn, scope=ldb.SCOPE_BASE,
+                                  attrs=["objectGUID"])
     self.ntds_guid = misc.GUID(self.samdb.schema_format_value("objectGUID", res[0]["objectGUID"][0]))
 
-
     source_dsa_invocation_id = misc.GUID(self.samdb.get_invocation_id())
     dest_dsa_invocation_id = misc.GUID(self.local_samdb.get_invocation_id())
     destination_dsa_guid = self.ntds_guid
 
+    # If we can't find an upToDateVector, replicate fully
+    hwm = drsuapi.DsReplicaHighWaterMark()
+    hwm.tmp_highest_usn = 0
+    hwm.reserved_usn = 0
+    hwm.highest_usn = 0
+
+    udv = None
+    if not full_sync:
+        res = self.local_samdb.search(base=NC, scope=ldb.SCOPE_BASE,
+                                      attrs=["repsFrom"])
+        if "repsFrom" in res[0]:
+            for reps_from_packed in res[0]["repsFrom"]:
+                reps_from_obj = ndr_unpack(drsblobs.repsFromToBlob, reps_from_packed)
+                if reps_from_obj.ctr.source_dsa_invocation_id == source_dsa_invocation_id:
+                    hwm = reps_from_obj.ctr.highwatermark
+
+        udv = drsuapi.DsReplicaCursorCtrEx()
+        udv.version = 2
+        udv.reserved1 = 0
+        udv.reserved2 = 0
+
+        cursors_v1 = []
+        cursors_v2 = dsdb._dsdb_load_udv_v2(self.local_samdb,
+                                            self.local_samdb.get_default_basedn())
+        for cursor_v2 in cursors_v2:
+            cursor_v1 = drsuapi.DsReplicaCursor()
+            cursor_v1.source_dsa_invocation_id = cursor_v2.source_dsa_invocation_id
+            cursor_v1.highest_usn = cursor_v2.highest_usn
+            cursors_v1.append(cursor_v1)
+
+        udv.cursors = cursors_v1
+        udv.count = len(cursors_v1)
+
     self.samdb.transaction_start()
     repl = drs_utils.drs_Replicate("ncacn_ip_tcp:%s[seal]" % self.server, self.lp,
                                    self.creds, self.local_samdb, dest_dsa_invocation_id)
@@ -271,13 +308,19 @@ def drs_local_replicate(self, SOURCE_DC, NC):
     # with the admin pw does not sync passwords
     rodc = self.local_samdb.am_rodc()
     try:
-        repl.replicate(NC, source_dsa_invocation_id, destination_dsa_guid, rodc=rodc)
+        (num_objects, num_links) = repl.replicate(NC,
+                                                  source_dsa_invocation_id, destination_dsa_guid,
+                                                  rodc=rodc, highwatermark=hwm, udv=udv)
     except Exception, e:
         raise CommandError("Error replicating DN %s" % NC, e)
     self.samdb.transaction_commit()
 
-    self.message("Replicate from %s to %s was successful." % (SOURCE_DC, self.local_samdb.url))
-
+    if full_sync:
+        self.message("Full Replication of all %d objects and %d links from %s to %s was successful."
+                     % (num_objects, num_links, SOURCE_DC, self.local_samdb.url))
+    else:
+        self.message("Incremental replication of %d objects and %d links from %s to %s was successful."
+                     % (num_objects, num_links, SOURCE_DC, self.local_samdb.url))
 
 
 class cmd_drs_replicate(Command):
@@ -314,7 +357,7 @@ class cmd_drs_replicate(Command):
         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
 
         if local:
-            drs_local_replicate(self, SOURCE_DC, NC)
+            drs_local_replicate(self, SOURCE_DC, NC, full_sync=full_sync)
             return
 
         if local_online:
diff --git a/python/samba/tests/blackbox/samba_tool_drs.py b/python/samba/tests/blackbox/samba_tool_drs.py
index 90921f4..df0e3d7 100644
--- a/python/samba/tests/blackbox/samba_tool_drs.py
+++ b/python/samba/tests/blackbox/samba_tool_drs.py
@@ -145,7 +145,7 @@ class SambaToolDrsTests(samba.tests.BlackboxTestCase):
         out = self.check_output("samba-tool drs replicate -P --local %s %s %s" % (self.dc1,
                                                                                   self.dc2,
                                                                                   nc_name))
-        self.assertTrue("Replicate from" in out)
+        self.assertTrue("Incremental" in out)
         self.assertTrue("was successful" in out)
 
     def test_samba_tool_replicate_local(self):
@@ -157,7 +157,7 @@ class SambaToolDrsTests(samba.tests.BlackboxTestCase):
                                                                                   self.dc2,
                                                                                   nc_name,
                                                                                   self.cmdline_creds))
-        self.assertTrue("Replicate from" in out)
+        self.assertTrue("Incremental" in out)
         self.assertTrue("was successful" in out)
 
     def test_samba_tool_replicate_machine_creds_P(self):
-- 
1.9.1


From e1e82880438a8cbf17e29569abdb1f850b8e45e8 Mon Sep 17 00:00:00 2001
From: Bob Campbell <bobcampbell at catalyst.net.nz>
Date: Fri, 27 Jan 2017 14:46:36 +1300
Subject: [PATCH 5/6] python/tests: move samba_tool_drs test to proper place

Signed-off-by: Bob Campbell <bobcampbell at catalyst.net.nz>
---
 python/samba/tests/blackbox/samba_tool_drs.py | 344 -------------------------
 source4/selftest/tests.py                     |   4 +-
 source4/torture/drs/python/samba_tool_drs.py  | 345 ++++++++++++++++++++++++++
 3 files changed, 348 insertions(+), 345 deletions(-)
 delete mode 100644 python/samba/tests/blackbox/samba_tool_drs.py
 create mode 100644 source4/torture/drs/python/samba_tool_drs.py

diff --git a/python/samba/tests/blackbox/samba_tool_drs.py b/python/samba/tests/blackbox/samba_tool_drs.py
deleted file mode 100644
index df0e3d7..0000000
--- a/python/samba/tests/blackbox/samba_tool_drs.py
+++ /dev/null
@@ -1,344 +0,0 @@
-# Blackbox tests for "samba-tool drs" command
-# Copyright (C) Kamen Mazdrashki <kamenim at samba.org> 2011
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-
-"""Blackbox tests for samba-tool drs."""
-
-import samba.tests
-import shutil
-import os
-import ldb
-
-class SambaToolDrsTests(samba.tests.BlackboxTestCase):
-    """Blackbox test case for samba-tool drs."""
-
-    def setUp(self):
-        super(SambaToolDrsTests, self).setUp()
-
-        self.dc1 = samba.tests.env_get_var_value("DC1")
-        self.dc2 = samba.tests.env_get_var_value("DC2")
-
-        creds = self.get_credentials()
-        self.cmdline_creds = "-U%s/%s%%%s" % (creds.get_domain(),
-                                              creds.get_username(), creds.get_password())
-
-    def _get_rootDSE(self, dc, ldap_only=True):
-        samdb = samba.tests.connect_samdb(dc, lp=self.get_loadparm(),
-                                          credentials=self.get_credentials(),
-                                          ldap_only=ldap_only)
-        return samdb.search(base="", scope=samba.tests.ldb.SCOPE_BASE)[0]
-
-    def test_samba_tool_bind(self):
-        """Tests 'samba-tool drs bind' command."""
-
-        # Output should be like:
-        #      Extensions supported:
-        #        <list-of-supported-extensions>
-        #      Site GUID: <GUID>
-        #      Repl epoch: 0
-        out = self.check_output("samba-tool drs bind %s %s" % (self.dc1,
-                                                               self.cmdline_creds))
-        self.assertTrue("Site GUID:" in out)
-        self.assertTrue("Repl epoch:" in out)
-
-    def test_samba_tool_kcc(self):
-        """Tests 'samba-tool drs kcc' command."""
-
-        # Output should be like 'Consistency check on <DC> successful.'
-        out = self.check_output("samba-tool drs kcc %s %s" % (self.dc1,
-                                                              self.cmdline_creds))
-        self.assertTrue("Consistency check on" in out)
-        self.assertTrue("successful" in out)
-
-    def test_samba_tool_showrepl(self):
-        """Tests 'samba-tool drs showrepl' command.
-        """
-        # Output should be like:
-        #      <site-name>/<domain-name>
-        #      DSA Options: <hex-options>
-        #      DSA object GUID: <DSA-object-GUID>
-        #      DSA invocationId: <DSA-invocationId>
-        #      <Inbound-connections-list>
-        #      <Outbound-connections-list>
-        #      <KCC-objects>
-        #      ...
-        #   TODO: Perhaps we should check at least for
-        #         DSA's objectGUDI and invocationId
-        out = self.check_output("samba-tool drs showrepl %s %s" % (self.dc1,
-                                                                   self.cmdline_creds))
-        self.assertTrue("DSA Options:" in out)
-        self.assertTrue("DSA object GUID:" in out)
-        self.assertTrue("DSA invocationId:" in out)
-
-    def test_samba_tool_options(self):
-        """Tests 'samba-tool drs options' command
-        """
-        # Output should be like 'Current DSA options: IS_GC <OTHER_FLAGS>'
-        out = self.check_output("samba-tool drs options %s %s" % (self.dc1,
-                                                                  self.cmdline_creds))
-        self.assertTrue("Current DSA options:" in out)
-
-    def test_samba_tool_replicate(self):
-        """Tests 'samba-tool drs replicate' command."""
-
-        # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was successful.'
-        nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"]
-        out = self.check_output("samba-tool drs replicate %s %s %s %s" % (self.dc1,
-                                                                          self.dc2,
-                                                                          nc_name,
-                                                                          self.cmdline_creds))
-        self.assertTrue("Replicate from" in out)
-        self.assertTrue("was successful" in out)
-
-    def test_samba_tool_replicate_async(self):
-        """Tests 'samba-tool drs replicate --async-op' command."""
-
-        # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was started.'
-        nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"]
-        out = self.check_output("samba-tool drs replicate --async-op %s %s %s %s" % (self.dc1,
-                                                                          self.dc2,
-                                                                          nc_name,
-                                                                          self.cmdline_creds))
-        self.assertTrue("Replicate from" in out)
-        self.assertTrue("was started" in out)
-
-    def test_samba_tool_replicate_local_online(self):
-        """Tests 'samba-tool drs replicate --local-online' command."""
-
-        # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was successful.'
-        nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"]
-        out = self.check_output("samba-tool drs replicate --local-online %s %s %s" % (self.dc1,
-                                                                                      self.dc2,
-                                                                                      nc_name))
-        self.assertTrue("Replicate from" in out)
-        self.assertTrue("was successful" in out)
-
-    def test_samba_tool_replicate_local_online_async(self):
-        """Tests 'samba-tool drs replicate --local-online --async-op' command."""
-
-        # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was started.'
-        nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"]
-        out = self.check_output("samba-tool drs replicate --local-online --async-op %s %s %s" % (self.dc1,
-                                                                                      self.dc2,
-                                                                                      nc_name))
-        self.assertTrue("Replicate from" in out)
-        self.assertTrue("was started" in out)
-
-    def test_samba_tool_replicate_local_machine_creds(self):
-        """Tests 'samba-tool drs replicate --local -P' command (uses machine creds)."""
-
-        # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was successful.'
-        nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"]
-        out = self.check_output("samba-tool drs replicate -P --local %s %s %s" % (self.dc1,
-                                                                                  self.dc2,
-                                                                                  nc_name))
-        self.assertTrue("Incremental" in out)
-        self.assertTrue("was successful" in out)
-
-    def test_samba_tool_replicate_local(self):
-        """Tests 'samba-tool drs replicate --local' command (uses machine creds)."""
-
-        # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was successful.'
-        nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"]
-        out = self.check_output("samba-tool drs replicate --local %s %s %s %s" % (self.dc1,
-                                                                                  self.dc2,
-                                                                                  nc_name,
-                                                                                  self.cmdline_creds))
-        self.assertTrue("Incremental" in out)
-        self.assertTrue("was successful" in out)
-
-    def test_samba_tool_replicate_machine_creds_P(self):
-        """Tests 'samba-tool drs replicate -P' command with machine creds."""
-
-        # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was successful.'
-        nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"]
-        out = self.check_output("samba-tool drs replicate -P %s %s %s" % (self.dc1,
-                                                                          self.dc2,
-                                                                          nc_name))
-        self.assertTrue("Replicate from" in out)
-        self.assertTrue("was successful" in out)
-
-    def test_samba_tool_replicate_machine_creds(self):
-        """Tests 'samba-tool drs replicate' command with implicit machine creds."""
-
-        # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was successful.'
-        nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"]
-        out = self.check_output("samba-tool drs replicate %s %s %s" % (self.dc1,
-                                                                       self.dc2,
-                                                                       nc_name))
-        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)
-
-        samdb = samba.tests.connect_samdb("tdb://" + os.path.join(self.tempdir, "private", "sam.ldb"),
-                                          ldap_only=False, lp=self.get_loadparm())
-        def get_krbtgt_pw():
-            krbtgt_pw = samdb.searchone("unicodePwd", "cn=krbtgt,CN=users,%s" % nc_name)
-        self.assertRaises(KeyError, get_krbtgt_pw)
-
-        server_dn = samdb.searchone("serverReferenceBL", "cn=%s,ou=domain controllers,%s" % (self.dc2, server_nc_name))
-        ntds_guid = samdb.searchone("objectGUID", "cn=ntds settings,%s" % server_dn)
-
-        res = samdb.search(base=str(server_nc_name),
-                           expression="(&(objectclass=user)(cn=dns-%s))" % (self.dc2),
-                           attrs=[], scope=ldb.SCOPE_SUBTREE)
-        if len(res) == 1:
-            dns_obj = res[0]
-        else:
-            dns_obj = None
-
-        # While we have this cloned, try demoting the other server on the clone, by GUID
-        out = self.check_output("samba-tool domain demote --remove-other-dead-server=%s -H %s/private/sam.ldb"
-                                % (ntds_guid,
-                                   self.tempdir))
-
-        # Check some of the objects that should have been removed
-        def check_machine_obj():
-            samdb.searchone("CN", "cn=%s,ou=domain controllers,%s" % (self.dc2, server_nc_name))
-        self.assertRaises(ldb.LdbError, check_machine_obj)
-
-        def check_server_obj():
-            samdb.searchone("CN", server_dn)
-        self.assertRaises(ldb.LdbError, check_server_obj)
-
-        def check_ntds_guid():
-            samdb.searchone("CN", "<GUID=%s>" % ntds_guid)
-        self.assertRaises(ldb.LdbError, check_ntds_guid)
-
-        if dns_obj is not None:
-            # Check some of the objects that should have been removed
-            def check_dns_account_obj():
-                samdb.search(base=dns_obj.dn, scope=ldb.SCOPE_BASE,
-                             attrs=[])
-            self.assertRaises(ldb.LdbError, check_dns_account_obj)
-
-        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"))
-
-    def test_samba_tool_drs_clone_dc_secrets(self):
-        """Tests 'samba-tool drs clone-dc-database --include-secrets' 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 --include-secrets"
-                                % (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"]
-        config_nc_name = ldb_rootdse["configurationNamingContext"]
-        ds_name = ldb_rootdse["dsServiceName"]
-        ldap_service_name = str(server_rootdse["ldapServiceName"][0])
-
-        samdb = samba.tests.connect_samdb("tdb://" + os.path.join(self.tempdir, "private", "sam.ldb"),
-                                          ldap_only=False, lp=self.get_loadparm())
-        krbtgt_pw = samdb.searchone("unicodePwd", "cn=krbtgt,CN=users,%s" % nc_name)
-        self.assertIsNotNone(krbtgt_pw)
-
-        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)
-
-        server_dn = samdb.searchone("serverReferenceBL", "cn=%s,ou=domain controllers,%s" % (self.dc2, server_nc_name))
-        ntds_guid = samdb.searchone("objectGUID", "cn=ntds settings,%s" % server_dn)
-
-        res = samdb.search(base=str(server_nc_name),
-                           expression="(&(objectclass=user)(cn=dns-%s))" % (self.dc2),
-                           attrs=[], scope=ldb.SCOPE_SUBTREE)
-        if len(res) == 1:
-            dns_obj = res[0]
-        else:
-            dns_obj = None
-
-        def demote_self():
-            # While we have this cloned, try demoting the other server on the clone
-            out = self.check_output("samba-tool domain demote --remove-other-dead-server=%s -H %s/private/sam.ldb"
-                                % (self.dc1,
-                                   self.tempdir))
-        self.assertRaises(samba.tests.BlackboxProcessError, demote_self)
-
-        # While we have this cloned, try demoting the other server on the clone
-        out = self.check_output("samba-tool domain demote --remove-other-dead-server=%s -H %s/private/sam.ldb"
-                                % (self.dc2,
-                                   self.tempdir))
-
-        # Check some of the objects that should have been removed
-        def check_machine_obj():
-            samdb.searchone("CN", "cn=%s,ou=domain controllers,%s" % (self.dc2, server_nc_name))
-        self.assertRaises(ldb.LdbError, check_machine_obj)
-
-        def check_server_obj():
-            samdb.searchone("CN", server_dn)
-        self.assertRaises(ldb.LdbError, check_server_obj)
-
-        def check_ntds_guid():
-            samdb.searchone("CN", "<GUID=%s>" % ntds_guid)
-        self.assertRaises(ldb.LdbError, check_ntds_guid)
-
-        if dns_obj is not None:
-            # Check some of the objects that should have been removed
-            def check_dns_account_obj():
-                samdb.search(base=dns_obj.dn, scope=ldb.SCOPE_BASE,
-                             attrs=[])
-            self.assertRaises(ldb.LdbError, check_dns_account_obj)
-
-        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"))
-
-    def test_samba_tool_drs_clone_dc_secrets_without_targetdir(self):
-        """Tests 'samba-tool drs clone-dc-database' command without --targetdir."""
-        server_rootdse = self._get_rootDSE(self.dc1)
-        server_ldap_service_name = str(server_rootdse["ldapServiceName"][0])
-        server_realm = server_ldap_service_name.split(":")[0]
-        creds = self.get_credentials()
-        def attempt_clone():
-            out = self.check_output("samba-tool drs clone-dc-database %s --server=%s %s"
-                                    % (server_realm,
-                                       self.dc1,
-                                       self.cmdline_creds))
-        self.assertRaises(samba.tests.BlackboxProcessError, attempt_clone)
diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
index ec07106..e38ca5f 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -666,7 +666,9 @@ planoldpythontestsuite(env, "ridalloc_exop",
                        extra_args=['-U$DOMAIN/$DC_USERNAME%$DC_PASSWORD'])
 
 for env in ['vampire_dc', 'promoted_dc']:
-    planoldpythontestsuite("%s:local" % env, "samba.tests.blackbox.samba_tool_drs",
+    planoldpythontestsuite("%s:local" % env, "samba_tool_drs",
+                           extra_path=[os.path.join(samba4srcdir, 'torture/drs/python')],
+                           name="samba4.drs.samba_tool_drs.python(%s)" % env,
                            environ={'DC1': '$DC_SERVER', 'DC2': '$%s_SERVER' % env.upper()},
                            extra_args=['-U$DOMAIN/$DC_USERNAME%$DC_PASSWORD'])
     planoldpythontestsuite("%s:local" % env, "replica_sync",
diff --git a/source4/torture/drs/python/samba_tool_drs.py b/source4/torture/drs/python/samba_tool_drs.py
new file mode 100644
index 0000000..ccdba1d
--- /dev/null
+++ b/source4/torture/drs/python/samba_tool_drs.py
@@ -0,0 +1,345 @@
+# Blackbox tests for "samba-tool drs" command
+# Copyright (C) Kamen Mazdrashki <kamenim at samba.org> 2011
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+"""Blackbox tests for samba-tool drs."""
+
+import samba.tests
+import shutil
+import os
+import ldb
+import drs_base
+
+class SambaToolDrsTests(drs_base.DrsBaseTestCase):
+    """Blackbox test case for samba-tool drs."""
+
+    def setUp(self):
+        super(SambaToolDrsTests, self).setUp()
+
+        self.dc1 = samba.tests.env_get_var_value("DC1")
+        self.dc2 = samba.tests.env_get_var_value("DC2")
+
+        creds = self.get_credentials()
+        self.cmdline_creds = "-U%s/%s%%%s" % (creds.get_domain(),
+                                              creds.get_username(), creds.get_password())
+
+    def _get_rootDSE(self, dc, ldap_only=True):
+        samdb = samba.tests.connect_samdb(dc, lp=self.get_loadparm(),
+                                          credentials=self.get_credentials(),
+                                          ldap_only=ldap_only)
+        return samdb.search(base="", scope=samba.tests.ldb.SCOPE_BASE)[0]
+
+    def test_samba_tool_bind(self):
+        """Tests 'samba-tool drs bind' command."""
+
+        # Output should be like:
+        #      Extensions supported:
+        #        <list-of-supported-extensions>
+        #      Site GUID: <GUID>
+        #      Repl epoch: 0
+        out = self.check_output("samba-tool drs bind %s %s" % (self.dc1,
+                                                               self.cmdline_creds))
+        self.assertTrue("Site GUID:" in out)
+        self.assertTrue("Repl epoch:" in out)
+
+    def test_samba_tool_kcc(self):
+        """Tests 'samba-tool drs kcc' command."""
+
+        # Output should be like 'Consistency check on <DC> successful.'
+        out = self.check_output("samba-tool drs kcc %s %s" % (self.dc1,
+                                                              self.cmdline_creds))
+        self.assertTrue("Consistency check on" in out)
+        self.assertTrue("successful" in out)
+
+    def test_samba_tool_showrepl(self):
+        """Tests 'samba-tool drs showrepl' command.
+        """
+        # Output should be like:
+        #      <site-name>/<domain-name>
+        #      DSA Options: <hex-options>
+        #      DSA object GUID: <DSA-object-GUID>
+        #      DSA invocationId: <DSA-invocationId>
+        #      <Inbound-connections-list>
+        #      <Outbound-connections-list>
+        #      <KCC-objects>
+        #      ...
+        #   TODO: Perhaps we should check at least for
+        #         DSA's objectGUDI and invocationId
+        out = self.check_output("samba-tool drs showrepl %s %s" % (self.dc1,
+                                                                   self.cmdline_creds))
+        self.assertTrue("DSA Options:" in out)
+        self.assertTrue("DSA object GUID:" in out)
+        self.assertTrue("DSA invocationId:" in out)
+
+    def test_samba_tool_options(self):
+        """Tests 'samba-tool drs options' command
+        """
+        # Output should be like 'Current DSA options: IS_GC <OTHER_FLAGS>'
+        out = self.check_output("samba-tool drs options %s %s" % (self.dc1,
+                                                                  self.cmdline_creds))
+        self.assertTrue("Current DSA options:" in out)
+
+    def test_samba_tool_replicate(self):
+        """Tests 'samba-tool drs replicate' command."""
+
+        # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was successful.'
+        nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"]
+        out = self.check_output("samba-tool drs replicate %s %s %s %s" % (self.dc1,
+                                                                          self.dc2,
+                                                                          nc_name,
+                                                                          self.cmdline_creds))
+        self.assertTrue("Replicate from" in out)
+        self.assertTrue("was successful" in out)
+
+    def test_samba_tool_replicate_async(self):
+        """Tests 'samba-tool drs replicate --async-op' command."""
+
+        # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was started.'
+        nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"]
+        out = self.check_output("samba-tool drs replicate --async-op %s %s %s %s" % (self.dc1,
+                                                                          self.dc2,
+                                                                          nc_name,
+                                                                          self.cmdline_creds))
+        self.assertTrue("Replicate from" in out)
+        self.assertTrue("was started" in out)
+
+    def test_samba_tool_replicate_local_online(self):
+        """Tests 'samba-tool drs replicate --local-online' command."""
+
+        # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was successful.'
+        nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"]
+        out = self.check_output("samba-tool drs replicate --local-online %s %s %s" % (self.dc1,
+                                                                                      self.dc2,
+                                                                                      nc_name))
+        self.assertTrue("Replicate from" in out)
+        self.assertTrue("was successful" in out)
+
+    def test_samba_tool_replicate_local_online_async(self):
+        """Tests 'samba-tool drs replicate --local-online --async-op' command."""
+
+        # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was started.'
+        nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"]
+        out = self.check_output("samba-tool drs replicate --local-online --async-op %s %s %s" % (self.dc1,
+                                                                                      self.dc2,
+                                                                                      nc_name))
+        self.assertTrue("Replicate from" in out)
+        self.assertTrue("was started" in out)
+
+    def test_samba_tool_replicate_local_machine_creds(self):
+        """Tests 'samba-tool drs replicate --local -P' command (uses machine creds)."""
+
+        # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was successful.'
+        nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"]
+        out = self.check_output("samba-tool drs replicate -P --local %s %s %s" % (self.dc1,
+                                                                                  self.dc2,
+                                                                                  nc_name))
+        self.assertTrue("Incremental" in out)
+        self.assertTrue("was successful" in out)
+
+    def test_samba_tool_replicate_local(self):
+        """Tests 'samba-tool drs replicate --local' command (uses machine creds)."""
+
+        # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was successful.'
+        nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"]
+        out = self.check_output("samba-tool drs replicate --local %s %s %s %s" % (self.dc1,
+                                                                                  self.dc2,
+                                                                                  nc_name,
+                                                                                  self.cmdline_creds))
+        self.assertTrue("Incremental" in out)
+        self.assertTrue("was successful" in out)
+
+    def test_samba_tool_replicate_machine_creds_P(self):
+        """Tests 'samba-tool drs replicate -P' command with machine creds."""
+
+        # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was successful.'
+        nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"]
+        out = self.check_output("samba-tool drs replicate -P %s %s %s" % (self.dc1,
+                                                                          self.dc2,
+                                                                          nc_name))
+        self.assertTrue("Replicate from" in out)
+        self.assertTrue("was successful" in out)
+
+    def test_samba_tool_replicate_machine_creds(self):
+        """Tests 'samba-tool drs replicate' command with implicit machine creds."""
+
+        # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was successful.'
+        nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"]
+        out = self.check_output("samba-tool drs replicate %s %s %s" % (self.dc1,
+                                                                       self.dc2,
+                                                                       nc_name))
+        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)
+
+        samdb = samba.tests.connect_samdb("tdb://" + os.path.join(self.tempdir, "private", "sam.ldb"),
+                                          ldap_only=False, lp=self.get_loadparm())
+        def get_krbtgt_pw():
+            krbtgt_pw = samdb.searchone("unicodePwd", "cn=krbtgt,CN=users,%s" % nc_name)
+        self.assertRaises(KeyError, get_krbtgt_pw)
+
+        server_dn = samdb.searchone("serverReferenceBL", "cn=%s,ou=domain controllers,%s" % (self.dc2, server_nc_name))
+        ntds_guid = samdb.searchone("objectGUID", "cn=ntds settings,%s" % server_dn)
+
+        res = samdb.search(base=str(server_nc_name),
+                           expression="(&(objectclass=user)(cn=dns-%s))" % (self.dc2),
+                           attrs=[], scope=ldb.SCOPE_SUBTREE)
+        if len(res) == 1:
+            dns_obj = res[0]
+        else:
+            dns_obj = None
+
+        # While we have this cloned, try demoting the other server on the clone, by GUID
+        out = self.check_output("samba-tool domain demote --remove-other-dead-server=%s -H %s/private/sam.ldb"
+                                % (ntds_guid,
+                                   self.tempdir))
+
+        # Check some of the objects that should have been removed
+        def check_machine_obj():
+            samdb.searchone("CN", "cn=%s,ou=domain controllers,%s" % (self.dc2, server_nc_name))
+        self.assertRaises(ldb.LdbError, check_machine_obj)
+
+        def check_server_obj():
+            samdb.searchone("CN", server_dn)
+        self.assertRaises(ldb.LdbError, check_server_obj)
+
+        def check_ntds_guid():
+            samdb.searchone("CN", "<GUID=%s>" % ntds_guid)
+        self.assertRaises(ldb.LdbError, check_ntds_guid)
+
+        if dns_obj is not None:
+            # Check some of the objects that should have been removed
+            def check_dns_account_obj():
+                samdb.search(base=dns_obj.dn, scope=ldb.SCOPE_BASE,
+                             attrs=[])
+            self.assertRaises(ldb.LdbError, check_dns_account_obj)
+
+        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"))
+
+    def test_samba_tool_drs_clone_dc_secrets(self):
+        """Tests 'samba-tool drs clone-dc-database --include-secrets' 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 --include-secrets"
+                                % (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"]
+        config_nc_name = ldb_rootdse["configurationNamingContext"]
+        ds_name = ldb_rootdse["dsServiceName"]
+        ldap_service_name = str(server_rootdse["ldapServiceName"][0])
+
+        samdb = samba.tests.connect_samdb("tdb://" + os.path.join(self.tempdir, "private", "sam.ldb"),
+                                          ldap_only=False, lp=self.get_loadparm())
+        krbtgt_pw = samdb.searchone("unicodePwd", "cn=krbtgt,CN=users,%s" % nc_name)
+        self.assertIsNotNone(krbtgt_pw)
+
+        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)
+
+        server_dn = samdb.searchone("serverReferenceBL", "cn=%s,ou=domain controllers,%s" % (self.dc2, server_nc_name))
+        ntds_guid = samdb.searchone("objectGUID", "cn=ntds settings,%s" % server_dn)
+
+        res = samdb.search(base=str(server_nc_name),
+                           expression="(&(objectclass=user)(cn=dns-%s))" % (self.dc2),
+                           attrs=[], scope=ldb.SCOPE_SUBTREE)
+        if len(res) == 1:
+            dns_obj = res[0]
+        else:
+            dns_obj = None
+
+        def demote_self():
+            # While we have this cloned, try demoting the other server on the clone
+            out = self.check_output("samba-tool domain demote --remove-other-dead-server=%s -H %s/private/sam.ldb"
+                                % (self.dc1,
+                                   self.tempdir))
+        self.assertRaises(samba.tests.BlackboxProcessError, demote_self)
+
+        # While we have this cloned, try demoting the other server on the clone
+        out = self.check_output("samba-tool domain demote --remove-other-dead-server=%s -H %s/private/sam.ldb"
+                                % (self.dc2,
+                                   self.tempdir))
+
+        # Check some of the objects that should have been removed
+        def check_machine_obj():
+            samdb.searchone("CN", "cn=%s,ou=domain controllers,%s" % (self.dc2, server_nc_name))
+        self.assertRaises(ldb.LdbError, check_machine_obj)
+
+        def check_server_obj():
+            samdb.searchone("CN", server_dn)
+        self.assertRaises(ldb.LdbError, check_server_obj)
+
+        def check_ntds_guid():
+            samdb.searchone("CN", "<GUID=%s>" % ntds_guid)
+        self.assertRaises(ldb.LdbError, check_ntds_guid)
+
+        if dns_obj is not None:
+            # Check some of the objects that should have been removed
+            def check_dns_account_obj():
+                samdb.search(base=dns_obj.dn, scope=ldb.SCOPE_BASE,
+                             attrs=[])
+            self.assertRaises(ldb.LdbError, check_dns_account_obj)
+
+        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"))
+
+    def test_samba_tool_drs_clone_dc_secrets_without_targetdir(self):
+        """Tests 'samba-tool drs clone-dc-database' command without --targetdir."""
+        server_rootdse = self._get_rootDSE(self.dc1)
+        server_ldap_service_name = str(server_rootdse["ldapServiceName"][0])
+        server_realm = server_ldap_service_name.split(":")[0]
+        creds = self.get_credentials()
+        def attempt_clone():
+            out = self.check_output("samba-tool drs clone-dc-database %s --server=%s %s"
+                                    % (server_realm,
+                                       self.dc1,
+                                       self.cmdline_creds))
+        self.assertRaises(samba.tests.BlackboxProcessError, attempt_clone)
-- 
1.9.1


From 3cb29ffab7ed98e0c0c94f545ecfbdf0f32827a5 Mon Sep 17 00:00:00 2001
From: Bob Campbell <bobcampbell at catalyst.net.nz>
Date: Fri, 27 Jan 2017 15:22:27 +1300
Subject: [PATCH 6/6] python/tests: improve samba-tool replicate --local test

It now makes sure that we only replicate incremental changes.

Signed-off-by: Bob Campbell <bobcampbell at catalyst.net.nz>
---
 source4/torture/drs/python/samba_tool_drs.py | 125 +++++++++++++++++++++++----
 1 file changed, 108 insertions(+), 17 deletions(-)

diff --git a/source4/torture/drs/python/samba_tool_drs.py b/source4/torture/drs/python/samba_tool_drs.py
index ccdba1d..477529d 100644
--- a/source4/torture/drs/python/samba_tool_drs.py
+++ b/source4/torture/drs/python/samba_tool_drs.py
@@ -1,5 +1,6 @@
 # Blackbox tests for "samba-tool drs" command
 # Copyright (C) Kamen Mazdrashki <kamenim at samba.org> 2011
+# Copyright (C) Andrew Bartlett <abartlet at samba.org> 2017
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -36,6 +37,21 @@ class SambaToolDrsTests(drs_base.DrsBaseTestCase):
         self.cmdline_creds = "-U%s/%s%%%s" % (creds.get_domain(),
                                               creds.get_username(), creds.get_password())
 
+    def tearDown(self):
+        self._enable_inbound_repl(self.dnsname_dc1)
+        self._enable_inbound_repl(self.dnsname_dc2)
+
+        try:
+            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"))
+        except Exception:
+            pass
+
+        super(SambaToolDrsTests, self).tearDown()
+
     def _get_rootDSE(self, dc, ldap_only=True):
         samdb = samba.tests.connect_samdb(dc, lp=self.get_loadparm(),
                                           credentials=self.get_credentials(),
@@ -154,12 +170,99 @@ class SambaToolDrsTests(drs_base.DrsBaseTestCase):
 
         # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was successful.'
         nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"]
-        out = self.check_output("samba-tool drs replicate --local %s %s %s %s" % (self.dc1,
-                                                                                  self.dc2,
-                                                                                  nc_name,
-                                                                                  self.cmdline_creds))
-        self.assertTrue("Incremental" in out)
+
+        def get_num_obj_links(output):
+            num_objs = None
+            num_links = None
+            for word in output.split(" "):
+                try:
+                    int(word)
+                    if num_objs is None:
+                        num_objs = int(word)
+                    elif num_links is None:
+                        num_links = int(word)
+                except ValueError:
+                    pass
+
+            return (num_objs, num_links)
+
+        out = self.check_output("samba-tool drs replicate --local --full-sync %s %s %s %s"
+                                % (self.dc1, self.dc2, nc_name, self.cmdline_creds))
+        self.assertTrue("was successful" in out)
+        self.assertTrue("Full" in out)
+
+        (first_obj, _) = get_num_obj_links(out)
+
+        out = self.check_output("samba-tool drs replicate --local %s %s %s %s"
+                                % (self.dc1, self.dc2, nc_name, self.cmdline_creds))
         self.assertTrue("was successful" in out)
+        self.assertTrue("Incremental" in out)
+
+        (second_obj, _) = get_num_obj_links(out)
+
+        self.assertTrue(first_obj > second_obj)
+
+        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()
+
+        # We have to give it a different netbiosname every time
+        # it runs, otherwise the collision causes strange issues
+        # to happen. This should be different on different environments.
+        netbiosname = "test" + self.dc2
+        if len(netbiosname) > 15:
+            netbiosname = netbiosname[:15]
+
+        out = self.check_output("samba-tool domain join %s dc --server=%s %s --targetdir=%s --option=netbiosname=%s"
+                                % (server_realm, self.dc1, self.cmdline_creds, self.tempdir, netbiosname))
+
+        new_dc_config_file = "%s/etc/smb.conf" % self.tempdir
+
+        self.check_output("samba-tool drs replicate --local %s %s %s %s -s %s"
+                          % ("invalid", self.dc1, nc_name,
+                             self.cmdline_creds, new_dc_config_file))
+
+        self._disable_inbound_repl(self.dnsname_dc1)
+        self._disable_inbound_repl(self.dnsname_dc2)
+
+        self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2)
+
+        # add an object with link on dc1
+        group_name = "group-repl-local-%s" % self.dc2
+        user_name = "user-repl-local-%s" % self.dc2
+
+        self.check_output("samba-tool group add %s %s -H ldap://%s"
+                          % (group_name, self.cmdline_creds, self.dc1))
+        self.check_output("samba-tool user add %s %s --random-password -H ldap://%s"
+                          % (user_name, self.cmdline_creds, self.dc1))
+        self.check_output("samba-tool group addmembers %s %s %s -H ldap://%s"
+                          % (group_name, user_name, self.cmdline_creds, self.dc1))
+
+        self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1)
+
+        # pull that change with --local into local db from dc1: should send link and some objects
+        out = self.check_output("samba-tool drs replicate --local %s %s %s %s -s %s"
+                                % ("invalid", self.dc1, nc_name,
+                                   self.cmdline_creds, new_dc_config_file))
+
+        (obj_1, link_1) = get_num_obj_links(out)
+
+        self.assertEqual(obj_1, 2)
+        self.assertEqual(link_1, 1)
+
+        # pull that change with --local into local db from dc2: shouldn't send link or object
+        # as we sent an up-to-dateness vector showing that we had already synced with DC1
+        out = self.check_output("samba-tool drs replicate --local %s %s %s %s -s %s"
+                                % ("invalid", self.dc2, nc_name,
+                                   self.cmdline_creds, new_dc_config_file))
+
+        (obj_2, link_2) = get_num_obj_links(out)
+
+        self.assertEqual(obj_2, 0)
+        self.assertEqual(link_2, 0)
 
     def test_samba_tool_replicate_machine_creds_P(self):
         """Tests 'samba-tool drs replicate -P' command with machine creds."""
@@ -247,12 +350,6 @@ class SambaToolDrsTests(drs_base.DrsBaseTestCase):
                              attrs=[])
             self.assertRaises(ldb.LdbError, check_dns_account_obj)
 
-        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"))
-
     def test_samba_tool_drs_clone_dc_secrets(self):
         """Tests 'samba-tool drs clone-dc-database --include-secrets' command ."""
         server_rootdse = self._get_rootDSE(self.dc1)
@@ -325,12 +422,6 @@ class SambaToolDrsTests(drs_base.DrsBaseTestCase):
                              attrs=[])
             self.assertRaises(ldb.LdbError, check_dns_account_obj)
 
-        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"))
-
     def test_samba_tool_drs_clone_dc_secrets_without_targetdir(self):
         """Tests 'samba-tool drs clone-dc-database' command without --targetdir."""
         server_rootdse = self._get_rootDSE(self.dc1)
-- 
1.9.1



More information about the samba-technical mailing list