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
27 from __future__ import print_function
32 from samba.tests.samba_tool.base import SambaToolCmdTest
33 from samba.kcc import ldif_import_export
34 from samba.graph import COLOUR_SETS
35 from samba.param import LoadParm
37 MULTISITE_LDIF = os.path.join(os.environ['SRCDIR_ABS'],
38 "testdata/ldif-utils-test-multisite.ldif")
40 # UNCONNECTED_LDIF is a single site, unconnected 5DC database that was
41 # created using samba-tool domain join in testenv.
42 UNCONNECTED_LDIF = os.path.join(os.environ['SRCDIR_ABS'],
43 "testdata/unconnected-intrasite.ldif")
45 DOMAIN = "DC=ad,DC=samba,DC=example,DC=com"
46 DN_TEMPLATE = "CN=%s,CN=Servers,CN=%s,CN=Sites,CN=Configuration," + DOMAIN
48 MULTISITE_LDIF_DSAS = [
49 ("WIN01", "Default-First-Site-Name"),
62 def samdb_from_ldif(ldif, tempdir, lp, dsa=None, tag=''):
64 dsa_name = 'default-DSA'
67 dburl = os.path.join(tempdir,
68 ("ldif-to-sambdb-%s-%s" %
70 samdb = ldif_import_export.ldif_to_samdb(dburl, lp, ldif,
75 def collapse_space(s, keep_empty_lines=False):
77 for line in s.splitlines():
78 line = ' '.join(line.strip().split())
79 if line or keep_empty_lines:
81 return '\n'.join(lines)
84 class SambaToolVisualizeLdif(SambaToolCmdTest):
86 super(SambaToolVisualizeLdif, self).setUp()
88 self.samdb, self.dbfile = samdb_from_ldif(MULTISITE_LDIF,
91 self.dburl = 'tdb://' + self.dbfile
94 self.remove_files(self.dbfile)
95 super(SambaToolVisualizeLdif, self).tearDown()
97 def remove_files(self, *files):
99 self.assertTrue(f.startswith(self.tempdir))
102 def test_colour(self):
103 """Ensure the colour output is the same as the monochrome output
104 EXCEPT for the colours, of which the monochrome one should
106 colour_re = re.compile('\033' r'\[[\d;]+m')
107 result, monochrome, err = self.runsubcmd("visualize", "ntdsconn",
110 self.assertCmdSuccess(result, monochrome, err)
111 self.assertFalse(colour_re.findall(monochrome))
113 colour_args = [['--color=yes']]
114 colour_args += [['--color-scheme', x] for x in COLOUR_SETS
117 for args in colour_args:
118 result, out, err = self.runsubcmd("visualize", "ntdsconn",
121 self.assertCmdSuccess(result, out, err)
122 self.assertTrue(colour_re.search(out))
123 uncoloured = colour_re.sub('', out)
125 self.assertStringsEqual(monochrome, uncoloured, strip=True)
127 def test_import_ldif_xdot(self):
128 """We can't test actual xdot, but using the environment we can
129 persuade samba-tool that a script we write is xdot and ensure
130 it gets the right text.
132 result, expected, err = self.runsubcmd("visualize", "ntdsconn",
136 self.assertCmdSuccess(result, expected, err)
138 # not that we're expecting anything here
139 old_xdot_path = os.environ.get('SAMBA_TOOL_XDOT_PATH')
141 tmpdir = tempfile.mkdtemp()
142 fake_xdot = os.path.join(tmpdir, 'fake_xdot')
143 content = os.path.join(tmpdir, 'content')
144 f = open(fake_xdot, 'w')
145 print('#!/bin/sh', file=f)
146 print('cp $1 %s' % content, file=f)
148 os.chmod(fake_xdot, 0o700)
150 os.environ['SAMBA_TOOL_XDOT_PATH'] = fake_xdot
151 result, empty, err = self.runsubcmd("visualize", "ntdsconn",
152 '--importldif', MULTISITE_LDIF,
163 if old_xdot_path is not None:
164 os.environ['SAMBA_TOOL_XDOT_PATH'] = old_xdot_path
166 del os.environ['SAMBA_TOOL_XDOT_PATH']
168 self.assertCmdSuccess(result, xdot, err)
169 self.assertStringsEqual(expected, xdot, strip=True)
171 def test_import_ldif(self):
172 """Make sure the samba-tool visualize --importldif option gives the
173 same output as using the externally generated db from the same
175 result, s1, err = self.runsubcmd("visualize", "ntdsconn",
178 self.assertCmdSuccess(result, s1, err)
180 result, s2, err = self.runsubcmd("visualize", "ntdsconn",
181 '--importldif', MULTISITE_LDIF,
183 self.assertCmdSuccess(result, s2, err)
185 self.assertStringsEqual(s1, s2)
187 def test_output_file(self):
188 """Check that writing to a file works, with and without
190 # NOTE, we can't really test --color=auto works with a TTY.
191 colour_re = re.compile('\033' r'\[[\d;]+m')
192 result, expected, err = self.runsubcmd("visualize", "ntdsconn",
194 '--color=auto', '-S')
195 self.assertCmdSuccess(result, expected, err)
196 # Not a TTY, so stdout output should be colourless
197 self.assertFalse(colour_re.search(expected))
198 expected = expected.strip()
200 color_auto_file = os.path.join(self.tempdir, 'color-auto')
202 result, out, err = self.runsubcmd("visualize", "ntdsconn",
204 '--color=auto', '-S',
205 '-o', color_auto_file)
206 self.assertCmdSuccess(result, out, err)
207 # We wrote to file, so stdout should be empty
208 self.assertEqual(out, '')
209 f = open(color_auto_file)
210 color_auto = f.read()
212 self.assertStringsEqual(color_auto, expected, strip=True)
213 self.remove_files(color_auto_file)
215 color_no_file = os.path.join(self.tempdir, 'color-no')
216 result, out, err = self.runsubcmd("visualize", "ntdsconn",
220 self.assertCmdSuccess(result, out, err)
221 self.assertEqual(out, '')
222 f = open(color_no_file)
225 self.remove_files(color_no_file)
227 self.assertStringsEqual(color_no, expected, strip=True)
229 color_yes_file = os.path.join(self.tempdir, 'color-no')
230 result, out, err = self.runsubcmd("visualize", "ntdsconn",
233 '-o', color_yes_file)
234 self.assertCmdSuccess(result, out, err)
235 self.assertEqual(out, '')
236 f = open(color_yes_file)
237 colour_yes = f.read()
239 self.assertNotEqual(colour_yes.strip(), expected)
241 self.remove_files(color_yes_file)
243 # Try the magic filename "-", meaning stdout.
244 # This doesn't exercise the case when stdout is a TTY
245 for c, equal in [('no', True), ('auto', True), ('yes', False)]:
246 result, out, err = self.runsubcmd("visualize", "ntdsconn",
250 self.assertCmdSuccess(result, out, err)
251 self.assertEqual((out.strip() == expected), equal)
254 """Ensure that --utf8 adds at least some expected utf-8, and that it
255 isn't there without --utf8."""
256 result, utf8, err = self.runsubcmd("visualize", "ntdsconn",
258 '--color=no', '-S', '--utf8')
259 self.assertCmdSuccess(result, utf8, err)
261 result, ascii, err = self.runsubcmd("visualize", "ntdsconn",
264 self.assertCmdSuccess(result, ascii, err)
265 for c in ('│', '─', '╭'):
266 self.assertTrue(c in utf8, 'UTF8 should contain %s' % c)
267 self.assertTrue(c not in ascii, 'ASCII should not contain %s' % c)
269 def test_forced_local_dsa(self):
270 # the forced_local_dsa shouldn't make any difference, except
271 # for the title line.
272 result, target, err = self.runsubcmd("visualize", "ntdsconn",
275 self.assertCmdSuccess(result, target, err)
277 target = target.strip().split('\n', 1)[1]
278 for cn, site in MULTISITE_LDIF_DSAS:
279 dsa = DN_TEMPLATE % (cn, site)
280 samdb, dbfile = samdb_from_ldif(MULTISITE_LDIF,
285 result, out, err = self.runsubcmd("visualize", "ntdsconn",
286 '-H', 'tdb://' + dbfile,
288 self.assertCmdSuccess(result, out, err)
289 # Separate out the title line, which will differ in the DN.
290 title, body = out.strip().split('\n', 1)
291 self.assertStringsEqual(target, body)
292 self.assertIn(cn, title)
294 self.remove_files(*files)
296 def test_short_names(self):
297 """Ensure the colour ones are the same as the monochrome ones EXCEPT
298 for the colours, of which the monochrome one should know nothing"""
299 result, short, err = self.runsubcmd("visualize", "ntdsconn",
301 '--color=no', '-S', '--no-key')
302 self.assertCmdSuccess(result, short, err)
303 result, long, err = self.runsubcmd("visualize", "ntdsconn",
305 '--color=no', '--no-key')
306 self.assertCmdSuccess(result, long, err)
308 lines = short.split('\n')
311 short_without_key = []
313 m = re.match(r"'(.{1,2})' stands for '(.+)'", line)
316 replacements.append((len(a), a, b))
317 key_lines.append(line)
319 short_without_key.append(line)
321 short = '\n'.join(short_without_key)
322 # we need to replace longest strings first
323 replacements.sort(reverse=True)
325 # we don't want to shorten the DC name in the header line.
326 long_header, long2short = long.strip().split('\n', 1)
327 for _, a, b in replacements:
328 short2long = short2long.replace(a, b)
329 long2short = long2short.replace(b, a)
331 long2short = '%s\n%s' % (long_header, long2short)
333 # The white space is going to be all wacky, so lets squish it down
334 short2long = collapse_space(short2long)
335 long2short = collapse_space(long2short)
336 short = collapse_space(short)
337 long = collapse_space(long)
339 self.assertStringsEqual(short2long, long, strip=True)
340 self.assertStringsEqual(short, long2short, strip=True)
342 def test_disconnected_ldif_with_key(self):
343 """Test that the 'unconnected' ldif shows up and exactly matches the
345 # This is not truly a disconnected graph because the
346 # vampre/local/promoted DCs are in there and they have
347 # relationships, and SERVER2 and SERVER3 for some reason refer
350 samdb, dbfile = samdb_from_ldif(UNCONNECTED_LDIF,
352 self.lp, tag='disconnected')
353 dburl = 'tdb://' + dbfile
354 result, output, err = self.runsubcmd("visualize", "ntdsconn",
357 self.remove_files(dbfile)
358 self.assertCmdSuccess(result, output, err)
359 self.assertStringsEqual(output,
360 EXPECTED_DISTANCE_GRAPH_WITH_KEY)
362 def test_dot_ntdsconn(self):
363 """Graphviz NTDS Connection output"""
364 result, dot, err = self.runsubcmd("visualize", "ntdsconn",
366 '--color=no', '-S', '--dot',
368 self.assertCmdSuccess(result, dot, err)
369 self.assertStringsEqual(EXPECTED_DOT_MULTISITE_NO_KEY, dot)
371 def test_dot_ntdsconn_disconnected(self):
372 """Graphviz NTDS Connection output from disconnected graph"""
373 samdb, dbfile = samdb_from_ldif(UNCONNECTED_LDIF,
375 self.lp, tag='disconnected')
377 result, dot, err = self.runsubcmd("visualize", "ntdsconn",
378 '-H', 'tdb://' + dbfile,
379 '--color=no', '-S', '--dot',
381 self.assertCmdSuccess(result, dot, err)
382 self.remove_files(dbfile)
383 self.assertStringsEqual(EXPECTED_DOT_NTDSCONN_DISCONNECTED, dot,
386 def test_dot_ntdsconn_disconnected_to_file(self):
387 """Graphviz NTDS Connection output into a file"""
388 samdb, dbfile = samdb_from_ldif(UNCONNECTED_LDIF,
390 self.lp, tag='disconnected')
392 dot_file = os.path.join(self.tempdir, 'dotfile')
394 result, dot, err = self.runsubcmd("visualize", "ntdsconn",
395 '-H', 'tdb://' + dbfile,
396 '--color=no', '-S', '--dot',
398 self.assertCmdSuccess(result, dot, err)
402 self.assertStringsEqual(EXPECTED_DOT_NTDSCONN_DISCONNECTED, dot)
404 self.remove_files(dbfile, dot_file)
407 EXPECTED_DOT_MULTISITE_NO_KEY = r"""/* generated by samba */
408 digraph A_samba_tool_production {
409 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";
412 node[fontname=Helvetica; fontsize=10];
414 "CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n...";
415 "CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n...";
416 "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n...";
417 "CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n...";
418 "CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n...";
419 "CN=NTDS Settings,\nCN=WIN06,\nCN=Servers,\nCN=Site-3,\n...";
420 "CN=NTDS Settings,\nCN=WIN07,\nCN=Servers,\nCN=Site-4,\n...";
421 "CN=NTDS Settings,\nCN=WIN08,\nCN=Servers,\nCN=Site-4,\n...";
422 "CN=NTDS Settings,\nCN=WIN09,\nCN=Servers,\nCN=Site-5,\n...";
423 "CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n...";
424 "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", ];
425 "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", ];
426 "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", ];
427 "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", ];
428 "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", ];
429 "CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
430 "CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
431 "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
432 "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
433 "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", ];
434 "CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
435 "CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
436 "CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
437 "CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
438 "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", ];
439 "CN=NTDS Settings,\nCN=WIN09,\nCN=Servers,\nCN=Site-5,\n..." -> "CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n..." [color="#000000", ];
440 "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", ];
441 "CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n..." -> "CN=NTDS Settings,\nCN=WIN09,\nCN=Servers,\nCN=Site-5,\n..." [color="#000000", ];
447 EXPECTED_DOT_NTDSCONN_DISCONNECTED = r"""/* generated by samba */
448 digraph A_samba_tool_production {
449 label="NTDS Connections known to CN=LOCALDC,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com";
452 node[fontname=Helvetica; fontsize=10];
454 "CN=NTDS Settings,\nCN=CLIENT,\n...";
455 "CN=NTDS Settings,\nCN=LOCALDC,\n...";
456 "CN=NTDS Settings,\nCN=PROMOTEDVDC,\n...";
457 "CN=NTDS Settings,\nCN=SERVER1,\n...";
458 "CN=NTDS Settings,\nCN=SERVER2,\n...";
459 "CN=NTDS Settings,\nCN=SERVER3,\n...";
460 "CN=NTDS Settings,\nCN=SERVER4,\n...";
461 "CN=NTDS Settings,\nCN=SERVER5,\n...";
462 "CN=NTDS Settings,\nCN=LOCALDC,\n..." -> "CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." [color="#000000", ];
463 "CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." -> "CN=NTDS Settings,\nCN=LOCALDC,\n..." [color="#000000", ];
464 "CN=NTDS Settings,\nCN=SERVER2,\n..." -> "CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." [color="#000000", ];
465 "CN=NTDS Settings,\nCN=SERVER3,\n..." -> "CN=NTDS Settings,\nCN=LOCALDC,\n..." [color="#000000", ];
466 subgraph cluster_key {
468 subgraph cluster_key_nodes {
473 subgraph cluster_key_edges {
476 subgraph cluster_key_0_ {
477 key_0_e1[label=src; color="#000000"; group="key_0__g"]
478 key_0_e2[label=dest; color="#000000"; group="key_0__g"]
479 key_0_e1 -> key_0_e2 [constraint = false; color="#000000"]
480 key_0__label[shape=plaintext; style=solid; width=2.000000; label="NTDS Connection\r"]
485 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"]
488 "CN=NTDS Settings,\nCN=CLIENT,\n..." -> key_0__label [style=invis];
489 "CN=NTDS Settings,\nCN=LOCALDC,\n..." -> key_0__label [style=invis];
490 "CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." -> key_0__label [style=invis];
491 "CN=NTDS Settings,\nCN=SERVER1,\n..." -> key_0__label [style=invis];
492 "CN=NTDS Settings,\nCN=SERVER2,\n..." -> key_0__label [style=invis];
493 "CN=NTDS Settings,\nCN=SERVER3,\n..." -> key_0__label [style=invis];
494 "CN=NTDS Settings,\nCN=SERVER4,\n..." -> key_0__label [style=invis];
495 "CN=NTDS Settings,\nCN=SERVER5,\n..." -> key_0__label [style=invis]
496 key_0__label -> elision0 [style=invis; weight=9]
501 EXPECTED_DISTANCE_GRAPH_WITH_KEY = """
502 NTDS Connections known to CN=LOCALDC,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com
505 ,-------- *,CN=CLIENT+
506 |,------- *,CN=LOCALDC+
507 ||,------ *,CN=PROMOTEDVDC+
508 |||,----- *,CN=SERVER1+
509 ||||,---- *,CN=SERVER2+
510 |||||,--- *,CN=SERVER3+
511 ||||||,-- *,CN=SERVER4+
512 source |||||||,- *,CN=SERVER5+
513 *,CN=CLIENT+ 0-------
514 *,CN=LOCALDC+ -01-----
515 *,CN=PROMOTEDVDC+ -10-----
516 *,CN=SERVER1+ ---0----
517 *,CN=SERVER2+ -21-0---
518 *,CN=SERVER3+ -12--0--
519 *,CN=SERVER4+ ------0-
520 *,CN=SERVER5+ -------0
522 '*' stands for 'CN=NTDS Settings'
523 '+' stands for ',CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com'
525 Data can get from source to destination in the indicated number of steps.
526 0 means zero steps (it is the same DC)
527 1 means a direct link
528 2 means a transitive link involving two steps (i.e. one intermediate DC)
529 - means there is no connection, even through other DCs