# -*- coding: utf-8 -*- # Tests for samba-tool visualize # Copyright (C) Andrew Bartlett 2015, 2018 # # by Douglas Bagnall # # 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 . # """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 no actual remote servers to query. """ import os import tempfile import re from io import StringIO 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"), ] class StringIOThinksItIsATTY(StringIO): """A StringIO that claims to be a TTY for testing --color=auto, by switching the stringIO class attribute.""" def isatty(self): return True 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, keep_empty_lines=False): lines = [] for line in s.splitlines(): line = ' '.join(line.strip().split()) if line or keep_empty_lines: lines.append(line) return '\n'.join(lines) class SambaToolVisualizeLdif(SambaToolCmdTest): def setUp(self): super().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().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), f"'{' '.join(args)}' should be colour") uncoloured = colour_re.sub('', out) self.assertStringsEqual(monochrome, uncoloured, strip=True) def assert_colour(self, text, has_colour=True, monochrome=None): colour_re = re.compile('\033' r'\[[\d;]+m') found = colour_re.search(text) if has_colour: self.assertTrue(found, text) else: self.assertFalse(found, text) if monochrome is not None: uncoloured = colour_re.sub('', text) self.assertStringsEqual(monochrome, uncoloured, strip=True) def test_colour_auto_tty(self): """Assert the behaviour of --colour=auto with and without NO_COLOUR on a fake tty""" result, monochrome, err = self.runsubcmd("visualize", "ntdsconn", '-H', self.dburl, '--color=no', '-S') self.assertCmdSuccess(result, monochrome, err) self.assert_colour(monochrome, False) cls = self.__class__ try: cls.stringIO = StringIOThinksItIsATTY old_no_color = os.environ.pop('NO_COLOR', None) # First with no NO_COLOR env var. There should be colour. result, out, err = self.runsubcmd("visualize", "ntdsconn", '-H', self.dburl, '-S', '--color=auto') self.assertCmdSuccess(result, out, err) self.assert_colour(out, True, monochrome) for env, opt, is_colour in [ # NO_COLOR='' should be as if no NO_COLOR ['', '--color=auto', True], # NO_COLOR='1': we expect no colour ['1', '--color=auto', False], # NO_COLOR='no': we still expect no colour ['no', '--color=auto', False], # NO_COLOR=' ', alias for 'auto' [' ', '--color=tty', False], # NO_COLOR=' ', alias for 'auto' [' ', '--color=if-tty', False], # NO_COLOR='', alias for 'auto' ['', '--color=tty', True], # NO_COLOR='', alias for 'no' ['', '--color=never', False], # NO_COLOR='x', alias for 'yes' (--color=yes wins) ['x', '--color=force', True], ]: os.environ['NO_COLOR'] = env try: result, out, err = self.runsubcmd("visualize", "ntdsconn", '-H', self.dburl, '-S', opt) except SystemExit: # optparse makes us do this self.fail(f"optparse rejects {env}, {opt}, {is_colour}") self.assertCmdSuccess(result, out, err) self.assert_colour(out, is_colour, monochrome) # with "-o -" output filename alias for stdout. result, out, err = self.runsubcmd("visualize", "ntdsconn", '-H', self.dburl, '-S', opt, '-o', '-') self.assertCmdSuccess(result, out, err) self.assert_colour(out, is_colour, monochrome) finally: cls.stringIO = StringIO if old_no_color is None: os.environ.pop('NO_COLOR', None) else: os.environ['NO_COLOR'] = old_no_color def test_import_ldif_xdot(self): """We can't test actual xdot, but using the environment we can persuade samba-tool that a script we write is xdot and ensure it gets the right text. """ result, expected, err = self.runsubcmd("visualize", "ntdsconn", '-H', self.dburl, '--color=no', '-S', '--dot') self.assertCmdSuccess(result, expected, err) # not that we're expecting anything here old_xdot_path = os.environ.get('SAMBA_TOOL_XDOT_PATH') tmpdir = tempfile.mkdtemp() fake_xdot = os.path.join(tmpdir, 'fake_xdot') content = os.path.join(tmpdir, 'content') f = open(fake_xdot, 'w') print('#!/bin/sh', file=f) print('cp $1 %s' % content, file=f) f.close() os.chmod(fake_xdot, 0o700) os.environ['SAMBA_TOOL_XDOT_PATH'] = fake_xdot result, empty, err = self.runsubcmd("visualize", "ntdsconn", '--importldif', MULTISITE_LDIF, '--color=no', '-S', '--xdot') f = open(content) xdot = f.read() f.close() os.remove(fake_xdot) os.remove(content) os.rmdir(tmpdir) if old_xdot_path is not None: os.environ['SAMBA_TOOL_XDOT_PATH'] = old_xdot_path else: del os.environ['SAMBA_TOOL_XDOT_PATH'] self.assertCmdSuccess(result, xdot, err) self.assertStringsEqual(expected, xdot, strip=True) def test_import_ldif(self): """Make sure the samba-tool visualize --importldif option gives the same output as using the externally generated db from the same LDIF.""" result, s1, err = self.runsubcmd("visualize", "ntdsconn", '-H', self.dburl, '--color=no', '-S') self.assertCmdSuccess(result, s1, err) result, s2, err = self.runsubcmd("visualize", "ntdsconn", '--importldif', MULTISITE_LDIF, '--color=no', '-S') self.assertCmdSuccess(result, s2, err) self.assertStringsEqual(s1, s2) 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-yes') 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, except # for the title line. result, target, err = self.runsubcmd("visualize", "ntdsconn", '-H', self.dburl, '--color=no', '-S') self.assertCmdSuccess(result, target, err) files = [] target = target.strip().split('\n', 1)[1] 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) # Separate out the title line, which will differ in the DN. title, body = out.strip().split('\n', 1) self.assertStringsEqual(target, body) self.assertIn(cn, title) 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 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) 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) EXPECTED_DOT_MULTISITE_NO_KEY = r"""/* generated by samba */ digraph A_samba_tool_production { label="NTDS Connections known to CN=WIN01,CN=Servers,CN=Default-First-Site-Name,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=LOCALDC,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=LOCALDC,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 """