sambatool tests: make assertMatch use assertIn
[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 actual remote servers to
25 query.
26 """
27
28 import samba
29 import os
30 import re
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 def samdb_from_ldif(ldif, tempdir, lp, dsa=None, tag=''):
62     if dsa is None:
63         dsa_name = 'default-DSA'
64     else:
65         dsa_name = dsa[:5]
66     dburl = os.path.join(tempdir,
67                          ("ldif-to-sambdb-%s-%s" %
68                           (tag, dsa_name)))
69     samdb = ldif_import_export.ldif_to_samdb(dburl, lp, ldif,
70                                              forced_local_dsa=dsa)
71     return (samdb, dburl)
72
73
74 def collapse_space(s):
75     lines = []
76     for line in s.splitlines():
77         line = ' '.join(line.strip().split())
78         lines.append(line)
79     return '\n'.join(lines)
80
81
82 class SambaToolVisualizeLdif(SambaToolCmdTest):
83     def setUp(self):
84         super(SambaToolVisualizeLdif, self).setUp()
85         self.lp = LoadParm()
86         self.samdb, self.dbfile = samdb_from_ldif(MULTISITE_LDIF,
87                                                   self.tempdir,
88                                                   self.lp)
89         self.dburl = 'tdb://' + self.dbfile
90
91     def tearDown(self):
92         self.remove_files(self.dbfile)
93         super(SambaToolVisualizeLdif, self).tearDown()
94
95     def remove_files(self, *files):
96         for f in files:
97             self.assertTrue(f.startswith(self.tempdir))
98             os.unlink(f)
99
100     def test_colour(self):
101         """Ensure the colour output is the same as the monochrome output
102         EXCEPT for the colours, of which the monochrome one should
103         know nothing."""
104         colour_re = re.compile('\033' r'\[[\d;]+m')
105         result, monochrome, err = self.runsubcmd("visualize", "ntdsconn",
106                                                  '-H', self.dburl,
107                                                  '--color=no', '-S')
108         self.assertCmdSuccess(result, monochrome, err)
109         self.assertFalse(colour_re.findall(monochrome))
110
111         colour_args = [['--color=yes']]
112         colour_args += [['--color-scheme', x] for x in COLOUR_SETS
113                         if x is not None]
114
115         for args in colour_args:
116             result, out, err = self.runsubcmd("visualize", "ntdsconn",
117                                               '-H', self.dburl,
118                                               '-S', *args)
119             self.assertCmdSuccess(result, out, err)
120             self.assertTrue(colour_re.search(out))
121             uncoloured = colour_re.sub('', out)
122
123             self.assertStringsEqual(monochrome, uncoloured, strip=True)
124
125     def test_output_file(self):
126         """Check that writing to a file works, with and without
127         --color=auto."""
128         # NOTE, we can't really test --color=auto works with a TTY.
129         colour_re = re.compile('\033' r'\[[\d;]+m')
130         result, expected, err = self.runsubcmd("visualize", "ntdsconn",
131                                                '-H', self.dburl,
132                                                '--color=auto', '-S')
133         self.assertCmdSuccess(result, expected, err)
134         # Not a TTY, so stdout output should be colourless
135         self.assertFalse(colour_re.search(expected))
136         expected = expected.strip()
137
138         color_auto_file = os.path.join(self.tempdir, 'color-auto')
139
140         result, out, err = self.runsubcmd("visualize", "ntdsconn",
141                                           '-H', self.dburl,
142                                           '--color=auto', '-S',
143                                           '-o', color_auto_file)
144         self.assertCmdSuccess(result, out, err)
145         # We wrote to file, so stdout should be empty
146         self.assertEqual(out, '')
147         f = open(color_auto_file)
148         color_auto = f.read()
149         f.close()
150         self.assertStringsEqual(color_auto, expected, strip=True)
151         self.remove_files(color_auto_file)
152
153         color_no_file = os.path.join(self.tempdir, 'color-no')
154         result, out, err = self.runsubcmd("visualize", "ntdsconn",
155                                           '-H', self.dburl,
156                                           '--color=no', '-S',
157                                           '-o', color_no_file)
158         self.assertCmdSuccess(result, out, err)
159         self.assertEqual(out, '')
160         f = open(color_no_file)
161         color_no = f.read()
162         f.close()
163         self.remove_files(color_no_file)
164
165         self.assertStringsEqual(color_no, expected, strip=True)
166
167         color_yes_file = os.path.join(self.tempdir, 'color-no')
168         result, out, err = self.runsubcmd("visualize", "ntdsconn",
169                                           '-H', self.dburl,
170                                           '--color=yes', '-S',
171                                           '-o', color_yes_file)
172         self.assertCmdSuccess(result, out, err)
173         self.assertEqual(out, '')
174         f = open(color_yes_file)
175         colour_yes = f.read()
176         f.close()
177         self.assertNotEqual(colour_yes.strip(), expected)
178
179         self.remove_files(color_yes_file)
180
181         # Try the magic filename "-", meaning stdout.
182         # This doesn't exercise the case when stdout is a TTY
183         for c, equal in [('no', True), ('auto', True), ('yes', False)]:
184             result, out, err = self.runsubcmd("visualize", "ntdsconn",
185                                               '-H', self.dburl,
186                                               '--color', c,
187                                               '-S', '-o', '-')
188             self.assertCmdSuccess(result, out, err)
189             self.assertEqual((out.strip() == expected), equal)
190
191     def test_utf8(self):
192         """Ensure that --utf8 adds at least some expected utf-8, and that it
193         isn't there without --utf8."""
194         result, utf8, err = self.runsubcmd("visualize", "ntdsconn",
195                                            '-H', self.dburl,
196                                            '--color=no', '-S', '--utf8')
197         self.assertCmdSuccess(result, utf8, err)
198
199         result, ascii, err = self.runsubcmd("visualize", "ntdsconn",
200                                             '-H', self.dburl,
201                                             '--color=no', '-S')
202         self.assertCmdSuccess(result, ascii, err)
203         for c in ('│', '─', '╭'):
204             self.assertTrue(c in utf8, 'UTF8 should contain %s' % c)
205             self.assertTrue(c not in ascii, 'ASCII should not contain %s' % c)
206
207     def test_forced_local_dsa(self):
208         # the forced_local_dsa shouldn't make any difference, except
209         # for the title line.
210         result, target, err = self.runsubcmd("visualize", "ntdsconn",
211                                              '-H', self.dburl,
212                                              '--color=no', '-S')
213         self.assertCmdSuccess(result, target, err)
214         files = []
215         target = target.strip().split('\n', 1)[1]
216         for cn, site in MULTISITE_LDIF_DSAS:
217             dsa = DN_TEMPLATE % (cn, site)
218             samdb, dbfile = samdb_from_ldif(MULTISITE_LDIF,
219                                             self.tempdir,
220                                             self.lp, dsa,
221                                             tag=cn)
222
223             result, out, err = self.runsubcmd("visualize", "ntdsconn",
224                                               '-H', 'tdb://' + dbfile,
225                                               '--color=no', '-S')
226             self.assertCmdSuccess(result, out, err)
227             # Separate out the title line, which will differ in the DN.
228             title, body = out.strip().split('\n', 1)
229             self.assertStringsEqual(target, body)
230             self.assertIn(cn, title)
231             files.append(dbfile)
232         self.remove_files(*files)
233
234     def test_short_names(self):
235         """Ensure the colour ones are the same as the monochrome ones EXCEPT
236         for the colours, of which the monochrome one should know nothing"""
237         result, short, err = self.runsubcmd("visualize", "ntdsconn",
238                                             '-H', self.dburl,
239                                             '--color=no', '-S', '--no-key')
240         self.assertCmdSuccess(result, short, err)
241         result, long, err = self.runsubcmd("visualize", "ntdsconn",
242                                            '-H', self.dburl,
243                                            '--color=no', '--no-key')
244         self.assertCmdSuccess(result, long, err)
245
246         lines = short.split('\n')
247         replacements = []
248         key_lines = ['']
249         short_without_key = []
250         for line in lines:
251             m = re.match(r"'(.{1,2})' stands for '(.+)'", line)
252             if m:
253                 a, b = m.groups()
254                 replacements.append((len(a), a, b))
255                 key_lines.append(line)
256             else:
257                 short_without_key.append(line)
258
259         short = '\n'.join(short_without_key)
260         # we need to replace longest strings first
261         replacements.sort(reverse=True)
262         short2long = short
263         # we don't want to shorten the DC name in the header line.
264         long_header, long2short = long.strip().split('\n', 1)
265         for _, a, b in replacements:
266             short2long = short2long.replace(a, b)
267             long2short = long2short.replace(b, a)
268
269         long2short = '%s\n%s' % (long_header, long2short)
270
271         # The white space is going to be all wacky, so lets squish it down
272         short2long = collapse_space(short2long)
273         long2short = collapse_space(long2short)
274         short = collapse_space(short)
275         long = collapse_space(long)
276
277         self.assertStringsEqual(short2long, long, strip=True)
278         self.assertStringsEqual(short, long2short, strip=True)
279
280     def test_disconnected_ldif_with_key(self):
281         """Test that the 'unconnected' ldif shows up and exactly matches the
282         expected output."""
283         # This is not truly a disconnected graph because the
284         # vampre/local/promoted DCs are in there and they have
285         # relationships, and SERVER2 and SERVER3 for some reason refer
286         # to them.
287
288         samdb, dbfile = samdb_from_ldif(UNCONNECTED_LDIF,
289                                         self.tempdir,
290                                         self.lp, tag='disconnected')
291         dburl = 'tdb://' + dbfile
292         print(dbfile)
293         result, output, err = self.runsubcmd("visualize", "ntdsconn",
294                                              '-H', dburl,
295                                              '--color=no', '-S')
296         self.remove_files(dbfile)
297         self.assertCmdSuccess(result, output, err)
298         self.assertStringsEqual(output,
299                                 EXPECTED_DISTANCE_GRAPH_WITH_KEY)
300
301     def test_dot_ntdsconn(self):
302         """Graphviz NTDS Connection output"""
303         result, dot, err = self.runsubcmd("visualize", "ntdsconn",
304                                           '-H', self.dburl,
305                                           '--color=no', '-S', '--dot',
306                                           '--no-key')
307         self.assertCmdSuccess(result, dot, err)
308         self.assertStringsEqual(EXPECTED_DOT_MULTISITE_NO_KEY, dot)
309
310     def test_dot_ntdsconn_disconnected(self):
311         """Graphviz NTDS Connection output from disconnected graph"""
312         samdb, dbfile = samdb_from_ldif(UNCONNECTED_LDIF,
313                                         self.tempdir,
314                                         self.lp, tag='disconnected')
315
316         result, dot, err = self.runsubcmd("visualize", "ntdsconn",
317                                           '-H', 'tdb://' + dbfile,
318                                           '--color=no', '-S', '--dot',
319                                           '-o', '-')
320         self.assertCmdSuccess(result, dot, err)
321         self.remove_files(dbfile)
322         print(dot)
323
324         self.assertStringsEqual(EXPECTED_DOT_NTDSCONN_DISCONNECTED, dot,
325                                 strip=True)
326
327     def test_dot_ntdsconn_disconnected_to_file(self):
328         """Graphviz NTDS Connection output into a file"""
329         samdb, dbfile = samdb_from_ldif(UNCONNECTED_LDIF,
330                                         self.tempdir,
331                                         self.lp, tag='disconnected')
332
333         dot_file = os.path.join(self.tempdir, 'dotfile')
334
335         result, dot, err = self.runsubcmd("visualize", "ntdsconn",
336                                           '-H', 'tdb://' + dbfile,
337                                           '--color=no', '-S', '--dot',
338                                           '-o', dot_file)
339         self.assertCmdSuccess(result, dot, err)
340         f = open(dot_file)
341         dot = f.read()
342         f.close()
343         self.assertStringsEqual(EXPECTED_DOT_NTDSCONN_DISCONNECTED, dot)
344
345         self.remove_files(dbfile, dot_file)
346         print(dot)
347
348 EXPECTED_DOT_MULTISITE_NO_KEY = r"""/* generated by samba */
349 digraph A_samba_tool_production {
350 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";
351 fontsize=10;
352
353 node[fontname=Helvetica; fontsize=10];
354
355 "CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n...";
356 "CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n...";
357 "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n...";
358 "CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n...";
359 "CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n...";
360 "CN=NTDS Settings,\nCN=WIN06,\nCN=Servers,\nCN=Site-3,\n...";
361 "CN=NTDS Settings,\nCN=WIN07,\nCN=Servers,\nCN=Site-4,\n...";
362 "CN=NTDS Settings,\nCN=WIN08,\nCN=Servers,\nCN=Site-4,\n...";
363 "CN=NTDS Settings,\nCN=WIN09,\nCN=Servers,\nCN=Site-5,\n...";
364 "CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n...";
365 "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", ];
366 "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", ];
367 "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", ];
368 "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", ];
369 "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", ];
370 "CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
371 "CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
372 "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
373 "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
374 "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", ];
375 "CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
376 "CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
377 "CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
378 "CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
379 "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", ];
380 "CN=NTDS Settings,\nCN=WIN09,\nCN=Servers,\nCN=Site-5,\n..." -> "CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n..." [color="#000000", ];
381 "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", ];
382 "CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n..." -> "CN=NTDS Settings,\nCN=WIN09,\nCN=Servers,\nCN=Site-5,\n..." [color="#000000", ];
383 }
384
385 """
386
387
388 EXPECTED_DOT_NTDSCONN_DISCONNECTED = r"""/* generated by samba */
389 digraph A_samba_tool_production {
390 label="NTDS Connections known to CN=LOCALDC,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com";
391 fontsize=10;
392
393 node[fontname=Helvetica; fontsize=10];
394
395 "CN=NTDS Settings,\nCN=CLIENT,\n...";
396 "CN=NTDS Settings,\nCN=LOCALDC,\n...";
397 "CN=NTDS Settings,\nCN=PROMOTEDVDC,\n...";
398 "CN=NTDS Settings,\nCN=SERVER1,\n...";
399 "CN=NTDS Settings,\nCN=SERVER2,\n...";
400 "CN=NTDS Settings,\nCN=SERVER3,\n...";
401 "CN=NTDS Settings,\nCN=SERVER4,\n...";
402 "CN=NTDS Settings,\nCN=SERVER5,\n...";
403 "CN=NTDS Settings,\nCN=LOCALDC,\n..." -> "CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." [color="#000000", ];
404 "CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." -> "CN=NTDS Settings,\nCN=LOCALDC,\n..." [color="#000000", ];
405 "CN=NTDS Settings,\nCN=SERVER2,\n..." -> "CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." [color="#000000", ];
406 "CN=NTDS Settings,\nCN=SERVER3,\n..." -> "CN=NTDS Settings,\nCN=LOCALDC,\n..." [color="#000000", ];
407 subgraph cluster_key {
408 label="Key";
409 subgraph cluster_key_nodes {
410 label="";
411 color = "invis";
412
413 }
414 subgraph cluster_key_edges {
415 label="";
416 color = "invis";
417 subgraph cluster_key_0_ {
418 key_0_e1[label=src; color="#000000"; group="key_0__g"]
419 key_0_e2[label=dest; color="#000000"; group="key_0__g"]
420 key_0_e1 -> key_0_e2 [constraint = false; color="#000000"]
421 key_0__label[shape=plaintext; style=solid; width=2.000000; label="NTDS Connection\r"]
422 }
423 {key_0__label}
424 }
425
426 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"]
427
428 }
429 "CN=NTDS Settings,\nCN=CLIENT,\n..." -> key_0__label [style=invis];
430 "CN=NTDS Settings,\nCN=LOCALDC,\n..." -> key_0__label [style=invis];
431 "CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." -> key_0__label [style=invis];
432 "CN=NTDS Settings,\nCN=SERVER1,\n..." -> key_0__label [style=invis];
433 "CN=NTDS Settings,\nCN=SERVER2,\n..." -> key_0__label [style=invis];
434 "CN=NTDS Settings,\nCN=SERVER3,\n..." -> key_0__label [style=invis];
435 "CN=NTDS Settings,\nCN=SERVER4,\n..." -> key_0__label [style=invis];
436 "CN=NTDS Settings,\nCN=SERVER5,\n..." -> key_0__label [style=invis]
437 key_0__label -> elision0 [style=invis; weight=9]
438
439 }
440 """
441
442 EXPECTED_DISTANCE_GRAPH_WITH_KEY = """
443 NTDS Connections known to CN=LOCALDC,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com
444                             destination
445                   ,-------- *,CN=CLIENT+
446                   |,------- *,CN=LOCALDC+
447                   ||,------ *,CN=PROMOTEDVDC+
448                   |||,----- *,CN=SERVER1+
449                   ||||,---- *,CN=SERVER2+
450                   |||||,--- *,CN=SERVER3+
451                   ||||||,-- *,CN=SERVER4+
452            source |||||||,- *,CN=SERVER5+
453      *,CN=CLIENT+ 0-------
454     *,CN=LOCALDC+ -01-----
455 *,CN=PROMOTEDVDC+ -10-----
456     *,CN=SERVER1+ ---0----
457     *,CN=SERVER2+ -21-0---
458     *,CN=SERVER3+ -12--0--
459     *,CN=SERVER4+ ------0-
460     *,CN=SERVER5+ -------0
461
462 '*' stands for 'CN=NTDS Settings'
463 '+' stands for ',CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com'
464
465 Data can get from source to destination in the indicated number of steps.
466 0 means zero steps (it is the same DC)
467 1 means a direct link
468 2 means a transitive link involving two steps (i.e. one intermediate DC)
469 - means there is no connection, even through other DCs
470
471 """