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