2017-08-10 11:57:24 +12:00
# Visualisation tools
#
# Copyright (C) Andrew Bartlett 2015, 2018
#
# by Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
from collections import defaultdict
2018-03-08 17:42:18 +13:00
import subprocess
2017-08-10 11:57:24 +12:00
import tempfile
import samba . getopt as options
2018-06-01 17:20:56 +12:00
from samba import dsdb
from samba import nttime2unix
2017-08-10 11:57:24 +12:00
from samba . netcmd import Command , SuperCommand , CommandError , Option
from samba . samdb import SamDB
from samba . graph import dot_graph
from samba . graph import distance_matrix , COLOUR_SETS
2018-06-01 17:20:56 +12:00
from samba . graph import full_matrix
2022-09-14 18:23:16 +12:00
from samba . colour import is_colour_wanted
2017-08-10 11:57:24 +12:00
from ldb import SCOPE_BASE , SCOPE_SUBTREE , LdbError
import time
2018-02-23 11:11:27 +13:00
import re
2018-03-07 13:55:08 +13:00
from samba . kcc import KCC , ldif_import_export
2017-08-10 11:57:24 +12:00
from samba . kcc . kcc_utils import KCCError
2018-10-03 22:21:54 +13:00
from samba . uptodateness import (
get_partition_maps ,
get_partition ,
2018-10-03 22:39:04 +13:00
get_own_cursor ,
get_utdv ,
2018-10-03 23:09:56 +13:00
get_utdv_edges ,
2018-10-03 23:21:11 +13:00
get_utdv_distances ,
2018-10-03 23:45:12 +13:00
get_utdv_max_distance ,
2018-10-04 00:42:08 +13:00
get_kcc_and_dsas ,
2018-10-03 22:21:54 +13:00
)
2017-08-10 11:57:24 +12:00
COMMON_OPTIONS = [
Option ( " -H " , " --URL " , help = " LDB URL for database or target server " ,
type = str , metavar = " URL " , dest = " H " ) ,
Option ( " -o " , " --output " , help = " write here (default stdout) " ,
type = str , metavar = " FILE " , default = None ) ,
Option ( " --distance " , help = " Distance matrix graph output (default) " ,
dest = ' format ' , const = ' distance ' , action = ' store_const ' ) ,
Option ( " --utf8 " , help = " Use utf-8 Unicode characters " ,
action = ' store_true ' ) ,
Option ( " --color-scheme " , help = ( " use this colour scheme "
" (implies --color=yes) " ) ,
2018-05-04 13:33:03 +01:00
choices = list ( COLOUR_SETS . keys ( ) ) ) ,
2017-08-10 11:57:24 +12:00
Option ( " -S " , " --shorten-names " ,
help = " don ' t print long common suffixes " ,
action = ' store_true ' , default = False ) ,
Option ( " -r " , " --talk-to-remote " , help = " query other DCs ' databases " ,
action = ' store_true ' , default = False ) ,
Option ( " --no-key " , help = " omit the explanatory key " ,
action = ' store_false ' , default = True , dest = ' key ' ) ,
]
2018-05-24 15:22:47 +12:00
DOT_OPTIONS = [
Option ( " --dot " , help = " Graphviz dot output " , dest = ' format ' ,
const = ' dot ' , action = ' store_const ' ) ,
Option ( " --xdot " , help = " attempt to call Graphviz xdot " , dest = ' format ' ,
const = ' xdot ' , action = ' store_const ' ) ,
]
2017-08-10 11:57:24 +12:00
TEMP_FILE = ' __temp__ '
class GraphCommand ( Command ) :
""" Base class for graphing commands """
synopsis = " % prog [options] "
takes_optiongroups = {
" sambaopts " : options . SambaOptions ,
" versionopts " : options . VersionOptions ,
" credopts " : options . CredentialsOptions ,
}
2018-05-24 15:22:47 +12:00
takes_options = COMMON_OPTIONS + DOT_OPTIONS
2017-08-10 11:57:24 +12:00
takes_args = ( )
def get_db ( self , H , sambaopts , credopts ) :
lp = sambaopts . get_loadparm ( )
creds = credopts . get_credentials ( lp , fallback_machine = True )
samdb = SamDB ( url = H , credentials = creds , lp = lp )
return samdb
def write ( self , s , fn = None , suffix = ' .dot ' ) :
""" Decide whether we ' re dealing with a filename, a tempfile, or
stdout , and write accordingly .
: param s : the string to write
: param fn : a destination
: param suffix : suffix , if destination is a tempfile
If fn is None or " - " , write to stdout .
If fn is visualize . TEMP_FILE , write to a temporary file
Otherwise fn should be a filename to write to .
"""
if fn is None or fn == ' - ' :
# we're just using stdout (a.k.a self.outf)
print ( s , file = self . outf )
return
if fn is TEMP_FILE :
fd , fn = tempfile . mkstemp ( prefix = ' samba-tool-visualise ' ,
suffix = suffix )
f = open ( fn , ' w ' )
os . close ( fd )
else :
f = open ( fn , ' w ' )
f . write ( s )
f . close ( )
return fn
def calc_output_format ( self , format , output ) :
""" Heuristics to work out what output format was wanted. """
if not format :
# They told us nothing! We have to work it out for ourselves.
if output and output . lower ( ) . endswith ( ' .dot ' ) :
return ' dot '
else :
return ' distance '
2018-03-08 17:42:18 +13:00
if format == ' xdot ' :
return ' dot '
2017-08-10 11:57:24 +12:00
return format
2018-03-08 17:42:18 +13:00
def call_xdot ( self , s , output ) :
if output is None :
fn = self . write ( s , TEMP_FILE )
else :
fn = self . write ( s , output )
xdot = os . environ . get ( ' SAMBA_TOOL_XDOT_PATH ' , ' /usr/bin/xdot ' )
subprocess . call ( [ xdot , fn ] )
os . remove ( fn )
2022-09-09 14:56:08 +12:00
def calc_distance_color_scheme ( self , color_scheme , output ) :
2017-08-10 11:57:24 +12:00
""" Heuristics to work out the colour scheme for distance matrices.
2023-06-06 13:17:58 +02:00
Returning None means no colour , otherwise it should be a colour
2017-08-10 11:57:24 +12:00
from graph . COLOUR_SETS """
2022-09-14 18:23:16 +12:00
if color_scheme is not None :
# --color-scheme implies --color=yes for *this* purpose.
return color_scheme
if output in ( ' - ' , None ) :
output = self . outf
want_colour = is_colour_wanted ( output , hint = self . requested_colour )
if not want_colour :
2017-08-10 11:57:24 +12:00
return None
2022-09-14 18:23:16 +12:00
# if we got to here, we are using colour according to the
# --color/NO_COLOR rules, but no colour scheme has been
# specified, so we choose some defaults.
if ' 256color ' in os . environ . get ( ' TERM ' , ' ' ) :
return ' xterm-256color-heatmap '
return ' ansi '
2017-08-10 11:57:24 +12:00
2018-02-23 11:11:27 +13:00
def get_dnstr_site ( dn ) :
""" Helper function for sorting and grouping DNs by site, if
possible . """
m = re . search ( r ' CN=Servers,CN= \ s*([^,]+) \ s*,CN=Sites ' , dn )
if m :
return m . group ( 1 )
# Oh well, let it sort by DN
return dn
2018-06-01 17:14:32 +12:00
def get_dnstrlist_site ( t ) :
""" Helper function for sorting and grouping lists of (DN, ...) tuples
by site , if possible . """
return get_dnstr_site ( t [ 0 ] )
2017-08-10 11:57:24 +12:00
def colour_hash ( x ) :
""" Generate a randomish but consistent darkish colour based on the
given object . """
from hashlib import md5
2018-05-04 12:05:27 +01:00
tmp_str = str ( x )
2020-07-04 14:27:06 +12:00
if isinstance ( tmp_str , str ) :
2018-05-04 12:05:27 +01:00
tmp_str = tmp_str . encode ( ' utf8 ' )
c = int ( md5 ( tmp_str ) . hexdigest ( ) [ : 6 ] , base = 16 ) & 0x7f7f7f
2017-08-10 11:57:24 +12:00
return ' # %06x ' % c
class cmd_reps ( GraphCommand ) :
" repsFrom/repsTo from every DSA "
2018-05-24 15:22:47 +12:00
takes_options = COMMON_OPTIONS + DOT_OPTIONS + [
2017-08-10 11:57:24 +12:00
Option ( " -p " , " --partition " , help = " restrict to this partition " ,
default = None ) ,
]
def run ( self , H = None , output = None , shorten_names = False ,
key = True , talk_to_remote = False ,
sambaopts = None , credopts = None , versionopts = None ,
2022-09-09 14:56:08 +12:00
mode = ' self ' , partition = None , color_scheme = None ,
2018-03-08 17:42:18 +13:00
utf8 = None , format = None , xdot = False ) :
2017-08-10 11:57:24 +12:00
# We use the KCC libraries in readonly mode to get the
# replication graph.
lp = sambaopts . get_loadparm ( )
creds = credopts . get_credentials ( lp , fallback_machine = True )
2018-10-04 00:42:08 +13:00
local_kcc , dsas = get_kcc_and_dsas ( H , lp , creds )
2017-08-10 11:57:24 +12:00
unix_now = local_kcc . unix_now
2018-03-29 15:52:25 +13:00
partition = get_partition ( local_kcc . samdb , partition )
2017-08-10 11:57:24 +12:00
# nc_reps is an autovivifying dictionary of dictionaries of lists.
# nc_reps[partition]['current' | 'needed'] is a list of
# (dsa dn string, repsFromTo object) pairs.
nc_reps = defaultdict ( lambda : defaultdict ( list ) )
guid_to_dnstr = { }
# We run a new KCC for each DSA even if we aren't talking to
# the remote, because after kcc.run (or kcc.list_dsas) the kcc
# ends up in a messy state.
for dsa_dn in dsas :
kcc = KCC ( unix_now , readonly = True )
if talk_to_remote :
res = local_kcc . samdb . search ( dsa_dn ,
scope = SCOPE_BASE ,
attrs = [ " dNSHostName " ] )
2018-08-10 18:07:11 +01:00
dns_name = str ( res [ 0 ] [ " dNSHostName " ] [ 0 ] )
2017-08-10 11:57:24 +12:00
print ( " Attempting to contact ldap:// %s ( %s ) " %
( dns_name , dsa_dn ) ,
file = sys . stderr )
try :
kcc . load_samdb ( " ldap:// %s " % dns_name , lp , creds )
except KCCError as e :
print ( " Could not contact ldap:// %s ( %s ) " % ( dns_name , e ) ,
file = sys . stderr )
continue
kcc . run ( H , lp , creds )
else :
kcc . load_samdb ( H , lp , creds )
kcc . run ( H , lp , creds , forced_local_dsa = dsa_dn )
dsas_from_here = set ( kcc . list_dsas ( ) )
if dsas != dsas_from_here :
print ( " found extra DSAs: " , file = sys . stderr )
for dsa in ( dsas_from_here - dsas ) :
print ( " %s " % dsa , file = sys . stderr )
print ( " missing DSAs (known locally, not by %s ): " % dsa_dn ,
file = sys . stderr )
for dsa in ( dsas - dsas_from_here ) :
print ( " %s " % dsa , file = sys . stderr )
for remote_dn in dsas_from_here :
if mode == ' others ' and remote_dn == dsa_dn :
continue
elif mode == ' self ' and remote_dn != dsa_dn :
continue
remote_dsa = kcc . get_dsa ( ' CN=NTDS Settings, ' + remote_dn )
kcc . translate_ntdsconn ( remote_dsa )
guid_to_dnstr [ str ( remote_dsa . dsa_guid ) ] = remote_dn
# get_reps_tables() returns two dictionaries mapping
# dns to NCReplica objects
c , n = remote_dsa . get_rep_tables ( )
2018-04-11 10:32:06 +12:00
for part , rep in c . items ( ) :
2017-08-10 11:57:24 +12:00
if partition is None or part == partition :
nc_reps [ part ] [ ' current ' ] . append ( ( dsa_dn , rep ) )
2018-04-11 10:32:06 +12:00
for part , rep in n . items ( ) :
2017-08-10 11:57:24 +12:00
if partition is None or part == partition :
nc_reps [ part ] [ ' needed ' ] . append ( ( dsa_dn , rep ) )
2018-07-30 18:19:11 +12:00
all_edges = { ' needed ' : { ' to ' : [ ] , ' from ' : [ ] } ,
2017-08-10 11:57:24 +12:00
' current ' : { ' to ' : [ ] , ' from ' : [ ] } }
2018-03-29 15:52:25 +13:00
short_partitions , long_partitions = get_partition_maps ( local_kcc . samdb )
2018-04-11 10:32:06 +12:00
for partname , part in nc_reps . items ( ) :
for state , edgelists in all_edges . items ( ) :
2017-08-10 11:57:24 +12:00
for dsa_dn , rep in part [ state ] :
short_name = long_partitions . get ( partname , partname )
for r in rep . rep_repsFrom :
edgelists [ ' from ' ] . append (
( dsa_dn ,
guid_to_dnstr [ str ( r . source_dsa_obj_guid ) ] ,
short_name ) )
for r in rep . rep_repsTo :
edgelists [ ' to ' ] . append (
( guid_to_dnstr [ str ( r . source_dsa_obj_guid ) ] ,
dsa_dn ,
short_name ) )
# Here we have the set of edges. From now it is a matter of
# interpretation and presentation.
if self . calc_output_format ( format , output ) == ' distance ' :
2022-09-09 14:56:08 +12:00
color_scheme = self . calc_distance_color_scheme ( color_scheme ,
2017-08-10 11:57:24 +12:00
output )
header_strings = {
' from ' : " RepsFrom objects for %s " ,
' to ' : " RepsTo objects for %s " ,
}
2018-04-11 10:32:06 +12:00
for state , edgelists in all_edges . items ( ) :
for direction , items in edgelists . items ( ) :
2017-08-10 11:57:24 +12:00
part_edges = defaultdict ( list )
for src , dest , part in items :
part_edges [ part ] . append ( ( src , dest ) )
2018-04-11 10:32:06 +12:00
for part , edges in part_edges . items ( ) :
2017-08-10 11:57:24 +12:00
s = distance_matrix ( None , edges ,
utf8 = utf8 ,
colour = color_scheme ,
shorten_names = shorten_names ,
2018-02-23 11:11:27 +13:00
generate_key = key ,
grouping_function = get_dnstr_site )
2017-08-10 11:57:24 +12:00
s = " \n %s \n %s " % ( header_strings [ direction ] % part , s )
self . write ( s , output )
return
edge_colours = [ ]
edge_styles = [ ]
dot_edges = [ ]
dot_vertices = set ( )
used_colours = { }
key_set = set ( )
2018-04-11 10:32:06 +12:00
for state , edgelist in all_edges . items ( ) :
for direction , items in edgelist . items ( ) :
2017-08-10 11:57:24 +12:00
for src , dest , part in items :
colour = used_colours . setdefault ( ( part ) ,
colour_hash ( ( part ,
direction ) ) )
linestyle = ' dotted ' if state == ' needed ' else ' solid '
arrow = ' open ' if direction == ' to ' else ' empty '
dot_vertices . add ( src )
dot_vertices . add ( dest )
dot_edges . append ( ( src , dest ) )
edge_colours . append ( colour )
style = ' style= " %s " ; arrowhead= %s ' % ( linestyle , arrow )
edge_styles . append ( style )
key_set . add ( ( part , ' reps ' + direction . title ( ) ,
colour , style ) )
key_items = [ ]
if key :
for part , direction , colour , linestyle in sorted ( key_set ) :
key_items . append ( ( False ,
' color= " %s " ; %s ' % ( colour , linestyle ) ,
" %s %s " % ( part , direction ) ) )
key_items . append ( ( False ,
' style= " dotted " ; arrowhead= " open " ' ,
" repsFromTo is needed " ) )
key_items . append ( ( False ,
' style= " solid " ; arrowhead= " open " ' ,
" repsFromTo currently exists " ) )
s = dot_graph ( dot_vertices , dot_edges ,
directed = True ,
edge_colors = edge_colours ,
edge_styles = edge_styles ,
shorten_names = shorten_names ,
key_items = key_items )
2018-03-08 17:42:18 +13:00
if format == ' xdot ' :
self . call_xdot ( s , output )
else :
self . write ( s , output )
2017-08-10 11:57:24 +12:00
class NTDSConn ( object ) :
""" Collects observation counts for NTDS connections, so we know
whether all DSAs agree . """
def __init__ ( self , src , dest ) :
self . observations = 0
self . src_attests = False
self . dest_attests = False
self . src = src
self . dest = dest
def attest ( self , attester ) :
self . observations + = 1
if attester == self . src :
self . src_attests = True
if attester == self . dest :
self . dest_attests = True
class cmd_ntdsconn ( GraphCommand ) :
" Draw the NTDSConnection graph "
2018-05-24 15:22:47 +12:00
takes_options = COMMON_OPTIONS + DOT_OPTIONS + [
2018-03-07 13:55:08 +13:00
Option ( " --importldif " , help = " graph from samba_kcc generated ldif " ,
default = None ) ,
]
def import_ldif_db ( self , ldif , lp ) :
d = tempfile . mkdtemp ( prefix = ' samba-tool-visualise ' )
fn = os . path . join ( d , ' imported.ldb ' )
self . _tmp_fn_to_delete = fn
samdb = ldif_import_export . ldif_to_samdb ( fn , lp , ldif )
return fn
2017-08-10 11:57:24 +12:00
def run ( self , H = None , output = None , shorten_names = False ,
key = True , talk_to_remote = False ,
sambaopts = None , credopts = None , versionopts = None ,
2022-09-09 14:56:08 +12:00
color_scheme = None ,
2018-03-08 17:42:18 +13:00
utf8 = None , format = None , importldif = None ,
xdot = False ) :
2018-03-07 13:55:08 +13:00
2017-08-10 11:57:24 +12:00
lp = sambaopts . get_loadparm ( )
2018-03-07 13:55:08 +13:00
if importldif is None :
creds = credopts . get_credentials ( lp , fallback_machine = True )
else :
creds = None
H = self . import_ldif_db ( importldif , lp )
2018-10-04 00:42:08 +13:00
local_kcc , dsas = get_kcc_and_dsas ( H , lp , creds )
2018-04-20 10:52:31 +12:00
local_dsa_dn = local_kcc . my_dsa_dnstr . split ( ' , ' , 1 ) [ 1 ]
2017-08-10 11:57:24 +12:00
vertices = set ( )
attested_edges = [ ]
for dsa_dn in dsas :
if talk_to_remote :
res = local_kcc . samdb . search ( dsa_dn ,
scope = SCOPE_BASE ,
attrs = [ " dNSHostName " ] )
dns_name = res [ 0 ] [ " dNSHostName " ] [ 0 ]
try :
samdb = self . get_db ( " ldap:// %s " % dns_name , sambaopts ,
credopts )
except LdbError as e :
print ( " Could not contact ldap:// %s ( %s ) " % ( dns_name , e ) ,
file = sys . stderr )
continue
ntds_dn = samdb . get_dsServiceName ( )
dn = samdb . domain_dn ( )
else :
samdb = self . get_db ( H , sambaopts , credopts )
ntds_dn = ' CN=NTDS Settings, ' + dsa_dn
dn = dsa_dn
2018-03-08 14:29:40 +13:00
res = samdb . search ( ntds_dn ,
scope = SCOPE_BASE ,
attrs = [ " msDS-isRODC " ] )
is_rodc = res [ 0 ] [ " msDS-isRODC " ] [ 0 ] == ' TRUE '
vertices . add ( ( ntds_dn , ' RODC ' if is_rodc else ' ' ) )
2017-08-10 11:57:24 +12:00
# XXX we could also look at schedule
res = samdb . search ( dn ,
scope = SCOPE_SUBTREE ,
expression = " (objectClass=nTDSConnection) " ,
attrs = [ ' fromServer ' ] ,
# XXX can't be critical for ldif test
2018-07-30 18:19:49 +12:00
# controls=["search_options:1:2"],
2017-08-10 11:57:24 +12:00
controls = [ " search_options:0:2 " ] ,
2018-07-30 18:14:43 +12:00
)
2017-08-10 11:57:24 +12:00
for msg in res :
msgdn = str ( msg . dn )
dest_dn = msgdn [ msgdn . index ( ' , ' ) + 1 : ]
2018-08-10 18:07:11 +01:00
attested_edges . append ( ( str ( msg [ ' fromServer ' ] [ 0 ] ) ,
2017-08-10 11:57:24 +12:00
dest_dn , ntds_dn ) )
2018-03-07 13:55:08 +13:00
if importldif and H == self . _tmp_fn_to_delete :
os . remove ( H )
os . rmdir ( os . path . dirname ( H ) )
2017-08-10 11:57:24 +12:00
# now we overlay all the graphs and generate styles accordingly
edges = { }
for src , dest , attester in attested_edges :
k = ( src , dest )
if k in edges :
e = edges [ k ]
else :
e = NTDSConn ( * k )
edges [ k ] = e
e . attest ( attester )
2018-03-08 14:29:40 +13:00
vertices , rodc_status = zip ( * sorted ( vertices ) )
2017-08-10 11:57:24 +12:00
if self . calc_output_format ( format , output ) == ' distance ' :
2022-09-09 14:56:08 +12:00
color_scheme = self . calc_distance_color_scheme ( color_scheme ,
2017-08-10 11:57:24 +12:00
output )
2018-03-08 14:29:40 +13:00
colours = COLOUR_SETS [ color_scheme ]
c_header = colours . get ( ' header ' , ' ' )
c_reset = colours . get ( ' reset ' , ' ' )
epilog = [ ]
if ' RODC ' in rodc_status :
epilog . append ( ' No outbound connections are expected from RODCs ' )
2017-08-10 11:57:24 +12:00
if not talk_to_remote :
# If we are not talking to remote servers, we list all
# the connections.
graph_edges = edges . keys ( )
2018-04-20 10:52:31 +12:00
title = ' NTDS Connections known to %s ' % local_dsa_dn
2017-08-10 11:57:24 +12:00
else :
# If we are talking to the remotes, there are
# interesting cases we can discover. What matters most
# is that the destination (i.e. owner) knowns about
# the connection, but it would be worth noting if the
# source doesn't. Another strange situation could be
# when a DC thinks there is a connection elsewhere,
# but the computers allegedly involved don't believe
# it exists.
#
# With limited bandwidth in the table, we mark the
# edges known to the destination, and note the other
# cases in a list after the diagram.
graph_edges = [ ]
source_denies = [ ]
dest_denies = [ ]
both_deny = [ ]
2018-04-11 10:32:06 +12:00
for e , conn in edges . items ( ) :
2017-08-10 11:57:24 +12:00
if conn . dest_attests :
graph_edges . append ( e )
if not conn . src_attests :
source_denies . append ( e )
elif conn . src_attests :
dest_denies . append ( e )
else :
both_deny . append ( e )
title = ' NTDS Connections known to each destination DC '
2018-03-08 14:29:40 +13:00
2017-08-10 11:57:24 +12:00
if both_deny :
epilog . append ( ' The following connections are alleged by '
' DCs other than the source and '
' destination: \n ' )
for e in both_deny :
epilog . append ( ' %s -> %s \n ' % e )
if dest_denies :
epilog . append ( ' The following connections are alleged by '
' DCs other than the destination but '
' including the source: \n ' )
for e in dest_denies :
epilog . append ( ' %s -> %s \n ' % e )
if source_denies :
epilog . append ( ' The following connections '
' (included in the chart) '
' are not known to the source DC: \n ' )
for e in source_denies :
epilog . append ( ' %s -> %s \n ' % e )
2018-03-08 14:29:40 +13:00
s = distance_matrix ( vertices , graph_edges ,
2017-08-10 11:57:24 +12:00
utf8 = utf8 ,
colour = color_scheme ,
shorten_names = shorten_names ,
2018-02-23 11:11:27 +13:00
generate_key = key ,
2018-06-01 17:14:32 +12:00
grouping_function = get_dnstrlist_site ,
2018-03-08 14:29:40 +13:00
row_comments = rodc_status )
epilog = ' ' . join ( epilog )
if epilog :
epilog = ' \n %s NOTES %s \n %s ' % ( c_header ,
c_reset ,
epilog )
self . write ( ' \n %s \n \n %s \n %s ' % ( title ,
s ,
epilog ) , output )
2017-08-10 11:57:24 +12:00
return
dot_edges = [ ]
edge_colours = [ ]
edge_styles = [ ]
edge_labels = [ ]
n_servers = len ( dsas )
2018-04-11 10:32:06 +12:00
for k , e in sorted ( edges . items ( ) ) :
2017-08-10 11:57:24 +12:00
dot_edges . append ( k )
if e . observations == n_servers or not talk_to_remote :
edge_colours . append ( ' #000000 ' )
edge_styles . append ( ' ' )
elif e . dest_attests :
edge_styles . append ( ' ' )
if e . src_attests :
edge_colours . append ( ' #0000ff ' )
else :
edge_colours . append ( ' #cc00ff ' )
elif e . src_attests :
edge_colours . append ( ' #ff0000 ' )
edge_styles . append ( ' style=dashed ' )
else :
edge_colours . append ( ' #ff0000 ' )
edge_styles . append ( ' style=dotted ' )
key_items = [ ]
if key :
key_items . append ( ( False ,
' color= " #000000 " ' ,
" NTDS Connection " ) )
for colour , desc in ( ( ' #0000ff ' , " missing from some DCs " ) ,
( ' #cc00ff ' , " missing from source DC " ) ) :
if colour in edge_colours :
key_items . append ( ( False , ' color= " %s " ' % colour , desc ) )
for style , desc in ( ( ' style=dashed ' , " unknown to destination " ) ,
( ' style=dotted ' ,
" unknown to source and destination " ) ) :
if style in edge_styles :
key_items . append ( ( False ,
' color= " #ff0000; %s " ' % style ,
desc ) )
if talk_to_remote :
title = ' NTDS Connections '
else :
2018-04-20 10:52:31 +12:00
title = ' NTDS Connections known to %s ' % local_dsa_dn
2017-08-10 11:57:24 +12:00
s = dot_graph ( sorted ( vertices ) , dot_edges ,
directed = True ,
title = title ,
edge_colors = edge_colours ,
edge_labels = edge_labels ,
edge_styles = edge_styles ,
shorten_names = shorten_names ,
key_items = key_items )
2018-03-08 17:42:18 +13:00
if format == ' xdot ' :
self . call_xdot ( s , output )
else :
self . write ( s , output )
2017-08-10 11:57:24 +12:00
2018-06-01 17:20:56 +12:00
class cmd_uptodateness ( GraphCommand ) :
""" visualize uptodateness vectors """
takes_options = COMMON_OPTIONS + [
Option ( " -p " , " --partition " , help = " restrict to this partition " ,
default = None ) ,
Option ( " --max-digits " , default = 3 , type = int ,
help = " display this many digits of out-of-date-ness " ) ,
]
def run ( self , H = None , output = None , shorten_names = False ,
key = True , talk_to_remote = False ,
sambaopts = None , credopts = None , versionopts = None ,
2022-09-09 14:56:08 +12:00
color_scheme = None ,
2018-06-01 17:20:56 +12:00
utf8 = False , format = None , importldif = None ,
xdot = False , partition = None , max_digits = 3 ) :
if not talk_to_remote :
print ( " this won ' t work without talking to the remote servers "
" (use -r) " , file = self . outf )
return
# We use the KCC libraries in readonly mode to get the
# replication graph.
lp = sambaopts . get_loadparm ( )
creds = credopts . get_credentials ( lp , fallback_machine = True )
2018-10-04 00:42:08 +13:00
local_kcc , dsas = get_kcc_and_dsas ( H , lp , creds )
2018-06-01 17:20:56 +12:00
self . samdb = local_kcc . samdb
partition = get_partition ( self . samdb , partition )
short_partitions , long_partitions = get_partition_maps ( self . samdb )
2022-09-09 14:56:08 +12:00
color_scheme = self . calc_distance_color_scheme ( color_scheme ,
2018-06-01 17:20:56 +12:00
output )
for part_name , part_dn in short_partitions . items ( ) :
if partition not in ( part_dn , None ) :
continue # we aren't doing this partition
2018-10-03 23:09:56 +13:00
utdv_edges = get_utdv_edges ( local_kcc , dsas , part_dn , lp , creds )
2018-06-01 17:20:56 +12:00
2018-10-03 23:45:12 +13:00
distances = get_utdv_distances ( utdv_edges , dsas )
max_distance = get_utdv_max_distance ( distances )
2018-06-01 17:20:56 +12:00
digits = min ( max_digits , len ( str ( max_distance ) ) )
if digits < 1 :
digits = 1
c_scale = 10 * * digits
s = full_matrix ( distances ,
utf8 = utf8 ,
colour = color_scheme ,
shorten_names = shorten_names ,
generate_key = key ,
grouping_function = get_dnstr_site ,
colour_scale = c_scale ,
digits = digits ,
ylabel = ' DC ' ,
xlabel = ' out-of-date-ness ' )
self . write ( ' \n %s \n \n %s ' % ( part_name , s ) , output )
2017-08-10 11:57:24 +12:00
class cmd_visualize ( SuperCommand ) :
2019-03-28 17:24:40 +01:00
""" Produces graphical representations of Samba network state. """
2017-08-10 11:57:24 +12:00
subcommands = { }
2018-04-11 10:32:06 +12:00
for k , v in globals ( ) . items ( ) :
2017-08-10 11:57:24 +12:00
if k . startswith ( ' cmd_ ' ) :
subcommands [ k [ 4 : ] ] = v ( )