diff options
-rw-r--r-- | python/samba/netcmd/drs.py | 34 | ||||
-rw-r--r-- | source4/torture/drs/python/samba_tool_drs_showrepl.py | 145 |
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") |