1 # -*- coding: utf-8 -*-
2 # Tests for samba-tool visualize
3 # Copyright (C) Andrew Bartlett 2015, 2018
5 # by Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
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.
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.
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/>.
20 """Tests for samba-tool visualize ntdsconn using the test ldif
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
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
36 MULTISITE_LDIF = os.path.join(os.environ['SRCDIR_ABS'],
37 "testdata/ldif-utils-test-multisite.ldif")
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")
44 DOMAIN = "DC=ad,DC=samba,DC=example,DC=com"
45 DN_TEMPLATE = "CN=%s,CN=Servers,CN=%s,CN=Sites,CN=Configuration," + DOMAIN
47 MULTISITE_LDIF_DSAS = [
48 ("WIN01", "Default-First-Site-Name"),
61 def samdb_from_ldif(ldif, tempdir, lp, dsa=None, tag=''):
63 dsa_name = 'default-DSA'
66 dburl = os.path.join(tempdir,
67 ("ldif-to-sambdb-%s-%s" %
69 samdb = ldif_import_export.ldif_to_samdb(dburl, lp, ldif,
74 def collapse_space(s):
76 for line in s.splitlines():
77 line = ' '.join(line.strip().split())
79 return '\n'.join(lines)
82 class SambaToolVisualizeLdif(SambaToolCmdTest):
84 super(SambaToolVisualizeLdif, self).setUp()
86 self.samdb, self.dbfile = samdb_from_ldif(MULTISITE_LDIF,
89 self.dburl = 'tdb://' + self.dbfile
92 self.remove_files(self.dbfile)
93 super(SambaToolVisualizeLdif, self).tearDown()
95 def remove_files(self, *files):
97 self.assertTrue(f.startswith(self.tempdir))
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
104 colour_re = re.compile('\033' r'\[[\d;]+m')
105 result, monochrome, err = self.runsubcmd("visualize", "ntdsconn",
108 self.assertCmdSuccess(result, monochrome, err)
109 self.assertFalse(colour_re.findall(monochrome))
111 colour_args = [['--color=yes']]
112 colour_args += [['--color-scheme', x] for x in COLOUR_SETS
115 for args in colour_args:
116 result, out, err = self.runsubcmd("visualize", "ntdsconn",
119 self.assertCmdSuccess(result, out, err)
120 self.assertTrue(colour_re.search(out))
121 uncoloured = colour_re.sub('', out)
123 self.assertStringsEqual(monochrome, uncoloured, strip=True)
125 def test_output_file(self):
126 """Check that writing to a file works, with and without
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",
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()
138 color_auto_file = os.path.join(self.tempdir, 'color-auto')
140 result, out, err = self.runsubcmd("visualize", "ntdsconn",
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()
150 self.assertStringsEqual(color_auto, expected, strip=True)
151 self.remove_files(color_auto_file)
153 color_no_file = os.path.join(self.tempdir, 'color-no')
154 result, out, err = self.runsubcmd("visualize", "ntdsconn",
158 self.assertCmdSuccess(result, out, err)
159 self.assertEqual(out, '')
160 f = open(color_no_file)
163 self.remove_files(color_no_file)
165 self.assertStringsEqual(color_no, expected, strip=True)
167 color_yes_file = os.path.join(self.tempdir, 'color-no')
168 result, out, err = self.runsubcmd("visualize", "ntdsconn",
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()
177 self.assertNotEqual(colour_yes.strip(), expected)
179 self.remove_files(color_yes_file)
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",
188 self.assertCmdSuccess(result, out, err)
189 self.assertEqual((out.strip() == expected), equal)
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",
196 '--color=no', '-S', '--utf8')
197 self.assertCmdSuccess(result, utf8, err)
199 result, ascii, err = self.runsubcmd("visualize", "ntdsconn",
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)
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",
213 self.assertCmdSuccess(result, target, err)
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,
223 result, out, err = self.runsubcmd("visualize", "ntdsconn",
224 '-H', 'tdb://' + dbfile,
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)
232 self.remove_files(*files)
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",
239 '--color=no', '-S', '--no-key')
240 self.assertCmdSuccess(result, short, err)
241 result, long, err = self.runsubcmd("visualize", "ntdsconn",
243 '--color=no', '--no-key')
244 self.assertCmdSuccess(result, long, err)
246 lines = short.split('\n')
249 short_without_key = []
251 m = re.match(r"'(.{1,2})' stands for '(.+)'", line)
254 replacements.append((len(a), a, b))
255 key_lines.append(line)
257 short_without_key.append(line)
259 short = '\n'.join(short_without_key)
260 # we need to replace longest strings first
261 replacements.sort(reverse=True)
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)
269 long2short = '%s\n%s' % (long_header, long2short)
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)
277 self.assertStringsEqual(short2long, long, strip=True)
278 self.assertStringsEqual(short, long2short, strip=True)
280 def test_disconnected_ldif_with_key(self):
281 """Test that the 'unconnected' ldif shows up and exactly matches the
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
288 samdb, dbfile = samdb_from_ldif(UNCONNECTED_LDIF,
290 self.lp, tag='disconnected')
291 dburl = 'tdb://' + dbfile
293 result, output, err = self.runsubcmd("visualize", "ntdsconn",
296 self.remove_files(dbfile)
297 self.assertCmdSuccess(result, output, err)
298 self.assertStringsEqual(output,
299 EXPECTED_DISTANCE_GRAPH_WITH_KEY)
301 def test_dot_ntdsconn(self):
302 """Graphviz NTDS Connection output"""
303 result, dot, err = self.runsubcmd("visualize", "ntdsconn",
305 '--color=no', '-S', '--dot',
307 self.assertCmdSuccess(result, dot, err)
308 self.assertStringsEqual(EXPECTED_DOT_MULTISITE_NO_KEY, dot)
310 def test_dot_ntdsconn_disconnected(self):
311 """Graphviz NTDS Connection output from disconnected graph"""
312 samdb, dbfile = samdb_from_ldif(UNCONNECTED_LDIF,
314 self.lp, tag='disconnected')
316 result, dot, err = self.runsubcmd("visualize", "ntdsconn",
317 '-H', 'tdb://' + dbfile,
318 '--color=no', '-S', '--dot',
320 self.assertCmdSuccess(result, dot, err)
321 self.remove_files(dbfile)
324 self.assertStringsEqual(EXPECTED_DOT_NTDSCONN_DISCONNECTED, dot,
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,
331 self.lp, tag='disconnected')
333 dot_file = os.path.join(self.tempdir, 'dotfile')
335 result, dot, err = self.runsubcmd("visualize", "ntdsconn",
336 '-H', 'tdb://' + dbfile,
337 '--color=no', '-S', '--dot',
339 self.assertCmdSuccess(result, dot, err)
343 self.assertStringsEqual(EXPECTED_DOT_NTDSCONN_DISCONNECTED, dot)
345 self.remove_files(dbfile, dot_file)
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";
353 node[fontname=Helvetica; fontsize=10];
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", ];
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";
393 node[fontname=Helvetica; fontsize=10];
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 {
409 subgraph cluster_key_nodes {
414 subgraph cluster_key_edges {
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"]
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"]
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]
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
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
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'
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