def test_basic_dot_files(self): vertices = tuple('abcdefgh') all_edges = tuple(itertools.combinations(vertices, 2)) line_edges = list(zip(vertices[1:], vertices[:-1])) ring_edges = line_edges + [(vertices[0], vertices[-1])] no_edges = [] # even join to even numbers, odd to odd disjoint_edges = [(a, b) for a, b in all_edges if ord(a) ^ ord(b) == 0] for name, edges in (('all', all_edges), ('line', line_edges), ('ring', ring_edges), ('no', no_edges), ('disjoint', disjoint_edges)): for directed, tag in ((True, "directed"), (False, "undirected")): title = "%s %s" % (name, tag) g = graph.dot_graph(vertices, edges, directed=directed, title=title) lines = g.split('\n') self.assertHeader(lines, title, directed) self.assertVertices(lines[7:], vertices) self.assertEdges(lines[len(vertices) + 7:], edges, directed)
def test_basic_dot_files(self): vertices = tuple('abcdefgh') all_edges = tuple(itertools.combinations(vertices, 2)) line_edges = zip(vertices[1:], vertices[:-1]) ring_edges = line_edges + [(vertices[0], vertices[-1])] no_edges = [] # even join to even numbers, odd to odd disjoint_edges = [(a, b) for a, b in all_edges if ord(a) ^ ord(b) == 0] for name, edges in (('all', all_edges), ('line', line_edges), ('ring', ring_edges), ('no', no_edges), ('disjoint', disjoint_edges)): for directed, tag in ((True, "directed"), (False, "undirected")): title = "%s %s" % (name, tag) g = graph.dot_graph(vertices, edges, directed=directed, title=title) print(g) lines = g.split('\n') self.assertHeader(lines, title, directed) self.assertVertices(lines[7:], vertices) self.assertEdges(lines[len(vertices) + 7:], edges, directed)
def write_dot_file(basename, edge_list, vertices=None, label=None, dot_file_dir=None, debug=None, **kwargs): s = dot_graph(vertices, edge_list, title=label, **kwargs) if label: # sanitise DN and guid labels basename += '_' + label.translate(None, ', ') filename = os.path.join(dot_file_dir, "%s.dot" % basename) if debug is not None: debug("writing graph to %s" % filename) f = open(filename, 'w') f.write(s) f.close()
def write_dot_file(basename, edge_list, vertices=None, label=None, dot_file_dir=None, debug=None, **kwargs): s = dot_graph(vertices, edge_list, title=label, **kwargs) if label: # sanitise DN and guid labels basename += '_' + label.replace(', ', '') filename = os.path.join(dot_file_dir, "%s.dot" % basename) if debug is not None: debug("writing graph to %s" % filename) f = open(filename, 'w') f.write(s) f.close()
def run(self, H=None, output=None, shorten_names=False, key=True, talk_to_remote=False, sambaopts=None, credopts=None, versionopts=None, color=None, color_scheme=None, utf8=None, format=None, importldif=None, xdot=False): lp = sambaopts.get_loadparm() if importldif is None: creds = credopts.get_credentials(lp, fallback_machine=True) else: creds = None H = self.import_ldif_db(importldif, lp) local_kcc, dsas = self.get_kcc_and_dsas(H, lp, creds) local_dsa_dn = local_kcc.my_dsa_dnstr.split(',', 1)[1] 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 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 '')) # 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 #controls=["search_options:1:2"], controls=["search_options:0:2"], ) for msg in res: msgdn = str(msg.dn) dest_dn = msgdn[msgdn.index(',') + 1:] attested_edges.append((msg['fromServer'][0], dest_dn, ntds_dn)) if importldif and H == self._tmp_fn_to_delete: os.remove(H) os.rmdir(os.path.dirname(H)) # 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) vertices, rodc_status = zip(*sorted(vertices)) if self.calc_output_format(format, output) == 'distance': color_scheme = self.calc_distance_color_scheme(color, color_scheme, output) 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') if not talk_to_remote: # If we are not talking to remote servers, we list all # the connections. graph_edges = edges.keys() title = 'NTDS Connections known to %s' % local_dsa_dn 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 = [] for e, conn in edges.items(): 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' 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) s = distance_matrix(vertices, graph_edges, utf8=utf8, colour=color_scheme, shorten_names=shorten_names, generate_key=key, grouping_function=get_dnstr_site, row_comments=rodc_status) epilog = ''.join(epilog) if epilog: epilog = '\n%sNOTES%s\n%s' % (c_header, c_reset, epilog) self.write('\n%s\n\n%s\n%s' % (title, s, epilog), output) return dot_edges = [] edge_colours = [] edge_styles = [] edge_labels = [] n_servers = len(dsas) for k, e in sorted(edges.items()): 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: title = 'NTDS Connections known to %s' % local_dsa_dn 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) if format == 'xdot': self.call_xdot(s, output) else: self.write(s, output)
def run(self, H=None, output=None, shorten_names=False, key=True, talk_to_remote=False, sambaopts=None, credopts=None, versionopts=None, mode='self', partition=None, color=None, color_scheme=None, utf8=None, format=None, xdot=False): # 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) local_kcc, dsas = self.get_kcc_and_dsas(H, lp, creds) unix_now = local_kcc.unix_now # Allow people to say "--partition=DOMAIN" rather than # "--partition=DC=blah,DC=..." short_partitions, long_partitions = get_partition_maps(local_kcc.samdb) if partition is not None: partition = short_partitions.get(partition.upper(), partition) if partition not in long_partitions: raise CommandError("unknown partition %s" % partition) # 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"]) dns_name = res[0]["dNSHostName"][0] 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() for part, rep in c.items(): if partition is None or part == partition: nc_reps[part]['current'].append((dsa_dn, rep)) for part, rep in n.items(): if partition is None or part == partition: nc_reps[part]['needed'].append((dsa_dn, rep)) all_edges = {'needed': {'to': [], 'from': []}, 'current': {'to': [], 'from': []}} for partname, part in nc_reps.items(): for state, edgelists in all_edges.items(): 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': color_scheme = self.calc_distance_color_scheme(color, color_scheme, output) header_strings = { 'from': "RepsFrom objects for %s", 'to': "RepsTo objects for %s", } for state, edgelists in all_edges.items(): for direction, items in edgelists.items(): part_edges = defaultdict(list) for src, dest, part in items: part_edges[part].append((src, dest)) for part, edges in part_edges.items(): s = distance_matrix(None, edges, utf8=utf8, colour=color_scheme, shorten_names=shorten_names, generate_key=key, grouping_function=get_dnstr_site) 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() for state, edgelist in all_edges.items(): for direction, items in edgelist.items(): 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) if format == 'xdot': self.call_xdot(s, output) else: self.write(s, output)