summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--python/samba/netcmd/drs.py34
-rw-r--r--source4/torture/drs/python/samba_tool_drs_showrepl.py145
2 files changed, 179 insertions, 0 deletions
diff --git a/python/samba/netcmd/drs.py b/python/samba/netcmd/drs.py
index 254122a2924..613f56c8a4f 100644
--- a/python/samba/netcmd/drs.py
+++ b/python/samba/netcmd/drs.py
@@ -101,6 +101,8 @@ class cmd_drs_showrepl(Command):
takes_options = [
Option("--json", help="replication details in JSON format",
dest='format', action='store_const', const='json'),
+ Option("--summary", help="summarize local DRS health",
+ dest='format', action='store_const', const='summary'),
Option("--classic", help="print local replication details",
dest='format', action='store_const', const='classic',
default=DEFAULT_SHOWREPL_FORMAT),
@@ -170,6 +172,7 @@ class cmd_drs_showrepl(Command):
self.verbose = verbose
output_function = {
+ 'summary': self.summary_output,
'json': self.json_output,
'classic': self.classic_output,
}.get(format)
@@ -184,6 +187,37 @@ class cmd_drs_showrepl(Command):
del data['server']
json.dump(data, self.outf, indent=2)
+ def summary_output(self):
+ """Print a short message if every seems fine, but print details of any
+ links that seem broken."""
+ failing_repsto = []
+ failing_repsfrom = []
+
+ local_data = self.get_local_repl_data()
+ for rep in local_data['repsTo']:
+ if rep["consecutive failures"] != 0 or rep["last success"] == 0:
+ failing_repsto.append(rep)
+
+ for rep in local_data['repsFrom']:
+ if rep["consecutive failures"] != 0 or rep["last success"] == 0:
+ failing_repsto.append(rep)
+
+ if failing_repsto or failing_repsfrom:
+ self.message(colour.c_RED("There are failing connections"))
+ if failing_repsto:
+ self.message(colour.c_RED("Failing outbound connections:"))
+ for rep in failing_repsto:
+ self.print_neighbour(rep)
+ if failing_repsfrom:
+ self.message(colour.c_RED("Failing inbound connection:"))
+ for rep in failing_repsfrom:
+ self.print_neighbour(rep)
+
+ return 1
+
+ self.message(colour.c_GREEN("[ALL GOOD]"))
+
+
def get_local_repl_data(self):
drsuapi_connect(self)
samdb_connect(self)
diff --git a/source4/torture/drs/python/samba_tool_drs_showrepl.py b/source4/torture/drs/python/samba_tool_drs_showrepl.py
index 90bb0484a27..f7a806e660e 100644
--- a/source4/torture/drs/python/samba_tool_drs_showrepl.py
+++ b/source4/torture/drs/python/samba_tool_drs_showrepl.py
@@ -20,8 +20,12 @@
from __future__ import print_function
import samba.tests
import drs_base
+from samba.dcerpc import drsuapi
+from samba import drs_utils
import re
import json
+import ldb
+import random
from samba.compat import PY3
if PY3:
@@ -158,3 +162,144 @@ class SambaToolDrsShowReplTests(drs_base.DrsBaseTestCase):
self.assertTrue(isinstance(n['options'], int))
self.assertTrue(isinstance(n['replicates NC'], list))
self.assertRegex("^%s$" % DN_RE, n["remote DN"])
+
+ def _force_all_reps(self, samdb, dc, direction):
+ if direction == 'inbound':
+ info_type = drsuapi.DRSUAPI_DS_REPLICA_INFO_NEIGHBORS
+ elif direction == 'outbound':
+ info_type = drsuapi.DRSUAPI_DS_REPLICA_INFO_REPSTO
+ else:
+ raise ValueError("expected 'inbound' or 'outbound'")
+
+ self._enable_all_repl(dc)
+ lp = self.get_loadparm()
+ creds = self.get_credentials()
+ drsuapi_conn, drsuapi_handle, _ = drs_utils.drsuapi_connect(dc, lp, creds)
+ req1 = drsuapi.DsReplicaGetInfoRequest1()
+ req1.info_type = info_type
+ _, info = drsuapi_conn.DsReplicaGetInfo(drsuapi_handle, 1, req1)
+ for x in info.array:
+ # you might think x.source_dsa_address was the thing, but no.
+ # and we need to filter out RODCs and deleted DCs
+
+ res = []
+ try:
+ res = samdb.search(base=x.source_dsa_obj_dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=['msDS-isRODC', 'isDeleted'],
+ controls=['show_deleted:0'])
+ except ldb.LdbError as e:
+ if e.args[0] != ldb.ERR_NO_SUCH_OBJECT:
+ raise
+
+ if (len(res) == 0 or
+ len(res[0].get('msDS-isRODC', '')) > 0 or
+ res[0]['isDeleted'] == 'TRUE'):
+ continue
+
+ dsa_dn = str(ldb.Dn(samdb, x.source_dsa_obj_dn).parent())
+ res = samdb.search(base=dsa_dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=['dNSHostName'])
+
+ remote = res[0]['dNSHostName'][0]
+ self._enable_all_repl(remote)
+ if direction == 'inbound':
+ src, dest = remote, dc
+ else:
+ src, dest = dc, remote
+ self._net_drs_replicate(dest, src, forced=True)
+
+ def test_samba_tool_showrepl_summary_all_good(self):
+ """Tests 'samba-tool drs showrepl --summary' command."""
+ # To be sure that all is good we need to force replication
+ # with everyone (because others might have it turned off), and
+ # turn replication on for them in case they suddenly decide to
+ # try again.
+ #
+ # We don't restore them to the non-auto-replication state.
+ samdb1 = self.getSamDB("-H", "ldap://%s" % self.dc1, "-U",
+ self.cmdline_creds)
+ self._enable_all_repl(self.dc1)
+ self._force_all_reps(samdb1, self.dc1, 'inbound')
+ self._force_all_reps(samdb1, self.dc1, 'outbound')
+
+ out = self.check_output("samba-tool drs showrepl --summary %s %s" %
+ (self.dc1, self.cmdline_creds))
+ self.assertStringsEqual(out, "[ALL GOOD]\n")
+
+ out = self.check_output("samba-tool drs showrepl --summary "
+ "--color=yes %s %s" %
+ (self.dc1, self.cmdline_creds))
+ self.assertStringsEqual(out, "\033[1;32m[ALL GOOD]\033[0m\n")
+
+ # --verbose output is still quiet when all is good.
+ out = self.check_output("samba-tool drs showrepl --summary -v %s %s" %
+ (self.dc1, self.cmdline_creds))
+ self.assertStringsEqual(out, "[ALL GOOD]\n")
+ out = self.check_output("samba-tool drs showrepl --summary -v "
+ "--color=yes %s %s" %
+ (self.dc1, self.cmdline_creds))
+ self.assertStringsEqual(out, "\033[1;32m[ALL GOOD]\033[0m\n")
+
+ def test_samba_tool_showrepl_summary_forced_failure(self):
+ """Tests 'samba-tool drs showrepl --summary' command when we break the
+ network on purpose.
+ """
+ self.addCleanup(self._enable_all_repl, self.dc1)
+ self._disable_all_repl(self.dc1)
+
+ samdb1 = self.getSamDB("-H", "ldap://%s" % self.dc1, "-U",
+ self.cmdline_creds)
+ samdb2 = self.getSamDB("-H", "ldap://%s" % self.dc2, "-U",
+ self.cmdline_creds)
+ domain_dn = samdb1.domain_dn()
+
+ # Add some things to NOT replicate
+ ou1 = "OU=dc1.%x,%s" % (random.randrange(1 << 64), domain_dn)
+ ou2 = "OU=dc2.%x,%s" % (random.randrange(1 << 64), domain_dn)
+ samdb1.add({
+ "dn": ou1,
+ "objectclass": "organizationalUnit"
+ })
+ self.addCleanup(samdb1.delete, ou1, ['tree_delete:1'])
+ samdb2.add({
+ "dn": ou2,
+ "objectclass": "organizationalUnit"
+ })
+ self.addCleanup(samdb2.delete, ou2, ['tree_delete:1'])
+
+ dn1 = 'cn=u1.%%d,%s' % (ou1)
+ dn2 = 'cn=u2.%%d,%s' % (ou2)
+
+ try:
+ for i in range(100):
+ samdb1.add({
+ "dn": dn1 % i,
+ "objectclass": "user"
+ })
+ samdb2.add({
+ "dn": dn2 % i,
+ "objectclass": "user"
+ })
+ out = self.check_output("samba-tool drs showrepl --summary -v "
+ "%s %s" %
+ (self.dc1, self.cmdline_creds))
+ self.assertStringsEqual('[ALL GOOD]', out, strip=True)
+ out = self.check_output("samba-tool drs showrepl --summary -v "
+ "--color=yes %s %s" %
+ (self.dc2, self.cmdline_creds))
+ self.assertIn('[ALL GOOD]', out)
+
+ except samba.tests.BlackboxProcessError as e:
+ print("Good, failed as expected after %d rounds: %r" % (i, e.cmd))
+ self.assertIn('There are failing connections', e.stdout)
+ self.assertRegexpMatches(e.stdout,
+ r'result 845[67] '
+ r'\(WERR_DS_DRA_(SINK|SOURCE)_DISABLED\)',
+ msg=("The process should have failed "
+ "because replication was forced off, "
+ "but it failed for some other reason."))
+ self.assertIn('consecutive failure(s).', e.stdout)
+ else:
+ self.fail("No DRS failure noticed after 100 rounds of trying")