1
0
mirror of https://github.com/samba-team/samba.git synced 2025-03-20 22:50:26 +03:00

sambatool visualize: add up-to-dateness visualization

Or more accurately, out-of-dateness visualization, which shows how far
each DCs is from every other using the difference in the up-to-dateness
vectors.

An example usage is

samba-tool visualize uptodateness -r -S -H ldap://somewhere \
      -UAdministrator --color=auto --partition=DOMAIN

Signed-off-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
This commit is contained in:
Douglas Bagnall 2018-06-01 17:20:56 +12:00 committed by Andrew Bartlett
parent 04a773f30f
commit 2d8cc50d39
2 changed files with 532 additions and 4 deletions

View File

@ -25,12 +25,14 @@ from collections import defaultdict
import subprocess
import tempfile
import samba
import samba.getopt as options
from samba import dsdb
from samba import nttime2unix
from samba.netcmd import Command, SuperCommand, CommandError, Option
from samba.samdb import SamDB
from samba.graph import dot_graph
from samba.graph import distance_matrix, COLOUR_SETS
from samba.graph import full_matrix
from ldb import SCOPE_BASE, SCOPE_SUBTREE, LdbError
import time
import re
@ -672,6 +674,140 @@ class cmd_ntdsconn(GraphCommand):
self.write(s, output)
class cmd_uptodateness(GraphCommand):
"""visualize uptodateness vectors"""
takes_options = COMMON_OPTIONS + [
Option("-p", "--partition", help="restrict to this partition",
default=None),
Option("--max-digits", default=3, type=int,
help="display this many digits of out-of-date-ness"),
]
def get_utdv(self, samdb, dn):
"""This finds the uptodateness vector in the database."""
cursors = []
config_dn = samdb.get_config_basedn()
for c in dsdb._dsdb_load_udv_v2(samdb, dn):
inv_id = str(c.source_dsa_invocation_id)
res = samdb.search(base=config_dn,
expression=("(&(invocationId=%s)"
"(objectClass=nTDSDSA))" % inv_id),
attrs=["distinguishedName", "invocationId"])
settings_dn = res[0]["distinguishedName"][0]
prefix, dsa_dn = settings_dn.split(',', 1)
if prefix != 'CN=NTDS Settings':
raise CommandError("Expected NTDS Settings DN, got %s" %
settings_dn)
cursors.append((dsa_dn,
inv_id,
int(c.highest_usn),
nttime2unix(c.last_sync_success)))
return cursors
def get_own_cursor(self, samdb):
res = samdb.search(base="",
scope=SCOPE_BASE,
attrs=["highestCommittedUSN"])
usn = int(res[0]["highestCommittedUSN"][0])
now = int(time.time())
return (usn, now)
def run(self, H=None, output=None, shorten_names=False,
key=True, talk_to_remote=False,
sambaopts=None, credopts=None, versionopts=None,
color=None, color_scheme=None,
utf8=False, format=None, importldif=None,
xdot=False, partition=None, max_digits=3):
if not talk_to_remote:
print("this won't work without talking to the remote servers "
"(use -r)", file=self.outf)
return
# We use the KCC libraries in readonly mode to get the
# replication graph.
lp = sambaopts.get_loadparm()
creds = credopts.get_credentials(lp, fallback_machine=True)
local_kcc, dsas = self.get_kcc_and_dsas(H, lp, creds)
self.samdb = local_kcc.samdb
partition = get_partition(self.samdb, partition)
short_partitions, long_partitions = get_partition_maps(self.samdb)
color_scheme = self.calc_distance_color_scheme(color,
color_scheme,
output)
for part_name, part_dn in short_partitions.items():
if partition not in (part_dn, None):
continue # we aren't doing this partition
cursors = self.get_utdv(self.samdb, part_dn)
# we talk to each remote and make a matrix of the vectors
# -- for each partition
# normalise by oldest
utdv_edges = {}
for dsa_dn in dsas:
res = local_kcc.samdb.search(dsa_dn,
scope=SCOPE_BASE,
attrs=["dNSHostName"])
ldap_url = "ldap://%s" % res[0]["dNSHostName"][0]
try:
samdb = self.get_db(ldap_url, sambaopts, credopts)
cursors = self.get_utdv(samdb, part_dn)
own_usn, own_time = self.get_own_cursor(samdb)
remotes = {dsa_dn: own_usn}
for dn, guid, usn, t in cursors:
remotes[dn] = usn
except LdbError as e:
print("Could not contact %s (%s)" % (ldap_url, e),
file=sys.stderr)
continue
utdv_edges[dsa_dn] = remotes
distances = {}
max_distance = 0
for dn1 in dsas:
try:
peak = utdv_edges[dn1][dn1]
except KeyError as e:
peak = 0
d = {}
distances[dn1] = d
for dn2 in dsas:
if dn2 in utdv_edges:
if dn1 in utdv_edges[dn2]:
dist = peak - utdv_edges[dn2][dn1]
d[dn2] = dist
if dist > max_distance:
max_distance = dist
else:
print("Missing dn %s from UTD vector" % dn1,
file=sys.stderr)
else:
print("missing dn %s from UTD vector list" % dn2,
file=sys.stderr)
digits = min(max_digits, len(str(max_distance)))
if digits < 1:
digits = 1
c_scale = 10 ** digits
s = full_matrix(distances,
utf8=utf8,
colour=color_scheme,
shorten_names=shorten_names,
generate_key=key,
grouping_function=get_dnstr_site,
colour_scale=c_scale,
digits=digits,
ylabel='DC',
xlabel='out-of-date-ness')
self.write('\n%s\n\n%s' % (part_name, s), output)
class cmd_visualize(SuperCommand):
"""Produces graphical representations of Samba network state"""
subcommands = {}

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
# Originally based on tests for samba.kcc.ldif_import_export.
# Copyright (C) Andrew Bartlett 2015, 2018
#
@ -16,15 +17,22 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""Tests for samba-tool visualize using the vampire DC and promoted DC
environments. We can't assert much about what state they are in, so we
mainly check for cmmand failure.
environments. For most tests we assume we can't assert much about what
state they are in, so we mainly check for command failure, but for
others we try to grasp control of replication and make more specific
assertions.
"""
from __future__ import print_function
import os
import re
import random
import subprocess
from samba.tests.samba_tool.base import SambaToolCmdTest
VERBOSE = False
ENV_DSAS = {
'promoted_dc': ['CN=PROMOTEDVDC,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com',
'CN=LOCALDC,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com'],
@ -33,6 +41,59 @@ ENV_DSAS = {
}
def set_auto_replication(dc, allow):
credstring = '-U%s%%%s' % (os.environ["USERNAME"], os.environ["PASSWORD"])
on_or_off = '-' if allow else '+'
for opt in ['DISABLE_INBOUND_REPL',
'DISABLE_OUTBOUND_REPL']:
cmd = ['bin/samba-tool',
'drs', 'options',
credstring, dc,
"--dsa-option=%s%s" % (on_or_off, opt)]
subprocess.check_call(cmd)
def force_replication(src, dest, base):
credstring = '-U%s%%%s' % (os.environ["USERNAME"], os.environ["PASSWORD"])
cmd = ['bin/samba-tool',
'drs', 'replicate',
dest, src, base,
credstring,
'--sync-forced']
subprocess.check_call(cmd)
def get_utf8_matrix(s):
# parse the graphical table *just* well enough for our tests
# decolourise first
s = re.sub("\033" r"\[[^m]+m", '', s)
lines = s.split('\n')
# matrix rows have '·' on the diagonal
rows = [x.strip().replace('·', '0') for x in lines if '·' in x]
names = []
values = []
for r in rows:
parts = r.rsplit(None, len(rows))
k, v = parts[0], parts[1:]
# we want the FOO in 'CN=FOO+' or 'CN=FOO,CN=x,DC=...'
k = re.match(r'cn=([^+,]+)', k.lower()).group(1)
names.append(k)
if len(v) == 1: # this is a single-digit matrix, no spaces
v = list(v[0])
values.append([int(x) if x.isdigit() else 1e999 for x in v])
d = {}
for n1, row in zip(names, values):
d[n1] = {}
for n2, v in zip(names, row):
d[n1][n2] = v
return d
class SambaToolVisualizeDrsTest(SambaToolCmdTest):
def setUp(self):
super(SambaToolVisualizeDrsTest, self).setUp()
@ -64,6 +125,337 @@ class SambaToolVisualizeDrsTest(SambaToolCmdTest):
'--color=no', '-S')
self.assertCmdSuccess(result, out, err)
def test_uptodateness_all_partitions(self):
creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
dc1 = os.environ["SERVER"]
dc2 = os.environ["DC_SERVER"]
# We will check that the visualisation works for the two
# stopped DCs, but we can't make assertions that the output
# will be the same because there may be replication between
# the two calls. Stopping the replication on these ones is not
# enough because there are other DCs about.
(result, out, err) = self.runsubcmd("visualize", "uptodateness",
"-r",
'-H', "ldap://%s" % dc1,
'-U', creds,
'--color=no', '-S')
self.assertCmdSuccess(result, out, err)
(result, out, err) = self.runsubcmd("visualize", "uptodateness",
"-r",
'-H', "ldap://%s" % dc2,
'-U', creds,
'--color=no', '-S')
self.assertCmdSuccess(result, out, err)
def test_uptodateness_partitions(self):
creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
dc1 = os.environ["SERVER"]
for part in ["CONFIGURATION",
"SCHEMA",
"DNSDOMAIN",
"DNSFOREST"]:
(result, out, err) = self.runsubcmd("visualize", "uptodateness",
"-r",
'-H', "ldap://%s" % dc1,
'-U', creds,
'--color=no', '-S',
'--partition', part)
self.assertCmdSuccess(result, out, err)
def assert_matrix_validity(self, matrix, dcs=()):
for dc in dcs:
self.assertIn(dc, matrix)
for k, row in matrix.items():
self.assertEqual(row[k], 0)
def test_uptodateness_stop_replication_domain(self):
creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
dc1 = os.environ["SERVER"]
dc2 = os.environ["DC_SERVER"]
self.addCleanup(set_auto_replication, dc1, True)
self.addCleanup(set_auto_replication, dc2, True)
def display(heading, out):
if VERBOSE:
print("========", heading, "=========")
print(out)
samdb1 = self.getSamDB("-H", "ldap://%s" % dc1, "-U", creds)
samdb2 = self.getSamDB("-H", "ldap://%s" % dc2, "-U", creds)
domain_dn = samdb1.domain_dn()
self.assertTrue(domain_dn == samdb2.domain_dn(),
"We expected the same domain_dn across DCs")
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"
})
samdb2.add({
"dn": ou2,
"objectclass": "organizationalUnit"
})
set_auto_replication(dc1, False)
(result, out, err) = self.runsubcmd("visualize", "uptodateness",
"-r",
'-H', "ldap://%s" % dc1,
'-U', creds,
'--color=yes',
'--utf8', '-S',
'--partition', 'DOMAIN')
display("dc1 replication is now off", out)
self.assertCmdSuccess(result, out, err)
matrix = get_utf8_matrix(out)
self.assert_matrix_validity(matrix, [dc1, dc2])
force_replication(dc2, dc1, domain_dn)
(result, out, err) = self.runsubcmd("visualize", "uptodateness",
"-r",
'-H', "ldap://%s" % dc1,
'-U', creds,
'--color=yes',
'--utf8', '-S',
'--partition', 'DOMAIN')
display("forced replication %s -> %s" % (dc2, dc1), out)
self.assertCmdSuccess(result, out, err)
matrix = get_utf8_matrix(out)
self.assert_matrix_validity(matrix, [dc1, dc2])
self.assertEqual(matrix[dc1][dc2], 0)
force_replication(dc1, dc2, domain_dn)
(result, out, err) = self.runsubcmd("visualize", "uptodateness",
"-r",
'-H', "ldap://%s" % dc1,
'-U', creds,
'--color=yes',
'--utf8', '-S',
'--partition', 'DOMAIN')
display("forced replication %s -> %s" % (dc2, dc1), out)
self.assertCmdSuccess(result, out, err)
matrix = get_utf8_matrix(out)
self.assert_matrix_validity(matrix, [dc1, dc2])
self.assertEqual(matrix[dc2][dc1], 0)
dn1 = 'cn=u1.%%d,%s' % (ou1)
dn2 = 'cn=u2.%%d,%s' % (ou2)
for i in range(10):
samdb1.add({
"dn": dn1 % i,
"objectclass": "user"
})
(result, out, err) = self.runsubcmd("visualize", "uptodateness",
"-r",
'-H', "ldap://%s" % dc1,
'-U', creds,
'--color=yes',
'--utf8', '-S',
'--partition', 'DOMAIN')
display("added 10 users on %s" % dc1, out)
self.assertCmdSuccess(result, out, err)
matrix = get_utf8_matrix(out)
self.assert_matrix_validity(matrix, [dc1, dc2])
# dc2's view of dc1 should now be 10 changes out of date
self.assertEqual(matrix[dc2][dc1], 10)
for i in range(10):
samdb2.add({
"dn": dn2 % i,
"objectclass": "user"
})
(result, out, err) = self.runsubcmd("visualize", "uptodateness",
"-r",
'-H', "ldap://%s" % dc1,
'-U', creds,
'--color=yes',
'--utf8', '-S',
'--partition', 'DOMAIN')
display("added 10 users on %s" % dc2, out)
self.assertCmdSuccess(result, out, err)
matrix = get_utf8_matrix(out)
self.assert_matrix_validity(matrix, [dc1, dc2])
# dc1's view of dc2 is probably 11 changes out of date
self.assertGreaterEqual(matrix[dc1][dc2], 10)
for i in range(10, 101):
samdb1.add({
"dn": dn1 % i,
"objectclass": "user"
})
samdb2.add({
"dn": dn2 % i,
"objectclass": "user"
})
(result, out, err) = self.runsubcmd("visualize", "uptodateness",
"-r",
'-H', "ldap://%s" % dc1,
'-U', creds,
'--color=yes',
'--utf8', '-S',
'--partition', 'DOMAIN')
display("added 91 users on both", out)
self.assertCmdSuccess(result, out, err)
matrix = get_utf8_matrix(out)
self.assert_matrix_validity(matrix, [dc1, dc2])
# the difference here should be ~101.
self.assertGreaterEqual(matrix[dc1][dc2], 100)
self.assertGreaterEqual(matrix[dc2][dc1], 100)
(result, out, err) = self.runsubcmd("visualize", "uptodateness",
"-r",
'-H', "ldap://%s" % dc1,
'-U', creds,
'--color=yes',
'--utf8', '-S',
'--partition', 'DOMAIN',
'--max-digits', '2')
display("with --max-digits 2", out)
self.assertCmdSuccess(result, out, err)
matrix = get_utf8_matrix(out)
self.assert_matrix_validity(matrix, [dc1, dc2])
# visualising with 2 digits mean these overflow into infinity
self.assertGreaterEqual(matrix[dc1][dc2], 1e99)
self.assertGreaterEqual(matrix[dc2][dc1], 1e99)
(result, out, err) = self.runsubcmd("visualize", "uptodateness",
"-r",
'-H', "ldap://%s" % dc1,
'-U', creds,
'--color=yes',
'--utf8', '-S',
'--partition', 'DOMAIN',
'--max-digits', '1')
display("with --max-digits 1", out)
self.assertCmdSuccess(result, out, err)
matrix = get_utf8_matrix(out)
self.assert_matrix_validity(matrix, [dc1, dc2])
# visualising with 1 digit means these overflow into infinity
self.assertGreaterEqual(matrix[dc1][dc2], 1e99)
self.assertGreaterEqual(matrix[dc2][dc1], 1e99)
force_replication(dc2, dc1, samdb1.domain_dn())
(result, out, err) = self.runsubcmd("visualize", "uptodateness",
"-r",
'-H', "ldap://%s" % dc1,
'-U', creds,
'--color=yes',
'--utf8', '-S',
'--partition', 'DOMAIN')
display("forced replication %s -> %s" % (dc2, dc1), out)
self.assertCmdSuccess(result, out, err)
matrix = get_utf8_matrix(out)
self.assert_matrix_validity(matrix, [dc1, dc2])
self.assertEqual(matrix[dc1][dc2], 0)
force_replication(dc1, dc2, samdb2.domain_dn())
(result, out, err) = self.runsubcmd("visualize", "uptodateness",
"-r",
'-H', "ldap://%s" % dc1,
'-U', creds,
'--color=yes',
'--utf8', '-S',
'--partition', 'DOMAIN')
display("forced replication %s -> %s" % (dc1, dc2), out)
self.assertCmdSuccess(result, out, err)
matrix = get_utf8_matrix(out)
self.assert_matrix_validity(matrix, [dc1, dc2])
self.assertEqual(matrix[dc2][dc1], 0)
samdb1.delete(ou1, ['tree_delete:1'])
samdb2.delete(ou2, ['tree_delete:1'])
(result, out, err) = self.runsubcmd("visualize", "uptodateness",
"-r",
'-H', "ldap://%s" % dc1,
'-U', creds,
'--color=yes',
'--utf8', '-S',
'--partition', 'DOMAIN')
display("tree delete both ous on %s" % (dc1,), out)
self.assertCmdSuccess(result, out, err)
matrix = get_utf8_matrix(out)
self.assert_matrix_validity(matrix, [dc1, dc2])
self.assertGreaterEqual(matrix[dc1][dc2], 100)
self.assertGreaterEqual(matrix[dc2][dc1], 100)
set_auto_replication(dc1, True)
(result, out, err) = self.runsubcmd("visualize", "uptodateness",
"-r",
'-H', "ldap://%s" % dc1,
'-U', creds,
'--color=yes',
'--utf8', '-S',
'--partition', 'DOMAIN')
display("replication is now on", out)
self.assertCmdSuccess(result, out, err)
matrix = get_utf8_matrix(out)
self.assert_matrix_validity(matrix, [dc1, dc2])
# We can't assert actual values after this because
# auto-replication is on and things will change underneath us.
(result, out, err) = self.runsubcmd("visualize", "uptodateness",
"-r",
'-H', "ldap://%s" % dc2,
'-U', creds,
'--color=yes',
'--utf8', '-S',
'--partition', 'DOMAIN')
display("%s's view" % dc2, out)
self.assertCmdSuccess(result, out, err)
matrix = get_utf8_matrix(out)
self.assert_matrix_validity(matrix, [dc1, dc2])
force_replication(dc1, dc2, samdb2.domain_dn())
(result, out, err) = self.runsubcmd("visualize", "uptodateness",
"-r",
'-H', "ldap://%s" % dc1,
'-U', creds,
'--color=yes',
'--utf8', '-S',
'--partition', 'DOMAIN')
display("forced replication %s -> %s" % (dc1, dc2), out)
self.assertCmdSuccess(result, out, err)
matrix = get_utf8_matrix(out)
self.assert_matrix_validity(matrix, [dc1, dc2])
force_replication(dc2, dc1, samdb2.domain_dn())
(result, out, err) = self.runsubcmd("visualize", "uptodateness",
"-r",
'-H', "ldap://%s" % dc1,
'-U', creds,
'--color=yes',
'--utf8', '-S',
'--partition', 'DOMAIN')
display("forced replication %s -> %s" % (dc2, dc1), out)
self.assertCmdSuccess(result, out, err)
matrix = get_utf8_matrix(out)
self.assert_matrix_validity(matrix, [dc1, dc2])
(result, out, err) = self.runsubcmd("visualize", "uptodateness",
"-r",
'-H', "ldap://%s" % dc2,
'-U', creds,
'--color=yes',
'--utf8', '-S',
'--partition', 'DOMAIN')
display("%s's view" % dc2, out)
self.assertCmdSuccess(result, out, err)
matrix = get_utf8_matrix(out)
self.assert_matrix_validity(matrix, [dc1, dc2])
def test_reps_remote(self):
server = "ldap://%s" % os.environ["SERVER"]
creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])