41d318beb46cd9f1a5f97d4fc080f96f25cb78cf
[samba.git] / python / samba / tests / samba_tool / visualize.py
1 # -*- coding: utf-8 -*-
2 # Tests for samba-tool visualize
3 # Copyright (C) Andrew Bartlett 2015, 2018
4 #
5 # by Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 """Tests for samba-tool visualize ntdsconn using the test ldif
21 topologies.
22
23 We don't test samba-tool visualize reps here because repsTo and
24 repsFrom are not replicated, and there are no actual remote servers to
25 query.
26 """
27 import os
28 import tempfile
29 import re
30 from io import StringIO
31 from samba.tests.samba_tool.base import SambaToolCmdTest
32 from samba.kcc import ldif_import_export
33 from samba.graph import COLOUR_SETS
34 from samba.param import LoadParm
35
36 MULTISITE_LDIF = os.path.join(os.environ['SRCDIR_ABS'],
37                               "testdata/ldif-utils-test-multisite.ldif")
38
39 # UNCONNECTED_LDIF is a single site, unconnected 5DC database that was
40 # created using samba-tool domain join in testenv.
41 UNCONNECTED_LDIF = os.path.join(os.environ['SRCDIR_ABS'],
42                                 "testdata/unconnected-intrasite.ldif")
43
44 DOMAIN = "DC=ad,DC=samba,DC=example,DC=com"
45 DN_TEMPLATE = "CN=%s,CN=Servers,CN=%s,CN=Sites,CN=Configuration," + DOMAIN
46
47 MULTISITE_LDIF_DSAS = [
48     ("WIN01", "Default-First-Site-Name"),
49     ("WIN08", "Site-4"),
50     ("WIN07", "Site-4"),
51     ("WIN06", "Site-3"),
52     ("WIN09", "Site-5"),
53     ("WIN10", "Site-5"),
54     ("WIN02", "Site-2"),
55     ("WIN04", "Site-2"),
56     ("WIN03", "Site-2"),
57     ("WIN05", "Site-2"),
58 ]
59
60
61 class StringIOThinksItIsATTY(StringIO):
62     """A StringIO that claims to be a TTY for testing --color=auto,
63     by switching the stringIO class attribute."""
64     def isatty(self):
65         return True
66
67
68 def samdb_from_ldif(ldif, tempdir, lp, dsa=None, tag=''):
69     if dsa is None:
70         dsa_name = 'default-DSA'
71     else:
72         dsa_name = dsa[:5]
73     dburl = os.path.join(tempdir,
74                          ("ldif-to-sambdb-%s-%s" %
75                           (tag, dsa_name)))
76     samdb = ldif_import_export.ldif_to_samdb(dburl, lp, ldif,
77                                              forced_local_dsa=dsa)
78     return (samdb, dburl)
79
80
81 def collapse_space(s, keep_empty_lines=False):
82     lines = []
83     for line in s.splitlines():
84         line = ' '.join(line.strip().split())
85         if line or keep_empty_lines:
86             lines.append(line)
87     return '\n'.join(lines)
88
89
90 class SambaToolVisualizeLdif(SambaToolCmdTest):
91     def setUp(self):
92         super(SambaToolVisualizeLdif, self).setUp()
93         self.lp = LoadParm()
94         self.samdb, self.dbfile = samdb_from_ldif(MULTISITE_LDIF,
95                                                   self.tempdir,
96                                                   self.lp)
97         self.dburl = 'tdb://' + self.dbfile
98
99     def tearDown(self):
100         self.remove_files(self.dbfile)
101         super(SambaToolVisualizeLdif, self).tearDown()
102
103     def remove_files(self, *files):
104         for f in files:
105             self.assertTrue(f.startswith(self.tempdir))
106             os.unlink(f)
107
108     def test_colour(self):
109         """Ensure the colour output is the same as the monochrome output
110         EXCEPT for the colours, of which the monochrome one should
111         know nothing."""
112         colour_re = re.compile('\033' r'\[[\d;]+m')
113         result, monochrome, err = self.runsubcmd("visualize", "ntdsconn",
114                                                  '-H', self.dburl,
115                                                  '--color=no', '-S')
116         self.assertCmdSuccess(result, monochrome, err)
117         self.assertFalse(colour_re.findall(monochrome))
118
119         colour_args = [['--color=yes']]
120         colour_args += [['--color-scheme', x] for x in COLOUR_SETS
121                         if x is not None]
122
123         for args in colour_args:
124             result, out, err = self.runsubcmd("visualize", "ntdsconn",
125                                               '-H', self.dburl,
126                                               '-S', *args)
127             self.assertCmdSuccess(result, out, err)
128             self.assertTrue(colour_re.search(out),
129                             f"'{' '.join(args)}' should be colour")
130             uncoloured = colour_re.sub('', out)
131
132             self.assertStringsEqual(monochrome, uncoloured, strip=True)
133
134     def assert_colour(self, text, has_colour=True, monochrome=None):
135         colour_re = re.compile('\033' r'\[[\d;]+m')
136         found = colour_re.search(text)
137         if has_colour:
138             self.assertTrue(found, text)
139         else:
140             self.assertFalse(found, text)
141         if monochrome is not None:
142             uncoloured = colour_re.sub('', text)
143             self.assertStringsEqual(monochrome, uncoloured, strip=True)
144
145     def test_colour_auto_tty(self):
146         """Assert the behaviour of --colour=auto with and without
147         NO_COLOUR on a fake tty"""
148         result, monochrome, err = self.runsubcmd("visualize", "ntdsconn",
149                                                  '-H', self.dburl,
150                                                  '--color=no', '-S')
151         self.assertCmdSuccess(result, monochrome, err)
152         self.assert_colour(monochrome, False)
153         cls = self.__class__
154
155         try:
156             cls.stringIO = StringIOThinksItIsATTY
157             old_no_color = os.environ.pop('NO_COLOR', None)
158             # First with no NO_COLOR env var. There should be colour.
159             result, out, err = self.runsubcmd("visualize", "ntdsconn",
160                                               '-H', self.dburl,
161                                               '-S',
162                                               '--color=auto')
163             self.assertCmdSuccess(result, out, err)
164             self.assert_colour(out, True, monochrome)
165
166             for env, opt, is_colour in [
167                     # NO_COLOR='' should be as if no NO_COLOR
168                     ['', '--color=auto', True],
169                     # NO_COLOR='1': we expect no colour
170                     ['1', '--color=auto', False],
171                     # NO_COLOR='no': we still expect no colour
172                     ['no', '--color=auto', False],
173                     # NO_COLOR=' ', alias for 'auto'
174                     [' ', '--color=tty', False],
175                     # NO_COLOR=' ', alias for 'auto'
176                     [' ', '--color=if-tty', False],
177                     # NO_COLOR='', alias for 'auto'
178                     ['', '--color=tty', True],
179                     # NO_COLOR='', alias for 'no'
180                     ['', '--color=never', False],
181                     # NO_COLOR='x', alias for 'yes' (--color=yes wins)
182                     ['x', '--color=force', True],
183                     ]:
184                 os.environ['NO_COLOR'] = env
185
186                 try:
187                     result, out, err = self.runsubcmd("visualize", "ntdsconn",
188                                                       '-H', self.dburl,
189                                                       '-S',
190                                                       opt)
191                 except SystemExit:
192                     # optparse makes us do this
193                     self.fail(f"optparse rejects {env}, {opt}, {is_colour}")
194
195                 self.assertCmdSuccess(result, out, err)
196                 self.assert_colour(out, is_colour, monochrome)
197
198                 # with "-o -" output filename alias for stdout.
199                 result, out, err = self.runsubcmd("visualize", "ntdsconn",
200                                                   '-H', self.dburl,
201                                                   '-S',
202                                                   opt,
203                                                   '-o', '-')
204                 self.assertCmdSuccess(result, out, err)
205                 self.assert_colour(out, is_colour, monochrome)
206
207         finally:
208             cls.stringIO = StringIO
209             if old_no_color is None:
210                 os.environ.pop('NO_COLOR', None)
211             else:
212                 os.environ['NO_COLOR'] = old_no_color
213
214     def test_import_ldif_xdot(self):
215         """We can't test actual xdot, but using the environment we can
216         persuade samba-tool that a script we write is xdot and ensure
217         it gets the right text.
218         """
219         result, expected, err = self.runsubcmd("visualize", "ntdsconn",
220                                                '-H', self.dburl,
221                                                '--color=no', '-S',
222                                                '--dot')
223         self.assertCmdSuccess(result, expected, err)
224
225         # not that we're expecting anything here
226         old_xdot_path = os.environ.get('SAMBA_TOOL_XDOT_PATH')
227
228         tmpdir = tempfile.mkdtemp()
229         fake_xdot = os.path.join(tmpdir, 'fake_xdot')
230         content = os.path.join(tmpdir, 'content')
231         f = open(fake_xdot, 'w')
232         print('#!/bin/sh', file=f)
233         print('cp $1 %s' % content, file=f)
234         f.close()
235         os.chmod(fake_xdot, 0o700)
236
237         os.environ['SAMBA_TOOL_XDOT_PATH'] = fake_xdot
238         result, empty, err = self.runsubcmd("visualize", "ntdsconn",
239                                             '--importldif', MULTISITE_LDIF,
240                                             '--color=no', '-S',
241                                             '--xdot')
242
243         f = open(content)
244         xdot = f.read()
245         f.close()
246         os.remove(fake_xdot)
247         os.remove(content)
248         os.rmdir(tmpdir)
249
250         if old_xdot_path is not None:
251             os.environ['SAMBA_TOOL_XDOT_PATH'] = old_xdot_path
252         else:
253             del os.environ['SAMBA_TOOL_XDOT_PATH']
254
255         self.assertCmdSuccess(result, xdot, err)
256         self.assertStringsEqual(expected, xdot, strip=True)
257
258     def test_import_ldif(self):
259         """Make sure the samba-tool visualize --importldif option gives the
260         same output as using the externally generated db from the same
261         LDIF."""
262         result, s1, err = self.runsubcmd("visualize", "ntdsconn",
263                                          '-H', self.dburl,
264                                          '--color=no', '-S')
265         self.assertCmdSuccess(result, s1, err)
266
267         result, s2, err = self.runsubcmd("visualize", "ntdsconn",
268                                          '--importldif', MULTISITE_LDIF,
269                                          '--color=no', '-S')
270         self.assertCmdSuccess(result, s2, err)
271
272         self.assertStringsEqual(s1, s2)
273
274     def test_output_file(self):
275         """Check that writing to a file works, with and without
276         --color=auto."""
277         # NOTE, we can't really test --color=auto works with a TTY.
278         colour_re = re.compile('\033' r'\[[\d;]+m')
279         result, expected, err = self.runsubcmd("visualize", "ntdsconn",
280                                                '-H', self.dburl,
281                                                '--color=auto', '-S')
282         self.assertCmdSuccess(result, expected, err)
283         # Not a TTY, so stdout output should be colourless
284         self.assertFalse(colour_re.search(expected))
285         expected = expected.strip()
286
287         color_auto_file = os.path.join(self.tempdir, 'color-auto')
288
289         result, out, err = self.runsubcmd("visualize", "ntdsconn",
290                                           '-H', self.dburl,
291                                           '--color=auto', '-S',
292                                           '-o', color_auto_file)
293         self.assertCmdSuccess(result, out, err)
294         # We wrote to file, so stdout should be empty
295         self.assertEqual(out, '')
296         f = open(color_auto_file)
297         color_auto = f.read()
298         f.close()
299         self.assertStringsEqual(color_auto, expected, strip=True)
300         self.remove_files(color_auto_file)
301
302         color_no_file = os.path.join(self.tempdir, 'color-no')
303         result, out, err = self.runsubcmd("visualize", "ntdsconn",
304                                           '-H', self.dburl,
305                                           '--color=no', '-S',
306                                           '-o', color_no_file)
307         self.assertCmdSuccess(result, out, err)
308         self.assertEqual(out, '')
309         f = open(color_no_file)
310         color_no = f.read()
311         f.close()
312         self.remove_files(color_no_file)
313
314         self.assertStringsEqual(color_no, expected, strip=True)
315
316         color_yes_file = os.path.join(self.tempdir, 'color-yes')
317         result, out, err = self.runsubcmd("visualize", "ntdsconn",
318                                           '-H', self.dburl,
319                                           '--color=yes', '-S',
320                                           '-o', color_yes_file)
321         self.assertCmdSuccess(result, out, err)
322         self.assertEqual(out, '')
323         f = open(color_yes_file)
324         colour_yes = f.read()
325         f.close()
326         self.assertNotEqual(colour_yes.strip(), expected)
327
328         self.remove_files(color_yes_file)
329
330         # Try the magic filename "-", meaning stdout.
331         # This doesn't exercise the case when stdout is a TTY
332         for c, equal in [('no', True), ('auto', True), ('yes', False)]:
333             result, out, err = self.runsubcmd("visualize", "ntdsconn",
334                                               '-H', self.dburl,
335                                               '--color', c,
336                                               '-S', '-o', '-')
337             self.assertCmdSuccess(result, out, err)
338             self.assertEqual((out.strip() == expected), equal)
339
340     def test_utf8(self):
341         """Ensure that --utf8 adds at least some expected utf-8, and that it
342         isn't there without --utf8."""
343         result, utf8, err = self.runsubcmd("visualize", "ntdsconn",
344                                            '-H', self.dburl,
345                                            '--color=no', '-S', '--utf8')
346         self.assertCmdSuccess(result, utf8, err)
347
348         result, ascii, err = self.runsubcmd("visualize", "ntdsconn",
349                                             '-H', self.dburl,
350                                             '--color=no', '-S')
351         self.assertCmdSuccess(result, ascii, err)
352         for c in ('│', '─', '╭'):
353             self.assertTrue(c in utf8, 'UTF8 should contain %s' % c)
354             self.assertTrue(c not in ascii, 'ASCII should not contain %s' % c)
355
356     def test_forced_local_dsa(self):
357         # the forced_local_dsa shouldn't make any difference, except
358         # for the title line.
359         result, target, err = self.runsubcmd("visualize", "ntdsconn",
360                                              '-H', self.dburl,
361                                              '--color=no', '-S')
362         self.assertCmdSuccess(result, target, err)
363         files = []
364         target = target.strip().split('\n', 1)[1]
365         for cn, site in MULTISITE_LDIF_DSAS:
366             dsa = DN_TEMPLATE % (cn, site)
367             samdb, dbfile = samdb_from_ldif(MULTISITE_LDIF,
368                                             self.tempdir,
369                                             self.lp, dsa,
370                                             tag=cn)
371
372             result, out, err = self.runsubcmd("visualize", "ntdsconn",
373                                               '-H', 'tdb://' + dbfile,
374                                               '--color=no', '-S')
375             self.assertCmdSuccess(result, out, err)
376             # Separate out the title line, which will differ in the DN.
377             title, body = out.strip().split('\n', 1)
378             self.assertStringsEqual(target, body)
379             self.assertIn(cn, title)
380             files.append(dbfile)
381         self.remove_files(*files)
382
383     def test_short_names(self):
384         """Ensure the colour ones are the same as the monochrome ones EXCEPT
385         for the colours, of which the monochrome one should know nothing"""
386         result, short, err = self.runsubcmd("visualize", "ntdsconn",
387                                             '-H', self.dburl,
388                                             '--color=no', '-S', '--no-key')
389         self.assertCmdSuccess(result, short, err)
390         result, long, err = self.runsubcmd("visualize", "ntdsconn",
391                                            '-H', self.dburl,
392                                            '--color=no', '--no-key')
393         self.assertCmdSuccess(result, long, err)
394
395         lines = short.split('\n')
396         replacements = []
397         key_lines = ['']
398         short_without_key = []
399         for line in lines:
400             m = re.match(r"'(.{1,2})' stands for '(.+)'", line)
401             if m:
402                 a, b = m.groups()
403                 replacements.append((len(a), a, b))
404                 key_lines.append(line)
405             else:
406                 short_without_key.append(line)
407
408         short = '\n'.join(short_without_key)
409         # we need to replace longest strings first
410         replacements.sort(reverse=True)
411         short2long = short
412         # we don't want to shorten the DC name in the header line.
413         long_header, long2short = long.strip().split('\n', 1)
414         for _, a, b in replacements:
415             short2long = short2long.replace(a, b)
416             long2short = long2short.replace(b, a)
417
418         long2short = '%s\n%s' % (long_header, long2short)
419
420         # The white space is going to be all wacky, so lets squish it down
421         short2long = collapse_space(short2long)
422         long2short = collapse_space(long2short)
423         short = collapse_space(short)
424         long = collapse_space(long)
425
426         self.assertStringsEqual(short2long, long, strip=True)
427         self.assertStringsEqual(short, long2short, strip=True)
428
429     def test_disconnected_ldif_with_key(self):
430         """Test that the 'unconnected' ldif shows up and exactly matches the
431         expected output."""
432         # This is not truly a disconnected graph because the
433         # vampre/local/promoted DCs are in there and they have
434         # relationships, and SERVER2 and SERVER3 for some reason refer
435         # to them.
436
437         samdb, dbfile = samdb_from_ldif(UNCONNECTED_LDIF,
438                                         self.tempdir,
439                                         self.lp, tag='disconnected')
440         dburl = 'tdb://' + dbfile
441         result, output, err = self.runsubcmd("visualize", "ntdsconn",
442                                              '-H', dburl,
443                                              '--color=no', '-S')
444         self.remove_files(dbfile)
445         self.assertCmdSuccess(result, output, err)
446         self.assertStringsEqual(output,
447                                 EXPECTED_DISTANCE_GRAPH_WITH_KEY)
448
449     def test_dot_ntdsconn(self):
450         """Graphviz NTDS Connection output"""
451         result, dot, err = self.runsubcmd("visualize", "ntdsconn",
452                                           '-H', self.dburl,
453                                           '--color=no', '-S', '--dot',
454                                           '--no-key')
455         self.assertCmdSuccess(result, dot, err)
456         self.assertStringsEqual(EXPECTED_DOT_MULTISITE_NO_KEY, dot)
457
458     def test_dot_ntdsconn_disconnected(self):
459         """Graphviz NTDS Connection output from disconnected graph"""
460         samdb, dbfile = samdb_from_ldif(UNCONNECTED_LDIF,
461                                         self.tempdir,
462                                         self.lp, tag='disconnected')
463
464         result, dot, err = self.runsubcmd("visualize", "ntdsconn",
465                                           '-H', 'tdb://' + dbfile,
466                                           '--color=no', '-S', '--dot',
467                                           '-o', '-')
468         self.assertCmdSuccess(result, dot, err)
469         self.remove_files(dbfile)
470         self.assertStringsEqual(EXPECTED_DOT_NTDSCONN_DISCONNECTED, dot,
471                                 strip=True)
472
473     def test_dot_ntdsconn_disconnected_to_file(self):
474         """Graphviz NTDS Connection output into a file"""
475         samdb, dbfile = samdb_from_ldif(UNCONNECTED_LDIF,
476                                         self.tempdir,
477                                         self.lp, tag='disconnected')
478
479         dot_file = os.path.join(self.tempdir, 'dotfile')
480
481         result, dot, err = self.runsubcmd("visualize", "ntdsconn",
482                                           '-H', 'tdb://' + dbfile,
483                                           '--color=no', '-S', '--dot',
484                                           '-o', dot_file)
485         self.assertCmdSuccess(result, dot, err)
486         f = open(dot_file)
487         dot = f.read()
488         f.close()
489         self.assertStringsEqual(EXPECTED_DOT_NTDSCONN_DISCONNECTED, dot)
490
491         self.remove_files(dbfile, dot_file)
492
493
494 EXPECTED_DOT_MULTISITE_NO_KEY = r"""/* generated by samba */
495 digraph A_samba_tool_production {
496 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";
497 fontsize=10;
498
499 node[fontname=Helvetica; fontsize=10];
500
501 "CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n...";
502 "CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n...";
503 "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n...";
504 "CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n...";
505 "CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n...";
506 "CN=NTDS Settings,\nCN=WIN06,\nCN=Servers,\nCN=Site-3,\n...";
507 "CN=NTDS Settings,\nCN=WIN07,\nCN=Servers,\nCN=Site-4,\n...";
508 "CN=NTDS Settings,\nCN=WIN08,\nCN=Servers,\nCN=Site-4,\n...";
509 "CN=NTDS Settings,\nCN=WIN09,\nCN=Servers,\nCN=Site-5,\n...";
510 "CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n...";
511 "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", ];
512 "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", ];
513 "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", ];
514 "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", ];
515 "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", ];
516 "CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
517 "CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
518 "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
519 "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
520 "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", ];
521 "CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
522 "CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
523 "CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
524 "CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
525 "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", ];
526 "CN=NTDS Settings,\nCN=WIN09,\nCN=Servers,\nCN=Site-5,\n..." -> "CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n..." [color="#000000", ];
527 "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", ];
528 "CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n..." -> "CN=NTDS Settings,\nCN=WIN09,\nCN=Servers,\nCN=Site-5,\n..." [color="#000000", ];
529 }
530
531 """
532
533
534 EXPECTED_DOT_NTDSCONN_DISCONNECTED = r"""/* generated by samba */
535 digraph A_samba_tool_production {
536 label="NTDS Connections known to CN=LOCALDC,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com";
537 fontsize=10;
538
539 node[fontname=Helvetica; fontsize=10];
540
541 "CN=NTDS Settings,\nCN=CLIENT,\n...";
542 "CN=NTDS Settings,\nCN=LOCALDC,\n...";
543 "CN=NTDS Settings,\nCN=PROMOTEDVDC,\n...";
544 "CN=NTDS Settings,\nCN=SERVER1,\n...";
545 "CN=NTDS Settings,\nCN=SERVER2,\n...";
546 "CN=NTDS Settings,\nCN=SERVER3,\n...";
547 "CN=NTDS Settings,\nCN=SERVER4,\n...";
548 "CN=NTDS Settings,\nCN=SERVER5,\n...";
549 "CN=NTDS Settings,\nCN=LOCALDC,\n..." -> "CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." [color="#000000", ];
550 "CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." -> "CN=NTDS Settings,\nCN=LOCALDC,\n..." [color="#000000", ];
551 "CN=NTDS Settings,\nCN=SERVER2,\n..." -> "CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." [color="#000000", ];
552 "CN=NTDS Settings,\nCN=SERVER3,\n..." -> "CN=NTDS Settings,\nCN=LOCALDC,\n..." [color="#000000", ];
553 subgraph cluster_key {
554 label="Key";
555 subgraph cluster_key_nodes {
556 label="";
557 color = "invis";
558
559 }
560 subgraph cluster_key_edges {
561 label="";
562 color = "invis";
563 subgraph cluster_key_0_ {
564 key_0_e1[label=src; color="#000000"; group="key_0__g"]
565 key_0_e2[label=dest; color="#000000"; group="key_0__g"]
566 key_0_e1 -> key_0_e2 [constraint = false; color="#000000"]
567 key_0__label[shape=plaintext; style=solid; width=2.000000; label="NTDS Connection\r"]
568 }
569 {key_0__label}
570 }
571
572 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"]
573
574 }
575 "CN=NTDS Settings,\nCN=CLIENT,\n..." -> key_0__label [style=invis];
576 "CN=NTDS Settings,\nCN=LOCALDC,\n..." -> key_0__label [style=invis];
577 "CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." -> key_0__label [style=invis];
578 "CN=NTDS Settings,\nCN=SERVER1,\n..." -> key_0__label [style=invis];
579 "CN=NTDS Settings,\nCN=SERVER2,\n..." -> key_0__label [style=invis];
580 "CN=NTDS Settings,\nCN=SERVER3,\n..." -> key_0__label [style=invis];
581 "CN=NTDS Settings,\nCN=SERVER4,\n..." -> key_0__label [style=invis];
582 "CN=NTDS Settings,\nCN=SERVER5,\n..." -> key_0__label [style=invis]
583 key_0__label -> elision0 [style=invis; weight=9]
584
585 }
586 """
587
588 EXPECTED_DISTANCE_GRAPH_WITH_KEY = """
589 NTDS Connections known to CN=LOCALDC,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com
590
591                             destination
592                   ,-------- *,CN=CLIENT+
593                   |,------- *,CN=LOCALDC+
594                   ||,------ *,CN=PROMOTEDVDC+
595                   |||,----- *,CN=SERVER1+
596                   ||||,---- *,CN=SERVER2+
597                   |||||,--- *,CN=SERVER3+
598                   ||||||,-- *,CN=SERVER4+
599            source |||||||,- *,CN=SERVER5+
600      *,CN=CLIENT+ 0-------
601     *,CN=LOCALDC+ -01-----
602 *,CN=PROMOTEDVDC+ -10-----
603     *,CN=SERVER1+ ---0----
604     *,CN=SERVER2+ -21-0---
605     *,CN=SERVER3+ -12--0--
606     *,CN=SERVER4+ ------0-
607     *,CN=SERVER5+ -------0
608
609 '*' stands for 'CN=NTDS Settings'
610 '+' stands for ',CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com'
611
612 Data can get from source to destination in the indicated number of steps.
613 0 means zero steps (it is the same DC)
614 1 means a direct link
615 2 means a transitive link involving two steps (i.e. one intermediate DC)
616 - means there is no connection, even through other DCs
617
618 """