sambatool visualize: add up-to-dateness visualization
[Samba.git] / python / samba / netcmd / visualize.py
bloba24962ea58a39df0c7cd0059abfc87f664ff8bd8
1 # Visualisation tools
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 from __future__ import print_function
22 import os
23 import sys
24 from collections import defaultdict
25 import subprocess
27 import tempfile
28 import samba.getopt as options
29 from samba import dsdb
30 from samba import nttime2unix
31 from samba.netcmd import Command, SuperCommand, CommandError, Option
32 from samba.samdb import SamDB
33 from samba.graph import dot_graph
34 from samba.graph import distance_matrix, COLOUR_SETS
35 from samba.graph import full_matrix
36 from ldb import SCOPE_BASE, SCOPE_SUBTREE, LdbError
37 import time
38 import re
39 from samba.kcc import KCC, ldif_import_export
40 from samba.kcc.kcc_utils import KCCError
41 from samba.compat import text_type
43 COMMON_OPTIONS = [
44 Option("-H", "--URL", help="LDB URL for database or target server",
45 type=str, metavar="URL", dest="H"),
46 Option("-o", "--output", help="write here (default stdout)",
47 type=str, metavar="FILE", default=None),
48 Option("--distance", help="Distance matrix graph output (default)",
49 dest='format', const='distance', action='store_const'),
50 Option("--utf8", help="Use utf-8 Unicode characters",
51 action='store_true'),
52 Option("--color", help="use color (yes, no, auto)",
53 choices=['yes', 'no', 'auto']),
54 Option("--color-scheme", help=("use this colour scheme "
55 "(implies --color=yes)"),
56 choices=list(COLOUR_SETS.keys())),
57 Option("-S", "--shorten-names",
58 help="don't print long common suffixes",
59 action='store_true', default=False),
60 Option("-r", "--talk-to-remote", help="query other DCs' databases",
61 action='store_true', default=False),
62 Option("--no-key", help="omit the explanatory key",
63 action='store_false', default=True, dest='key'),
66 DOT_OPTIONS = [
67 Option("--dot", help="Graphviz dot output", dest='format',
68 const='dot', action='store_const'),
69 Option("--xdot", help="attempt to call Graphviz xdot", dest='format',
70 const='xdot', action='store_const'),
73 TEMP_FILE = '__temp__'
76 class GraphCommand(Command):
77 """Base class for graphing commands"""
79 synopsis = "%prog [options]"
80 takes_optiongroups = {
81 "sambaopts": options.SambaOptions,
82 "versionopts": options.VersionOptions,
83 "credopts": options.CredentialsOptions,
85 takes_options = COMMON_OPTIONS + DOT_OPTIONS
86 takes_args = ()
88 def get_db(self, H, sambaopts, credopts):
89 lp = sambaopts.get_loadparm()
90 creds = credopts.get_credentials(lp, fallback_machine=True)
91 samdb = SamDB(url=H, credentials=creds, lp=lp)
92 return samdb
94 def get_kcc_and_dsas(self, H, lp, creds):
95 """Get a readonly KCC object and the list of DSAs it knows about."""
96 unix_now = int(time.time())
97 kcc = KCC(unix_now, readonly=True)
98 kcc.load_samdb(H, lp, creds)
100 dsa_list = kcc.list_dsas()
101 dsas = set(dsa_list)
102 if len(dsas) != len(dsa_list):
103 print("There seem to be duplicate dsas", file=sys.stderr)
105 return kcc, dsas
107 def write(self, s, fn=None, suffix='.dot'):
108 """Decide whether we're dealing with a filename, a tempfile, or
109 stdout, and write accordingly.
111 :param s: the string to write
112 :param fn: a destination
113 :param suffix: suffix, if destination is a tempfile
115 If fn is None or "-", write to stdout.
116 If fn is visualize.TEMP_FILE, write to a temporary file
117 Otherwise fn should be a filename to write to.
119 if fn is None or fn == '-':
120 # we're just using stdout (a.k.a self.outf)
121 print(s, file=self.outf)
122 return
124 if fn is TEMP_FILE:
125 fd, fn = tempfile.mkstemp(prefix='samba-tool-visualise',
126 suffix=suffix)
127 f = open(fn, 'w')
128 os.close(fd)
129 else:
130 f = open(fn, 'w')
132 f.write(s)
133 f.close()
134 return fn
136 def calc_output_format(self, format, output):
137 """Heuristics to work out what output format was wanted."""
138 if not format:
139 # They told us nothing! We have to work it out for ourselves.
140 if output and output.lower().endswith('.dot'):
141 return 'dot'
142 else:
143 return 'distance'
145 if format == 'xdot':
146 return 'dot'
148 return format
150 def call_xdot(self, s, output):
151 if output is None:
152 fn = self.write(s, TEMP_FILE)
153 else:
154 fn = self.write(s, output)
155 xdot = os.environ.get('SAMBA_TOOL_XDOT_PATH', '/usr/bin/xdot')
156 subprocess.call([xdot, fn])
157 os.remove(fn)
159 def calc_distance_color_scheme(self, color, color_scheme, output):
160 """Heuristics to work out the colour scheme for distance matrices.
161 Returning None means no colour, otherwise it sould be a colour
162 from graph.COLOUR_SETS"""
163 if color == 'no':
164 return None
166 if color == 'auto':
167 if isinstance(output, str) and output != '-':
168 return None
169 if not hasattr(self.outf, 'isatty'):
170 # not a real file, perhaps cStringIO in testing
171 return None
172 if not self.outf.isatty():
173 return None
175 if color_scheme is None:
176 if '256color' in os.environ.get('TERM', ''):
177 return 'xterm-256color-heatmap'
178 return 'ansi'
180 return color_scheme
183 def get_dnstr_site(dn):
184 """Helper function for sorting and grouping DNs by site, if
185 possible."""
186 m = re.search(r'CN=Servers,CN=\s*([^,]+)\s*,CN=Sites', dn)
187 if m:
188 return m.group(1)
189 # Oh well, let it sort by DN
190 return dn
193 def get_dnstrlist_site(t):
194 """Helper function for sorting and grouping lists of (DN, ...) tuples
195 by site, if possible."""
196 return get_dnstr_site(t[0])
199 def colour_hash(x):
200 """Generate a randomish but consistent darkish colour based on the
201 given object."""
202 from hashlib import md5
203 tmp_str = str(x)
204 if isinstance(tmp_str, text_type):
205 tmp_str = tmp_str.encode('utf8')
206 c = int(md5(tmp_str).hexdigest()[:6], base=16) & 0x7f7f7f
207 return '#%06x' % c
210 def get_partition_maps(samdb):
211 """Generate dictionaries mapping short partition names to the
212 appropriate DNs."""
213 base_dn = samdb.domain_dn()
214 short_to_long = {
215 "DOMAIN": base_dn,
216 "CONFIGURATION": str(samdb.get_config_basedn()),
217 "SCHEMA": "CN=Schema,%s" % samdb.get_config_basedn(),
218 "DNSDOMAIN": "DC=DomainDnsZones,%s" % base_dn,
219 "DNSFOREST": "DC=ForestDnsZones,%s" % base_dn
222 long_to_short = {}
223 for s, l in short_to_long.items():
224 long_to_short[l] = s
226 return short_to_long, long_to_short
229 def get_partition(samdb, part):
230 # Allow people to say "--partition=DOMAIN" rather than
231 # "--partition=DC=blah,DC=..."
232 if part is not None:
233 short_partitions, long_partitions = get_partition_maps(samdb)
234 part = short_partitions.get(part.upper(), part)
235 if part not in long_partitions:
236 raise CommandError("unknown partition %s" % part)
237 return part
240 class cmd_reps(GraphCommand):
241 "repsFrom/repsTo from every DSA"
243 takes_options = COMMON_OPTIONS + DOT_OPTIONS + [
244 Option("-p", "--partition", help="restrict to this partition",
245 default=None),
248 def run(self, H=None, output=None, shorten_names=False,
249 key=True, talk_to_remote=False,
250 sambaopts=None, credopts=None, versionopts=None,
251 mode='self', partition=None, color=None, color_scheme=None,
252 utf8=None, format=None, xdot=False):
253 # We use the KCC libraries in readonly mode to get the
254 # replication graph.
255 lp = sambaopts.get_loadparm()
256 creds = credopts.get_credentials(lp, fallback_machine=True)
257 local_kcc, dsas = self.get_kcc_and_dsas(H, lp, creds)
258 unix_now = local_kcc.unix_now
260 partition = get_partition(local_kcc.samdb, partition)
262 # nc_reps is an autovivifying dictionary of dictionaries of lists.
263 # nc_reps[partition]['current' | 'needed'] is a list of
264 # (dsa dn string, repsFromTo object) pairs.
265 nc_reps = defaultdict(lambda: defaultdict(list))
267 guid_to_dnstr = {}
269 # We run a new KCC for each DSA even if we aren't talking to
270 # the remote, because after kcc.run (or kcc.list_dsas) the kcc
271 # ends up in a messy state.
272 for dsa_dn in dsas:
273 kcc = KCC(unix_now, readonly=True)
274 if talk_to_remote:
275 res = local_kcc.samdb.search(dsa_dn,
276 scope=SCOPE_BASE,
277 attrs=["dNSHostName"])
278 dns_name = res[0]["dNSHostName"][0]
279 print("Attempting to contact ldap://%s (%s)" %
280 (dns_name, dsa_dn),
281 file=sys.stderr)
282 try:
283 kcc.load_samdb("ldap://%s" % dns_name, lp, creds)
284 except KCCError as e:
285 print("Could not contact ldap://%s (%s)" % (dns_name, e),
286 file=sys.stderr)
287 continue
289 kcc.run(H, lp, creds)
290 else:
291 kcc.load_samdb(H, lp, creds)
292 kcc.run(H, lp, creds, forced_local_dsa=dsa_dn)
294 dsas_from_here = set(kcc.list_dsas())
295 if dsas != dsas_from_here:
296 print("found extra DSAs:", file=sys.stderr)
297 for dsa in (dsas_from_here - dsas):
298 print(" %s" % dsa, file=sys.stderr)
299 print("missing DSAs (known locally, not by %s):" % dsa_dn,
300 file=sys.stderr)
301 for dsa in (dsas - dsas_from_here):
302 print(" %s" % dsa, file=sys.stderr)
304 for remote_dn in dsas_from_here:
305 if mode == 'others' and remote_dn == dsa_dn:
306 continue
307 elif mode == 'self' and remote_dn != dsa_dn:
308 continue
310 remote_dsa = kcc.get_dsa('CN=NTDS Settings,' + remote_dn)
311 kcc.translate_ntdsconn(remote_dsa)
312 guid_to_dnstr[str(remote_dsa.dsa_guid)] = remote_dn
313 # get_reps_tables() returns two dictionaries mapping
314 # dns to NCReplica objects
315 c, n = remote_dsa.get_rep_tables()
316 for part, rep in c.items():
317 if partition is None or part == partition:
318 nc_reps[part]['current'].append((dsa_dn, rep))
319 for part, rep in n.items():
320 if partition is None or part == partition:
321 nc_reps[part]['needed'].append((dsa_dn, rep))
323 all_edges = {'needed': {'to': [], 'from': []},
324 'current': {'to': [], 'from': []}}
326 short_partitions, long_partitions = get_partition_maps(local_kcc.samdb)
328 for partname, part in nc_reps.items():
329 for state, edgelists in all_edges.items():
330 for dsa_dn, rep in part[state]:
331 short_name = long_partitions.get(partname, partname)
332 for r in rep.rep_repsFrom:
333 edgelists['from'].append(
334 (dsa_dn,
335 guid_to_dnstr[str(r.source_dsa_obj_guid)],
336 short_name))
337 for r in rep.rep_repsTo:
338 edgelists['to'].append(
339 (guid_to_dnstr[str(r.source_dsa_obj_guid)],
340 dsa_dn,
341 short_name))
343 # Here we have the set of edges. From now it is a matter of
344 # interpretation and presentation.
346 if self.calc_output_format(format, output) == 'distance':
347 color_scheme = self.calc_distance_color_scheme(color,
348 color_scheme,
349 output)
350 header_strings = {
351 'from': "RepsFrom objects for %s",
352 'to': "RepsTo objects for %s",
354 for state, edgelists in all_edges.items():
355 for direction, items in edgelists.items():
356 part_edges = defaultdict(list)
357 for src, dest, part in items:
358 part_edges[part].append((src, dest))
359 for part, edges in part_edges.items():
360 s = distance_matrix(None, edges,
361 utf8=utf8,
362 colour=color_scheme,
363 shorten_names=shorten_names,
364 generate_key=key,
365 grouping_function=get_dnstr_site)
367 s = "\n%s\n%s" % (header_strings[direction] % part, s)
368 self.write(s, output)
369 return
371 edge_colours = []
372 edge_styles = []
373 dot_edges = []
374 dot_vertices = set()
375 used_colours = {}
376 key_set = set()
377 for state, edgelist in all_edges.items():
378 for direction, items in edgelist.items():
379 for src, dest, part in items:
380 colour = used_colours.setdefault((part),
381 colour_hash((part,
382 direction)))
383 linestyle = 'dotted' if state == 'needed' else 'solid'
384 arrow = 'open' if direction == 'to' else 'empty'
385 dot_vertices.add(src)
386 dot_vertices.add(dest)
387 dot_edges.append((src, dest))
388 edge_colours.append(colour)
389 style = 'style="%s"; arrowhead=%s' % (linestyle, arrow)
390 edge_styles.append(style)
391 key_set.add((part, 'reps' + direction.title(),
392 colour, style))
394 key_items = []
395 if key:
396 for part, direction, colour, linestyle in sorted(key_set):
397 key_items.append((False,
398 'color="%s"; %s' % (colour, linestyle),
399 "%s %s" % (part, direction)))
400 key_items.append((False,
401 'style="dotted"; arrowhead="open"',
402 "repsFromTo is needed"))
403 key_items.append((False,
404 'style="solid"; arrowhead="open"',
405 "repsFromTo currently exists"))
407 s = dot_graph(dot_vertices, dot_edges,
408 directed=True,
409 edge_colors=edge_colours,
410 edge_styles=edge_styles,
411 shorten_names=shorten_names,
412 key_items=key_items)
414 if format == 'xdot':
415 self.call_xdot(s, output)
416 else:
417 self.write(s, output)
420 class NTDSConn(object):
421 """Collects observation counts for NTDS connections, so we know
422 whether all DSAs agree."""
423 def __init__(self, src, dest):
424 self.observations = 0
425 self.src_attests = False
426 self.dest_attests = False
427 self.src = src
428 self.dest = dest
430 def attest(self, attester):
431 self.observations += 1
432 if attester == self.src:
433 self.src_attests = True
434 if attester == self.dest:
435 self.dest_attests = True
438 class cmd_ntdsconn(GraphCommand):
439 "Draw the NTDSConnection graph"
440 takes_options = COMMON_OPTIONS + DOT_OPTIONS + [
441 Option("--importldif", help="graph from samba_kcc generated ldif",
442 default=None),
445 def import_ldif_db(self, ldif, lp):
446 d = tempfile.mkdtemp(prefix='samba-tool-visualise')
447 fn = os.path.join(d, 'imported.ldb')
448 self._tmp_fn_to_delete = fn
449 samdb = ldif_import_export.ldif_to_samdb(fn, lp, ldif)
450 return fn
452 def run(self, H=None, output=None, shorten_names=False,
453 key=True, talk_to_remote=False,
454 sambaopts=None, credopts=None, versionopts=None,
455 color=None, color_scheme=None,
456 utf8=None, format=None, importldif=None,
457 xdot=False):
459 lp = sambaopts.get_loadparm()
460 if importldif is None:
461 creds = credopts.get_credentials(lp, fallback_machine=True)
462 else:
463 creds = None
464 H = self.import_ldif_db(importldif, lp)
466 local_kcc, dsas = self.get_kcc_and_dsas(H, lp, creds)
467 local_dsa_dn = local_kcc.my_dsa_dnstr.split(',', 1)[1]
468 vertices = set()
469 attested_edges = []
470 for dsa_dn in dsas:
471 if talk_to_remote:
472 res = local_kcc.samdb.search(dsa_dn,
473 scope=SCOPE_BASE,
474 attrs=["dNSHostName"])
475 dns_name = res[0]["dNSHostName"][0]
476 try:
477 samdb = self.get_db("ldap://%s" % dns_name, sambaopts,
478 credopts)
479 except LdbError as e:
480 print("Could not contact ldap://%s (%s)" % (dns_name, e),
481 file=sys.stderr)
482 continue
484 ntds_dn = samdb.get_dsServiceName()
485 dn = samdb.domain_dn()
486 else:
487 samdb = self.get_db(H, sambaopts, credopts)
488 ntds_dn = 'CN=NTDS Settings,' + dsa_dn
489 dn = dsa_dn
491 res = samdb.search(ntds_dn,
492 scope=SCOPE_BASE,
493 attrs=["msDS-isRODC"])
495 is_rodc = res[0]["msDS-isRODC"][0] == 'TRUE'
497 vertices.add((ntds_dn, 'RODC' if is_rodc else ''))
498 # XXX we could also look at schedule
499 res = samdb.search(dn,
500 scope=SCOPE_SUBTREE,
501 expression="(objectClass=nTDSConnection)",
502 attrs=['fromServer'],
503 # XXX can't be critical for ldif test
504 #controls=["search_options:1:2"],
505 controls=["search_options:0:2"],
508 for msg in res:
509 msgdn = str(msg.dn)
510 dest_dn = msgdn[msgdn.index(',') + 1:]
511 attested_edges.append((msg['fromServer'][0],
512 dest_dn, ntds_dn))
514 if importldif and H == self._tmp_fn_to_delete:
515 os.remove(H)
516 os.rmdir(os.path.dirname(H))
518 # now we overlay all the graphs and generate styles accordingly
519 edges = {}
520 for src, dest, attester in attested_edges:
521 k = (src, dest)
522 if k in edges:
523 e = edges[k]
524 else:
525 e = NTDSConn(*k)
526 edges[k] = e
527 e.attest(attester)
529 vertices, rodc_status = zip(*sorted(vertices))
531 if self.calc_output_format(format, output) == 'distance':
532 color_scheme = self.calc_distance_color_scheme(color,
533 color_scheme,
534 output)
535 colours = COLOUR_SETS[color_scheme]
536 c_header = colours.get('header', '')
537 c_reset = colours.get('reset', '')
539 epilog = []
540 if 'RODC' in rodc_status:
541 epilog.append('No outbound connections are expected from RODCs')
543 if not talk_to_remote:
544 # If we are not talking to remote servers, we list all
545 # the connections.
546 graph_edges = edges.keys()
547 title = 'NTDS Connections known to %s' % local_dsa_dn
549 else:
550 # If we are talking to the remotes, there are
551 # interesting cases we can discover. What matters most
552 # is that the destination (i.e. owner) knowns about
553 # the connection, but it would be worth noting if the
554 # source doesn't. Another strange situation could be
555 # when a DC thinks there is a connection elsewhere,
556 # but the computers allegedly involved don't believe
557 # it exists.
559 # With limited bandwidth in the table, we mark the
560 # edges known to the destination, and note the other
561 # cases in a list after the diagram.
562 graph_edges = []
563 source_denies = []
564 dest_denies = []
565 both_deny = []
566 for e, conn in edges.items():
567 if conn.dest_attests:
568 graph_edges.append(e)
569 if not conn.src_attests:
570 source_denies.append(e)
571 elif conn.src_attests:
572 dest_denies.append(e)
573 else:
574 both_deny.append(e)
576 title = 'NTDS Connections known to each destination DC'
578 if both_deny:
579 epilog.append('The following connections are alleged by '
580 'DCs other than the source and '
581 'destination:\n')
582 for e in both_deny:
583 epilog.append(' %s -> %s\n' % e)
584 if dest_denies:
585 epilog.append('The following connections are alleged by '
586 'DCs other than the destination but '
587 'including the source:\n')
588 for e in dest_denies:
589 epilog.append(' %s -> %s\n' % e)
590 if source_denies:
591 epilog.append('The following connections '
592 '(included in the chart) '
593 'are not known to the source DC:\n')
594 for e in source_denies:
595 epilog.append(' %s -> %s\n' % e)
597 s = distance_matrix(vertices, graph_edges,
598 utf8=utf8,
599 colour=color_scheme,
600 shorten_names=shorten_names,
601 generate_key=key,
602 grouping_function=get_dnstrlist_site,
603 row_comments=rodc_status)
605 epilog = ''.join(epilog)
606 if epilog:
607 epilog = '\n%sNOTES%s\n%s' % (c_header,
608 c_reset,
609 epilog)
611 self.write('\n%s\n\n%s\n%s' % (title,
613 epilog), output)
614 return
616 dot_edges = []
617 edge_colours = []
618 edge_styles = []
619 edge_labels = []
620 n_servers = len(dsas)
621 for k, e in sorted(edges.items()):
622 dot_edges.append(k)
623 if e.observations == n_servers or not talk_to_remote:
624 edge_colours.append('#000000')
625 edge_styles.append('')
626 elif e.dest_attests:
627 edge_styles.append('')
628 if e.src_attests:
629 edge_colours.append('#0000ff')
630 else:
631 edge_colours.append('#cc00ff')
632 elif e.src_attests:
633 edge_colours.append('#ff0000')
634 edge_styles.append('style=dashed')
635 else:
636 edge_colours.append('#ff0000')
637 edge_styles.append('style=dotted')
639 key_items = []
640 if key:
641 key_items.append((False,
642 'color="#000000"',
643 "NTDS Connection"))
644 for colour, desc in (('#0000ff', "missing from some DCs"),
645 ('#cc00ff', "missing from source DC")):
646 if colour in edge_colours:
647 key_items.append((False, 'color="%s"' % colour, desc))
649 for style, desc in (('style=dashed', "unknown to destination"),
650 ('style=dotted',
651 "unknown to source and destination")):
652 if style in edge_styles:
653 key_items.append((False,
654 'color="#ff0000; %s"' % style,
655 desc))
657 if talk_to_remote:
658 title = 'NTDS Connections'
659 else:
660 title = 'NTDS Connections known to %s' % local_dsa_dn
662 s = dot_graph(sorted(vertices), dot_edges,
663 directed=True,
664 title=title,
665 edge_colors=edge_colours,
666 edge_labels=edge_labels,
667 edge_styles=edge_styles,
668 shorten_names=shorten_names,
669 key_items=key_items)
671 if format == 'xdot':
672 self.call_xdot(s, output)
673 else:
674 self.write(s, output)
677 class cmd_uptodateness(GraphCommand):
678 """visualize uptodateness vectors"""
680 takes_options = COMMON_OPTIONS + [
681 Option("-p", "--partition", help="restrict to this partition",
682 default=None),
683 Option("--max-digits", default=3, type=int,
684 help="display this many digits of out-of-date-ness"),
687 def get_utdv(self, samdb, dn):
688 """This finds the uptodateness vector in the database."""
689 cursors = []
690 config_dn = samdb.get_config_basedn()
691 for c in dsdb._dsdb_load_udv_v2(samdb, dn):
692 inv_id = str(c.source_dsa_invocation_id)
693 res = samdb.search(base=config_dn,
694 expression=("(&(invocationId=%s)"
695 "(objectClass=nTDSDSA))" % inv_id),
696 attrs=["distinguishedName", "invocationId"])
697 settings_dn = res[0]["distinguishedName"][0]
698 prefix, dsa_dn = settings_dn.split(',', 1)
699 if prefix != 'CN=NTDS Settings':
700 raise CommandError("Expected NTDS Settings DN, got %s" %
701 settings_dn)
703 cursors.append((dsa_dn,
704 inv_id,
705 int(c.highest_usn),
706 nttime2unix(c.last_sync_success)))
707 return cursors
709 def get_own_cursor(self, samdb):
710 res = samdb.search(base="",
711 scope=SCOPE_BASE,
712 attrs=["highestCommittedUSN"])
713 usn = int(res[0]["highestCommittedUSN"][0])
714 now = int(time.time())
715 return (usn, now)
717 def run(self, H=None, output=None, shorten_names=False,
718 key=True, talk_to_remote=False,
719 sambaopts=None, credopts=None, versionopts=None,
720 color=None, color_scheme=None,
721 utf8=False, format=None, importldif=None,
722 xdot=False, partition=None, max_digits=3):
723 if not talk_to_remote:
724 print("this won't work without talking to the remote servers "
725 "(use -r)", file=self.outf)
726 return
728 # We use the KCC libraries in readonly mode to get the
729 # replication graph.
730 lp = sambaopts.get_loadparm()
731 creds = credopts.get_credentials(lp, fallback_machine=True)
732 local_kcc, dsas = self.get_kcc_and_dsas(H, lp, creds)
733 self.samdb = local_kcc.samdb
734 partition = get_partition(self.samdb, partition)
736 short_partitions, long_partitions = get_partition_maps(self.samdb)
737 color_scheme = self.calc_distance_color_scheme(color,
738 color_scheme,
739 output)
741 for part_name, part_dn in short_partitions.items():
742 if partition not in (part_dn, None):
743 continue # we aren't doing this partition
745 cursors = self.get_utdv(self.samdb, part_dn)
747 # we talk to each remote and make a matrix of the vectors
748 # -- for each partition
749 # normalise by oldest
750 utdv_edges = {}
751 for dsa_dn in dsas:
752 res = local_kcc.samdb.search(dsa_dn,
753 scope=SCOPE_BASE,
754 attrs=["dNSHostName"])
755 ldap_url = "ldap://%s" % res[0]["dNSHostName"][0]
756 try:
757 samdb = self.get_db(ldap_url, sambaopts, credopts)
758 cursors = self.get_utdv(samdb, part_dn)
759 own_usn, own_time = self.get_own_cursor(samdb)
760 remotes = {dsa_dn: own_usn}
761 for dn, guid, usn, t in cursors:
762 remotes[dn] = usn
763 except LdbError as e:
764 print("Could not contact %s (%s)" % (ldap_url, e),
765 file=sys.stderr)
766 continue
767 utdv_edges[dsa_dn] = remotes
769 distances = {}
770 max_distance = 0
771 for dn1 in dsas:
772 try:
773 peak = utdv_edges[dn1][dn1]
774 except KeyError as e:
775 peak = 0
776 d = {}
777 distances[dn1] = d
778 for dn2 in dsas:
779 if dn2 in utdv_edges:
780 if dn1 in utdv_edges[dn2]:
781 dist = peak - utdv_edges[dn2][dn1]
782 d[dn2] = dist
783 if dist > max_distance:
784 max_distance = dist
785 else:
786 print("Missing dn %s from UTD vector" % dn1,
787 file=sys.stderr)
788 else:
789 print("missing dn %s from UTD vector list" % dn2,
790 file=sys.stderr)
792 digits = min(max_digits, len(str(max_distance)))
793 if digits < 1:
794 digits = 1
795 c_scale = 10 ** digits
797 s = full_matrix(distances,
798 utf8=utf8,
799 colour=color_scheme,
800 shorten_names=shorten_names,
801 generate_key=key,
802 grouping_function=get_dnstr_site,
803 colour_scale=c_scale,
804 digits=digits,
805 ylabel='DC',
806 xlabel='out-of-date-ness')
808 self.write('\n%s\n\n%s' % (part_name, s), output)
811 class cmd_visualize(SuperCommand):
812 """Produces graphical representations of Samba network state"""
813 subcommands = {}
815 for k, v in globals().items():
816 if k.startswith('cmd_'):
817 subcommands[k[4:]] = v()