summaryrefslogtreecommitdiff
path: root/python/samba
diff options
context:
space:
mode:
authorDouglas Bagnall <douglas.bagnall@catalyst.net.nz>2017-08-10 11:57:24 +1200
committerKarolin Seeger <kseeger@samba.org>2018-01-13 17:37:07 +0100
commitc6294c3c7b6c97f15daad7d463bda267726245c7 (patch)
tree06ab08dda8ab2ee28a8ea78f74d92180d8442f0a /python/samba
parentba2306f00d32d2fc55685b388e03e28fd7d97fd7 (diff)
downloadsamba-c6294c3c7b6c97f15daad7d463bda267726245c7.tar.gz
samba-tool visualize for understanding AD DC behaviour
To work out what is happening in a replication graph, it is sometimes helpful to use visualisations. We introduce a samba-tool subcommand to write Graphviz dot output and generate text-based heatmaps of the distance in hops between DCs. There are two subcommands, two graphical modes, and (roughly) two modes of operation with respect to the location of authority. `samba-tool visualize ntdsconn` looks at NTDS Connections. `samba-tool visualize reps` looks at repsTo and repsFrom objects. In '--distance' mode (default), the distances between DCs are shown in a matrix in the terminal. With '--color=yes', this is depicted as a heatmap. With '--utf8' it is a lttle prettier. In '--dot' mode, Graphviz dot output is generated. When viewed using dot or xdot, this shows the network as a graph with DCs as vertices and connections edges. Certain types of degenerate edges are shown in different colours or line-styles. Normally samba-tool talks to one database; with the '-r' (a.k.a. '--talk-to-remote') option attempts are made to contact all the DCs known to the first database. This is necessary to get sensible results from `samba-tool visualize reps` because the repsFrom/To objects are not replicated, and it can reveal replication issues in other modes. Signed-off-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abartlet@samba.org>
Diffstat (limited to 'python/samba')
-rw-r--r--python/samba/netcmd/main.py1
-rw-r--r--python/samba/netcmd/visualize.py574
-rw-r--r--python/samba/tests/samba_tool/visualize.py466
-rw-r--r--python/samba/tests/samba_tool/visualize_drs.py110
4 files changed, 1151 insertions, 0 deletions
diff --git a/python/samba/netcmd/main.py b/python/samba/netcmd/main.py
index cc16e4a3fe5..7f94f897897 100644
--- a/python/samba/netcmd/main.py
+++ b/python/samba/netcmd/main.py
@@ -76,3 +76,4 @@ class cmd_sambatool(SuperCommand):
subcommands["time"] = None
subcommands["user"] = None
subcommands["processes"] = None
+ subcommands["visualize"] = None
diff --git a/python/samba/netcmd/visualize.py b/python/samba/netcmd/visualize.py
new file mode 100644
index 00000000000..473872a7d72
--- /dev/null
+++ b/python/samba/netcmd/visualize.py
@@ -0,0 +1,574 @@
+# Visualisation tools
+#
+# Copyright (C) Andrew Bartlett 2015, 2018
+#
+# by Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
+#
+# 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/>.
+
+from __future__ import print_function
+
+import os
+import sys
+from collections import defaultdict
+
+import tempfile
+import samba
+import samba.getopt as options
+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 ldb import SCOPE_BASE, SCOPE_SUBTREE, LdbError
+import time
+from samba.kcc import KCC
+from samba.kcc.kcc_utils import KCCError
+
+COMMON_OPTIONS = [
+ Option("-H", "--URL", help="LDB URL for database or target server",
+ type=str, metavar="URL", dest="H"),
+ Option("-o", "--output", help="write here (default stdout)",
+ type=str, metavar="FILE", default=None),
+ Option("--dot", help="Graphviz dot output", dest='format',
+ const='dot', action='store_const'),
+ Option("--distance", help="Distance matrix graph output (default)",
+ dest='format', const='distance', action='store_const'),
+ Option("--utf8", help="Use utf-8 Unicode characters",
+ action='store_true'),
+ Option("--color", help="use color (yes, no, auto)",
+ choices=['yes', 'no', 'auto']),
+ Option("--color-scheme", help=("use this colour scheme "
+ "(implies --color=yes)"),
+ choices=COLOUR_SETS.keys()),
+ Option("-S", "--shorten-names",
+ help="don't print long common suffixes",
+ action='store_true', default=False),
+ Option("-r", "--talk-to-remote", help="query other DCs' databases",
+ action='store_true', default=False),
+ Option("--no-key", help="omit the explanatory key",
+ action='store_false', default=True, dest='key'),
+]
+
+TEMP_FILE = '__temp__'
+
+
+class GraphCommand(Command):
+ """Base class for graphing commands"""
+
+ synopsis = "%prog [options]"
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ "credopts": options.CredentialsOptions,
+ }
+ takes_options = COMMON_OPTIONS
+ takes_args = ()
+
+ def get_db(self, H, sambaopts, credopts):
+ lp = sambaopts.get_loadparm()
+ creds = credopts.get_credentials(lp, fallback_machine=True)
+ samdb = SamDB(url=H, credentials=creds, lp=lp)
+ return samdb
+
+ def get_kcc_and_dsas(self, H, lp, creds):
+ """Get a readonly KCC object and the list of DSAs it knows about."""
+ unix_now = int(time.time())
+ kcc = KCC(unix_now, readonly=True)
+ kcc.load_samdb(H, lp, creds)
+
+ dsa_list = kcc.list_dsas()
+ dsas = set(dsa_list)
+ if len(dsas) != len(dsa_list):
+ print("There seem to be duplicate dsas", file=sys.stderr)
+
+ return kcc, dsas
+
+ def write(self, s, fn=None, suffix='.dot'):
+ """Decide whether we're dealing with a filename, a tempfile, or
+ stdout, and write accordingly.
+
+ :param s: the string to write
+ :param fn: a destination
+ :param suffix: suffix, if destination is a tempfile
+
+ If fn is None or "-", write to stdout.
+ If fn is visualize.TEMP_FILE, write to a temporary file
+ Otherwise fn should be a filename to write to.
+ """
+ if fn is None or fn == '-':
+ # we're just using stdout (a.k.a self.outf)
+ print(s, file=self.outf)
+ return
+
+ if fn is TEMP_FILE:
+ fd, fn = tempfile.mkstemp(prefix='samba-tool-visualise',
+ suffix=suffix)
+ f = open(fn, 'w')
+ os.close(fd)
+ else:
+ f = open(fn, 'w')
+
+ f.write(s)
+ f.close()
+ return fn
+
+ def calc_output_format(self, format, output):
+ """Heuristics to work out what output format was wanted."""
+ if not format:
+ # They told us nothing! We have to work it out for ourselves.
+ if output and output.lower().endswith('.dot'):
+ return 'dot'
+ else:
+ return 'distance'
+ return format
+
+ def calc_distance_color_scheme(self, color, color_scheme, output):
+ """Heuristics to work out the colour scheme for distance matrices.
+ Returning None means no colour, otherwise it sould be a colour
+ from graph.COLOUR_SETS"""
+ if color == 'no':
+ return None
+
+ if color == 'auto':
+ if isinstance(output, str) and output != '-':
+ return None
+ if not hasattr(self.outf, 'isatty'):
+ # not a real file, perhaps cStringIO in testing
+ return None
+ if not self.outf.isatty():
+ return None
+
+ if color_scheme is None:
+ if '256color' in os.environ.get('TERM', ''):
+ return 'xterm-256color-heatmap'
+ return 'ansi'
+
+ return color_scheme
+
+
+def colour_hash(x):
+ """Generate a randomish but consistent darkish colour based on the
+ given object."""
+ from hashlib import md5
+ c = int(md5(str(x)).hexdigest()[:6], base=16) & 0x7f7f7f
+ return '#%06x' % c
+
+
+def get_partition_maps(samdb):
+ """Generate dictionaries mapping short partition names to the
+ appropriate DNs."""
+ base_dn = samdb.domain_dn()
+ short_to_long = {
+ "DOMAIN": base_dn,
+ "CONFIGURATION": str(samdb.get_config_basedn()),
+ "SCHEMA": "CN=Schema,%s" % samdb.get_config_basedn(),
+ "DNSDOMAIN": "DC=DomainDnsZones,%s" % base_dn,
+ "DNSFOREST": "DC=ForestDnsZones,%s" % base_dn
+ }
+ long_to_short = {v: k for k, v in short_to_long.iteritems()}
+ return short_to_long, long_to_short
+
+
+class cmd_reps(GraphCommand):
+ "repsFrom/repsTo from every DSA"
+
+ takes_options = COMMON_OPTIONS + [
+ Option("-p", "--partition", help="restrict to this partition",
+ default=None),
+ ]
+
+ def run(self, H=None, output=None, shorten_names=False,
+ key=True, talk_to_remote=False,
+ sambaopts=None, credopts=None, versionopts=None,
+ mode='self', partition=None, color=None, color_scheme=None,
+ utf8=None, format=None):
+ # 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)
+ unix_now = local_kcc.unix_now
+
+ # Allow people to say "--partition=DOMAIN" rather than
+ # "--partition=DC=blah,DC=..."
+ short_partitions, long_partitions = get_partition_maps(local_kcc.samdb)
+ if partition is not None:
+ partition = short_partitions.get(partition.upper(), partition)
+ if partition not in long_partitions:
+ raise CommandError("unknown partition %s" % partition)
+
+ # nc_reps is an autovivifying dictionary of dictionaries of lists.
+ # nc_reps[partition]['current' | 'needed'] is a list of
+ # (dsa dn string, repsFromTo object) pairs.
+ nc_reps = defaultdict(lambda: defaultdict(list))
+
+ guid_to_dnstr = {}
+
+ # We run a new KCC for each DSA even if we aren't talking to
+ # the remote, because after kcc.run (or kcc.list_dsas) the kcc
+ # ends up in a messy state.
+ for dsa_dn in dsas:
+ kcc = KCC(unix_now, readonly=True)
+ if talk_to_remote:
+ res = local_kcc.samdb.search(dsa_dn,
+ scope=SCOPE_BASE,
+ attrs=["dNSHostName"])
+ dns_name = res[0]["dNSHostName"][0]
+ print("Attempting to contact ldap://%s (%s)" %
+ (dns_name, dsa_dn),
+ file=sys.stderr)
+ try:
+ kcc.load_samdb("ldap://%s" % dns_name, lp, creds)
+ except KCCError as e:
+ print("Could not contact ldap://%s (%s)" % (dns_name, e),
+ file=sys.stderr)
+ continue
+
+ kcc.run(H, lp, creds)
+ else:
+ kcc.load_samdb(H, lp, creds)
+ kcc.run(H, lp, creds, forced_local_dsa=dsa_dn)
+
+ dsas_from_here = set(kcc.list_dsas())
+ if dsas != dsas_from_here:
+ print("found extra DSAs:", file=sys.stderr)
+ for dsa in (dsas_from_here - dsas):
+ print(" %s" % dsa, file=sys.stderr)
+ print("missing DSAs (known locally, not by %s):" % dsa_dn,
+ file=sys.stderr)
+ for dsa in (dsas - dsas_from_here):
+ print(" %s" % dsa, file=sys.stderr)
+
+ for remote_dn in dsas_from_here:
+ if mode == 'others' and remote_dn == dsa_dn:
+ continue
+ elif mode == 'self' and remote_dn != dsa_dn:
+ continue
+
+ remote_dsa = kcc.get_dsa('CN=NTDS Settings,' + remote_dn)
+ kcc.translate_ntdsconn(remote_dsa)
+ guid_to_dnstr[str(remote_dsa.dsa_guid)] = remote_dn
+ # get_reps_tables() returns two dictionaries mapping
+ # dns to NCReplica objects
+ c, n = remote_dsa.get_rep_tables()
+ for part, rep in c.iteritems():
+ if partition is None or part == partition:
+ nc_reps[part]['current'].append((dsa_dn, rep))
+ for part, rep in n.iteritems():
+ if partition is None or part == partition:
+ nc_reps[part]['needed'].append((dsa_dn, rep))
+
+ all_edges = {'needed': {'to': [], 'from': []},
+ 'current': {'to': [], 'from': []}}
+
+ for partname, part in nc_reps.iteritems():
+ for state, edgelists in all_edges.iteritems():
+ for dsa_dn, rep in part[state]:
+ short_name = long_partitions.get(partname, partname)
+ for r in rep.rep_repsFrom:
+ edgelists['from'].append(
+ (dsa_dn,
+ guid_to_dnstr[str(r.source_dsa_obj_guid)],
+ short_name))
+ for r in rep.rep_repsTo:
+ edgelists['to'].append(
+ (guid_to_dnstr[str(r.source_dsa_obj_guid)],
+ dsa_dn,
+ short_name))
+
+ # Here we have the set of edges. From now it is a matter of
+ # interpretation and presentation.
+
+ if self.calc_output_format(format, output) == 'distance':
+ color_scheme = self.calc_distance_color_scheme(color,
+ color_scheme,
+ output)
+ header_strings = {
+ 'from': "RepsFrom objects for %s",
+ 'to': "RepsTo objects for %s",
+ }
+ for state, edgelists in all_edges.iteritems():
+ for direction, items in edgelists.iteritems():
+ part_edges = defaultdict(list)
+ for src, dest, part in items:
+ part_edges[part].append((src, dest))
+ for part, edges in part_edges.iteritems():
+ s = distance_matrix(None, edges,
+ utf8=utf8,
+ colour=color_scheme,
+ shorten_names=shorten_names,
+ generate_key=key)
+
+ s = "\n%s\n%s" % (header_strings[direction] % part, s)
+ self.write(s, output)
+ return
+
+ edge_colours = []
+ edge_styles = []
+ dot_edges = []
+ dot_vertices = set()
+ used_colours = {}
+ key_set = set()
+ for state, edgelist in all_edges.iteritems():
+ for direction, items in edgelist.iteritems():
+ for src, dest, part in items:
+ colour = used_colours.setdefault((part),
+ colour_hash((part,
+ direction)))
+ linestyle = 'dotted' if state == 'needed' else 'solid'
+ arrow = 'open' if direction == 'to' else 'empty'
+ dot_vertices.add(src)
+ dot_vertices.add(dest)
+ dot_edges.append((src, dest))
+ edge_colours.append(colour)
+ style = 'style="%s"; arrowhead=%s' % (linestyle, arrow)
+ edge_styles.append(style)
+ key_set.add((part, 'reps' + direction.title(),
+ colour, style))
+
+ key_items = []
+ if key:
+ for part, direction, colour, linestyle in sorted(key_set):
+ key_items.append((False,
+ 'color="%s"; %s' % (colour, linestyle),
+ "%s %s" % (part, direction)))
+ key_items.append((False,
+ 'style="dotted"; arrowhead="open"',
+ "repsFromTo is needed"))
+ key_items.append((False,
+ 'style="solid"; arrowhead="open"',
+ "repsFromTo currently exists"))
+
+ s = dot_graph(dot_vertices, dot_edges,
+ directed=True,
+ edge_colors=edge_colours,
+ edge_styles=edge_styles,
+ shorten_names=shorten_names,
+ key_items=key_items)
+
+ self.write(s, output)
+
+
+class NTDSConn(object):
+ """Collects observation counts for NTDS connections, so we know
+ whether all DSAs agree."""
+ def __init__(self, src, dest):
+ self.observations = 0
+ self.src_attests = False
+ self.dest_attests = False
+ self.src = src
+ self.dest = dest
+
+ def attest(self, attester):
+ self.observations += 1
+ if attester == self.src:
+ self.src_attests = True
+ if attester == self.dest:
+ self.dest_attests = True
+
+
+class cmd_ntdsconn(GraphCommand):
+ "Draw the NTDSConnection graph"
+ 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=None, format=None):
+ lp = sambaopts.get_loadparm()
+ creds = credopts.get_credentials(lp, fallback_machine=True)
+ local_kcc, dsas = self.get_kcc_and_dsas(H, lp, creds)
+
+ vertices = set()
+ attested_edges = []
+ for dsa_dn in dsas:
+ if talk_to_remote:
+ res = local_kcc.samdb.search(dsa_dn,
+ scope=SCOPE_BASE,
+ attrs=["dNSHostName"])
+ dns_name = res[0]["dNSHostName"][0]
+ try:
+ samdb = self.get_db("ldap://%s" % dns_name, sambaopts,
+ credopts)
+ except LdbError as e:
+ print("Could not contact ldap://%s (%s)" % (dns_name, e),
+ file=sys.stderr)
+ continue
+
+ ntds_dn = samdb.get_dsServiceName()
+ dn = samdb.domain_dn()
+ else:
+ samdb = self.get_db(H, sambaopts, credopts)
+ ntds_dn = 'CN=NTDS Settings,' + dsa_dn
+ dn = dsa_dn
+
+ vertices.add(ntds_dn)
+ # XXX we could also look at schedule
+ res = samdb.search(dn,
+ scope=SCOPE_SUBTREE,
+ expression="(objectClass=nTDSConnection)",
+ attrs=['fromServer'],
+ # XXX can't be critical for ldif test
+ #controls=["search_options:1:2"],
+ controls=["search_options:0:2"],
+ )
+
+ for msg in res:
+ msgdn = str(msg.dn)
+ dest_dn = msgdn[msgdn.index(',') + 1:]
+ attested_edges.append((msg['fromServer'][0],
+ dest_dn, ntds_dn))
+
+ # now we overlay all the graphs and generate styles accordingly
+ edges = {}
+ for src, dest, attester in attested_edges:
+ k = (src, dest)
+ if k in edges:
+ e = edges[k]
+ else:
+ e = NTDSConn(*k)
+ edges[k] = e
+ e.attest(attester)
+
+ if self.calc_output_format(format, output) == 'distance':
+ color_scheme = self.calc_distance_color_scheme(color,
+ color_scheme,
+ output)
+ if not talk_to_remote:
+ # If we are not talking to remote servers, we list all
+ # the connections.
+ graph_edges = edges.keys()
+ title = 'NTDS Connections known to %s' % dsa_dn
+ epilog = ''
+
+ else:
+ # If we are talking to the remotes, there are
+ # interesting cases we can discover. What matters most
+ # is that the destination (i.e. owner) knowns about
+ # the connection, but it would be worth noting if the
+ # source doesn't. Another strange situation could be
+ # when a DC thinks there is a connection elsewhere,
+ # but the computers allegedly involved don't believe
+ # it exists.
+ #
+ # With limited bandwidth in the table, we mark the
+ # edges known to the destination, and note the other
+ # cases in a list after the diagram.
+ graph_edges = []
+ source_denies = []
+ dest_denies = []
+ both_deny = []
+ for e, conn in edges.iteritems():
+ if conn.dest_attests:
+ graph_edges.append(e)
+ if not conn.src_attests:
+ source_denies.append(e)
+ elif conn.src_attests:
+ dest_denies.append(e)
+ else:
+ both_deny.append(e)
+
+ title = 'NTDS Connections known to each destination DC'
+ epilog = []
+ if both_deny:
+ epilog.append('The following connections are alleged by '
+ 'DCs other than the source and '
+ 'destination:\n')
+ for e in both_deny:
+ epilog.append(' %s -> %s\n' % e)
+ if dest_denies:
+ epilog.append('The following connections are alleged by '
+ 'DCs other than the destination but '
+ 'including the source:\n')
+ for e in dest_denies:
+ epilog.append(' %s -> %s\n' % e)
+ if source_denies:
+ epilog.append('The following connections '
+ '(included in the chart) '
+ 'are not known to the source DC:\n')
+ for e in source_denies:
+ epilog.append(' %s -> %s\n' % e)
+ epilog = ''.join(epilog)
+
+ s = distance_matrix(sorted(vertices), graph_edges,
+ utf8=utf8,
+ colour=color_scheme,
+ shorten_names=shorten_names,
+ generate_key=key)
+ self.write('\n%s\n%s\n%s' % (title, s, epilog), output)
+ return
+
+ dot_edges = []
+ edge_colours = []
+ edge_styles = []
+ edge_labels = []
+ n_servers = len(dsas)
+ for k, e in sorted(edges.iteritems()):
+ dot_edges.append(k)
+ if e.observations == n_servers or not talk_to_remote:
+ edge_colours.append('#000000')
+ edge_styles.append('')
+ elif e.dest_attests:
+ edge_styles.append('')
+ if e.src_attests:
+ edge_colours.append('#0000ff')
+ else:
+ edge_colours.append('#cc00ff')
+ elif e.src_attests:
+ edge_colours.append('#ff0000')
+ edge_styles.append('style=dashed')
+ else:
+ edge_colours.append('#ff0000')
+ edge_styles.append('style=dotted')
+
+ key_items = []
+ if key:
+ key_items.append((False,
+ 'color="#000000"',
+ "NTDS Connection"))
+ for colour, desc in (('#0000ff', "missing from some DCs"),
+ ('#cc00ff', "missing from source DC")):
+ if colour in edge_colours:
+ key_items.append((False, 'color="%s"' % colour, desc))
+
+ for style, desc in (('style=dashed', "unknown to destination"),
+ ('style=dotted',
+ "unknown to source and destination")):
+ if style in edge_styles:
+ key_items.append((False,
+ 'color="#ff0000; %s"' % style,
+ desc))
+
+ if talk_to_remote:
+ title = 'NTDS Connections'
+ else:
+ title = 'NTDS Connections known to %s' % dsa_dn
+
+ s = dot_graph(sorted(vertices), dot_edges,
+ directed=True,
+ title=title,
+ edge_colors=edge_colours,
+ edge_labels=edge_labels,
+ edge_styles=edge_styles,
+ shorten_names=shorten_names,
+ key_items=key_items)
+ self.write(s, output)
+
+
+class cmd_visualize(SuperCommand):
+ """Produces graphical representations of Samba network state"""
+ subcommands = {}
+
+ for k, v in globals().iteritems():
+ if k.startswith('cmd_'):
+ subcommands[k[4:]] = v()
diff --git a/python/samba/tests/samba_tool/visualize.py b/python/samba/tests/samba_tool/visualize.py
new file mode 100644
index 00000000000..292d4961f45
--- /dev/null
+++ b/python/samba/tests/samba_tool/visualize.py
@@ -0,0 +1,466 @@
+# -*- coding: utf-8 -*-
+# Tests for samba-tool visualize
+# Copyright (C) Andrew Bartlett 2015, 2018
+#
+# by Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
+#
+# 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/>.
+#
+"""Tests for samba-tool visualize ntdsconn using the test ldif
+topologies.
+
+We don't test samba-tool visualize reps here because repsTo and
+repsFrom are not replicated, and there are actual remote servers to
+query.
+"""
+
+import samba
+import os
+import re
+from samba.tests.samba_tool.base import SambaToolCmdTest
+from samba.kcc import ldif_import_export
+from samba.graph import COLOUR_SETS
+from samba.param import LoadParm
+
+MULTISITE_LDIF = os.path.join(os.environ['SRCDIR_ABS'],
+ "testdata/ldif-utils-test-multisite.ldif")
+
+# UNCONNECTED_LDIF is a single site, unconnected 5DC database that was
+# created using samba-tool domain join in testenv.
+UNCONNECTED_LDIF = os.path.join(os.environ['SRCDIR_ABS'],
+ "testdata/unconnected-intrasite.ldif")
+
+DOMAIN = "DC=ad,DC=samba,DC=example,DC=com"
+DN_TEMPLATE = "CN=%s,CN=Servers,CN=%s,CN=Sites,CN=Configuration," + DOMAIN
+
+MULTISITE_LDIF_DSAS = [
+ ("WIN01", "Default-First-Site-Name"),
+ ("WIN08", "Site-4"),
+ ("WIN07", "Site-4"),
+ ("WIN06", "Site-3"),
+ ("WIN09", "Site-5"),
+ ("WIN10", "Site-5"),
+ ("WIN02", "Site-2"),
+ ("WIN04", "Site-2"),
+ ("WIN03", "Site-2"),
+ ("WIN05", "Site-2"),
+]
+
+
+def samdb_from_ldif(ldif, tempdir, lp, dsa=None, tag=''):
+ if dsa is None:
+ dsa_name = 'default-DSA'
+ else:
+ dsa_name = dsa[:5]
+ dburl = os.path.join(tempdir,
+ ("ldif-to-sambdb-%s-%s" %
+ (tag, dsa_name)))
+ samdb = ldif_import_export.ldif_to_samdb(dburl, lp, ldif,
+ forced_local_dsa=dsa)
+ return (samdb, dburl)
+
+
+def collapse_space(s):
+ lines = []
+ for line in s.splitlines():
+ line = ' '.join(line.strip().split())
+ lines.append(line)
+ return '\n'.join(lines)
+
+
+class SambaToolVisualizeLdif(SambaToolCmdTest):
+ def setUp(self):
+ super(SambaToolVisualizeLdif, self).setUp()
+ self.lp = LoadParm()
+ self.samdb, self.dbfile = samdb_from_ldif(MULTISITE_LDIF,
+ self.tempdir,
+ self.lp)
+ self.dburl = 'tdb://' + self.dbfile
+
+ def tearDown(self):
+ self.remove_files(self.dbfile)
+ super(SambaToolVisualizeLdif, self).tearDown()
+
+ def remove_files(self, *files):
+ for f in files:
+ self.assertTrue(f.startswith(self.tempdir))
+ os.unlink(f)
+
+ def test_colour(self):
+ """Ensure the colour output is the same as the monochrome output
+ EXCEPT for the colours, of which the monochrome one should
+ know nothing."""
+ colour_re = re.compile('\033' r'\[[\d;]+m')
+ result, monochrome, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=no', '-S')
+ self.assertCmdSuccess(result, monochrome, err)
+ self.assertFalse(colour_re.findall(monochrome))
+
+ colour_args = [['--color=yes']]
+ colour_args += [['--color-scheme', x] for x in COLOUR_SETS
+ if x is not None]
+
+ for args in colour_args:
+ result, out, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '-S', *args)
+ self.assertCmdSuccess(result, out, err)
+ self.assertTrue(colour_re.search(out))
+ uncoloured = colour_re.sub('', out)
+
+ self.assertStringsEqual(monochrome, uncoloured, strip=True)
+
+ def test_output_file(self):
+ """Check that writing to a file works, with and without
+ --color=auto."""
+ # NOTE, we can't really test --color=auto works with a TTY.
+ colour_re = re.compile('\033' r'\[[\d;]+m')
+ result, expected, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=auto', '-S')
+ self.assertCmdSuccess(result, expected, err)
+ # Not a TTY, so stdout output should be colourless
+ self.assertFalse(colour_re.search(expected))
+ expected = expected.strip()
+
+ color_auto_file = os.path.join(self.tempdir, 'color-auto')
+
+ result, out, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=auto', '-S',
+ '-o', color_auto_file)
+ self.assertCmdSuccess(result, out, err)
+ # We wrote to file, so stdout should be empty
+ self.assertEqual(out, '')
+ f = open(color_auto_file)
+ color_auto = f.read()
+ f.close()
+ self.assertStringsEqual(color_auto, expected, strip=True)
+ self.remove_files(color_auto_file)
+
+ color_no_file = os.path.join(self.tempdir, 'color-no')
+ result, out, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=no', '-S',
+ '-o', color_no_file)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(out, '')
+ f = open(color_no_file)
+ color_no = f.read()
+ f.close()
+ self.remove_files(color_no_file)
+
+ self.assertStringsEqual(color_no, expected, strip=True)
+
+ color_yes_file = os.path.join(self.tempdir, 'color-no')
+ result, out, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=yes', '-S',
+ '-o', color_yes_file)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(out, '')
+ f = open(color_yes_file)
+ colour_yes = f.read()
+ f.close()
+ self.assertNotEqual(colour_yes.strip(), expected)
+
+ self.remove_files(color_yes_file)
+
+ # Try the magic filename "-", meaning stdout.
+ # This doesn't exercise the case when stdout is a TTY
+ for c, equal in [('no', True), ('auto', True), ('yes', False)]:
+ result, out, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color', c,
+ '-S', '-o', '-')
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual((out.strip() == expected), equal)
+
+ def test_utf8(self):
+ """Ensure that --utf8 adds at least some expected utf-8, and that it
+ isn't there without --utf8."""
+ result, utf8, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=no', '-S', '--utf8')
+ self.assertCmdSuccess(result, utf8, err)
+
+ result, ascii, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=no', '-S')
+ self.assertCmdSuccess(result, ascii, err)
+ for c in ('│', '─', '╭'):
+ self.assertTrue(c in utf8, 'UTF8 should contain %s' % c)
+ self.assertTrue(c not in ascii, 'ASCII should not contain %s' % c)
+
+ def test_forced_local_dsa(self):
+ # the forced_local_dsa shouldn't make any difference
+ result, target, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=no', '-S')
+ self.assertCmdSuccess(result, target, err)
+ files = []
+ for cn, site in MULTISITE_LDIF_DSAS:
+ dsa = DN_TEMPLATE % (cn, site)
+ samdb, dbfile = samdb_from_ldif(MULTISITE_LDIF,
+ self.tempdir,
+ self.lp, dsa,
+ tag=cn)
+
+ result, out, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', 'tdb://' + dbfile,
+ '--color=no', '-S')
+ self.assertCmdSuccess(result, out, err)
+ self.assertStringsEqual(target, out)
+ files.append(dbfile)
+ self.remove_files(*files)
+
+ def test_short_names(self):
+ """Ensure the colour ones are the same as the monochrome ones EXCEPT
+ for the colours, of which the monochrome one should know nothing"""
+ result, short, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=no', '-S', '--no-key')
+ self.assertCmdSuccess(result, short, err)
+ result, long, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=no', '--no-key')
+ self.assertCmdSuccess(result, long, err)
+
+ lines = short.split('\n')
+ replacements = []
+ key_lines = ['']
+ short_without_key = []
+ for line in lines:
+ m = re.match(r"'(.{1,2})' stands for '(.+)'", line)
+ if m:
+ a, b = m.groups()
+ replacements.append((len(a), a, b))
+ key_lines.append(line)
+ else:
+ short_without_key.append(line)
+
+ short = '\n'.join(short_without_key)
+ # we need to replace longest strings first
+ replacements.sort(reverse=True)
+ short2long = short
+ # we don't want to shorten the DC name in the header line.
+ long_header, long2short = long.strip().split('\n', 1)
+ for _, a, b in replacements:
+ short2long = short2long.replace(a, b)
+ long2short = long2short.replace(b, a)
+
+ long2short = '%s\n%s' % (long_header, long2short)
+
+ # The white space is going to be all wacky, so lets squish it down
+ short2long = collapse_space(short2long)
+ long2short = collapse_space(long2short)
+ short = collapse_space(short)
+ long = collapse_space(long)
+
+ self.assertStringsEqual(short2long, long, strip=True)
+ self.assertStringsEqual(short, long2short, strip=True)
+
+ def test_disconnected_ldif_with_key(self):
+ """Test that the 'unconnected' ldif shows up and exactly matches the
+ expected output."""
+ # This is not truly a disconnected graph because the
+ # vampre/local/promoted DCs are in there and they have
+ # relationships, and SERVER2 and SERVER3 for some reason refer
+ # to them.
+
+ samdb, dbfile = samdb_from_ldif(UNCONNECTED_LDIF,
+ self.tempdir,
+ self.lp, tag='disconnected')
+ dburl = 'tdb://' + dbfile
+ print dbfile
+ result, output, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', dburl,
+ '--color=no', '-S')
+ self.remove_files(dbfile)
+ self.assertCmdSuccess(result, output, err)
+ self.assertStringsEqual(output,
+ EXPECTED_DISTANCE_GRAPH_WITH_KEY)
+
+ def test_dot_ntdsconn(self):
+ """Graphviz NTDS Connection output"""
+ result, dot, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=no', '-S', '--dot',
+ '--no-key')
+ self.assertCmdSuccess(result, dot, err)
+ self.assertStringsEqual(EXPECTED_DOT_MULTISITE_NO_KEY, dot)
+
+ def test_dot_ntdsconn_disconnected(self):
+ """Graphviz NTDS Connection output from disconnected graph"""
+ samdb, dbfile = samdb_from_ldif(UNCONNECTED_LDIF,
+ self.tempdir,
+ self.lp, tag='disconnected')
+
+ result, dot, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', 'tdb://' + dbfile,
+ '--color=no', '-S', '--dot',
+ '-o', '-')
+ self.assertCmdSuccess(result, dot, err)
+ self.remove_files(dbfile)
+ print dot
+
+ self.assertStringsEqual(EXPECTED_DOT_NTDSCONN_DISCONNECTED, dot,
+ strip=True)
+
+ def test_dot_ntdsconn_disconnected_to_file(self):
+ """Graphviz NTDS Connection output into a file"""
+ samdb, dbfile = samdb_from_ldif(UNCONNECTED_LDIF,
+ self.tempdir,
+ self.lp, tag='disconnected')
+
+ dot_file = os.path.join(self.tempdir, 'dotfile')
+
+ result, dot, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', 'tdb://' + dbfile,
+ '--color=no', '-S', '--dot',
+ '-o', dot_file)
+ self.assertCmdSuccess(result, dot, err)
+ f = open(dot_file)
+ dot = f.read()
+ f.close()
+ self.assertStringsEqual(EXPECTED_DOT_NTDSCONN_DISCONNECTED, dot)
+
+ self.remove_files(dbfile, dot_file)
+ print dot
+
+EXPECTED_DOT_MULTISITE_NO_KEY = r"""/* generated by samba */
+digraph A_samba_tool_production {
+label="NTDS Connections known to CN=WIN07,CN=Servers,CN=Site-4,CN=Sites,CN=Configuration,DC=ad,DC=samba,DC=example,DC=com";
+fontsize=10;
+
+node[fontname=Helvetica; fontsize=10];
+
+"CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n...";
+"CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n...";
+"CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n...";
+"CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n...";
+"CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n...";
+"CN=NTDS Settings,\nCN=WIN06,\nCN=Servers,\nCN=Site-3,\n...";
+"CN=NTDS Settings,\nCN=WIN07,\nCN=Servers,\nCN=Site-4,\n...";
+"CN=NTDS Settings,\nCN=WIN08,\nCN=Servers,\nCN=Site-4,\n...";
+"CN=NTDS Settings,\nCN=WIN09,\nCN=Servers,\nCN=Site-5,\n...";
+"CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n...";
+"CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." -> "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." -> "CN=NTDS Settings,\nCN=WIN06,\nCN=Servers,\nCN=Site-3,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." -> "CN=NTDS Settings,\nCN=WIN07,\nCN=Servers,\nCN=Site-4,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." -> "CN=NTDS Settings,\nCN=WIN08,\nCN=Servers,\nCN=Site-4,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." -> "CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN07,\nCN=Servers,\nCN=Site-4,\n..." -> "CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN09,\nCN=Servers,\nCN=Site-5,\n..." -> "CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n..." -> "CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n..." -> "CN=NTDS Settings,\nCN=WIN09,\nCN=Servers,\nCN=Site-5,\n..." [color="#000000", ];
+}
+
+"""
+
+
+EXPECTED_DOT_NTDSCONN_DISCONNECTED = r"""/* generated by samba */
+digraph A_samba_tool_production {
+label="NTDS Connections known to CN=SERVER2,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com";
+fontsize=10;
+
+node[fontname=Helvetica; fontsize=10];
+
+"CN=NTDS Settings,\nCN=CLIENT,\n...";
+"CN=NTDS Settings,\nCN=LOCALDC,\n...";
+"CN=NTDS Settings,\nCN=PROMOTEDVDC,\n...";
+"CN=NTDS Settings,\nCN=SERVER1,\n...";
+"CN=NTDS Settings,\nCN=SERVER2,\n...";
+"CN=NTDS Settings,\nCN=SERVER3,\n...";
+"CN=NTDS Settings,\nCN=SERVER4,\n...";
+"CN=NTDS Settings,\nCN=SERVER5,\n...";
+"CN=NTDS Settings,\nCN=LOCALDC,\n..." -> "CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." -> "CN=NTDS Settings,\nCN=LOCALDC,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=SERVER2,\n..." -> "CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=SERVER3,\n..." -> "CN=NTDS Settings,\nCN=LOCALDC,\n..." [color="#000000", ];
+subgraph cluster_key {
+label="Key";
+subgraph cluster_key_nodes {
+label="";
+color = "invis";
+
+}
+subgraph cluster_key_edges {
+label="";
+color = "invis";
+subgraph cluster_key_0_ {
+key_0_e1[label=src; color="#000000"; group="key_0__g"]
+key_0_e2[label=dest; color="#000000"; group="key_0__g"]
+key_0_e1 -> key_0_e2 [constraint = false; color="#000000"]
+key_0__label[shape=plaintext; style=solid; width=2.000000; label="NTDS Connection\r"]
+}
+{key_0__label}
+}
+
+elision0[shape=plaintext; style=solid; label="\“...” means “CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com”\r"]
+
+}
+"CN=NTDS Settings,\nCN=CLIENT,\n..." -> key_0__label [style=invis];
+"CN=NTDS Settings,\nCN=LOCALDC,\n..." -> key_0__label [style=invis];
+"CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." -> key_0__label [style=invis];
+"CN=NTDS Settings,\nCN=SERVER1,\n..." -> key_0__label [style=invis];
+"CN=NTDS Settings,\nCN=SERVER2,\n..." -> key_0__label [style=invis];
+"CN=NTDS Settings,\nCN=SERVER3,\n..." -> key_0__label [style=invis];
+"CN=NTDS Settings,\nCN=SERVER4,\n..." -> key_0__label [style=invis];
+"CN=NTDS Settings,\nCN=SERVER5,\n..." -> key_0__label [style=invis]
+key_0__label -> elision0 [style=invis; weight=9]
+
+}
+"""
+
+EXPECTED_DISTANCE_GRAPH_WITH_KEY = """
+NTDS Connections known to CN=SERVER2,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com
+ destination
+ ,-------- *,CN=CLIENT+
+ |,------- *,CN=LOCALDC+
+ ||,------ *,CN=PROMOTEDVDC+
+ |||,----- *,CN=SERVER1+
+ ||||,---- *,CN=SERVER2+
+ |||||,--- *,CN=SERVER3+
+ ||||||,-- *,CN=SERVER4+
+ source |||||||,- *,CN=SERVER5+
+ *,CN=CLIENT+ 0-------
+ *,CN=LOCALDC+ -01-----
+*,CN=PROMOTEDVDC+ -10-----
+ *,CN=SERVER1+ ---0----
+ *,CN=SERVER2+ -21-0---
+ *,CN=SERVER3+ -12--0--
+ *,CN=SERVER4+ ------0-
+ *,CN=SERVER5+ -------0
+
+'*' stands for 'CN=NTDS Settings'
+'+' stands for ',CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com'
+
+Data can get from source to destination in the indicated number of steps.
+0 means zero steps (it is the same DC)
+1 means a direct link
+2 means a transitive link involving two steps (i.e. one intermediate DC)
+- means there is no connection, even through other DCs
+
+"""
diff --git a/python/samba/tests/samba_tool/visualize_drs.py b/python/samba/tests/samba_tool/visualize_drs.py
new file mode 100644
index 00000000000..7da0a4b1083
--- /dev/null
+++ b/python/samba/tests/samba_tool/visualize_drs.py
@@ -0,0 +1,110 @@
+# Originally based on tests for samba.kcc.ldif_import_export.
+# Copyright (C) Andrew Bartlett 2015, 2018
+#
+# by Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
+#
+# 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/>.
+#
+
+"""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.
+"""
+
+import os
+from samba.tests.samba_tool.base import SambaToolCmdTest
+
+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'],
+ 'vampire_dc': ['CN=LOCALDC,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com',
+ 'CN=LOCALVAMPIREDC,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com'],
+}
+
+
+class SambaToolVisualizeDrsTest(SambaToolCmdTest):
+ def setUp(self):
+ super(SambaToolVisualizeDrsTest, self).setUp()
+
+ def test_ntdsconn(self):
+ server = "ldap://%s" % os.environ["SERVER"]
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ (result, out, err) = self.runsubcmd("visualize", "ntdsconn",
+ '-H', server,
+ '-U', creds,
+ '--color=no', '-S')
+ self.assertCmdSuccess(result, out, err)
+
+ def test_ntdsconn_remote(self):
+ server = "ldap://%s" % os.environ["SERVER"]
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ (result, out, err) = self.runsubcmd("visualize", "ntdsconn",
+ '-H', server,
+ '-U', creds,
+ '--color=no', '-S', '-r')
+ self.assertCmdSuccess(result, out, err)
+
+ def test_reps(self):
+ server = "ldap://%s" % os.environ["SERVER"]
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ (result, out, err) = self.runsubcmd("visualize", "reps",
+ '-H', server,
+ '-U', creds,
+ '--color=no', '-S')
+ self.assertCmdSuccess(result, out, err)
+
+ def test_reps_remote(self):
+ server = "ldap://%s" % os.environ["SERVER"]
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ (result, out, err) = self.runsubcmd("visualize", "reps",
+ '-H', server,
+ '-U', creds,
+ '--color=no', '-S', '-r')
+ self.assertCmdSuccess(result, out, err)
+
+ def test_ntdsconn_dot(self):
+ server = "ldap://%s" % os.environ["SERVER"]
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ (result, out, err) = self.runsubcmd("visualize", "ntdsconn",
+ '-H', server,
+ '-U', creds, '--dot',
+ '--color=no', '-S')
+ self.assertCmdSuccess(result, out, err)
+
+ def test_ntdsconn_remote_dot(self):
+ server = "ldap://%s" % os.environ["SERVER"]
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ (result, out, err) = self.runsubcmd("visualize", "ntdsconn",
+ '-H', server,
+ '-U', creds, '--dot',
+ '--color=no', '-S', '-r')
+ self.assertCmdSuccess(result, out, err)
+
+ def test_reps_dot(self):
+ server = "ldap://%s" % os.environ["SERVER"]
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ (result, out, err) = self.runsubcmd("visualize", "reps",
+ '-H', server,
+ '-U', creds, '--dot',
+ '--color=no', '-S')
+ self.assertCmdSuccess(result, out, err)
+
+ def test_reps_remote_dot(self):
+ server = "ldap://%s" % os.environ["SERVER"]
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ (result, out, err) = self.runsubcmd("visualize", "reps",
+ '-H', server,
+ '-U', creds, '--dot',
+ '--color=no', '-S', '-r')
+ self.assertCmdSuccess(result, out, err)