samba-tool drs showrepl --summary for a quick local check
authorDouglas Bagnall <douglas.bagnall@catalyst.net.nz>
Thu, 7 Jun 2018 02:15:10 +0000 (14:15 +1200)
committerDouglas Bagnall <dbagnall@samba.org>
Thu, 28 Jun 2018 04:29:16 +0000 (06:29 +0200)
The default output ("classic") gives you a lot of very uninteresting
detail when everything is fine. --summary shuts up about things that
are fine but shouts a little bit when things are broken. It doesn't
provide any new information, just tries to present it in a more useful
format.

Signed-off-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
python/samba/netcmd/drs.py
source4/torture/drs/python/samba_tool_drs_showrepl.py

index 254122a292499e853908f28fbbb27e2a30c76681..613f56c8a4fd0f423f14417398fcb903e39bc271 100644 (file)
@@ -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)
index 90bb0484a27922ac6f0985760068a2fcc4655746..f7a806e660e86030b0666e5f8d96b11d79854228 100644 (file)
 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")