1
0
mirror of https://github.com/samba-team/samba.git synced 2025-01-08 21:18:16 +03:00

KCC: shift graph plotting and verification into a separate module

These might possibly be useful outside the KCC context, and the don't
rely on the rest of kcc_utils.

Signed-off-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Reviewed-by: Garming Sam <garming@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
This commit is contained in:
Douglas Bagnall 2015-03-18 17:17:02 +13:00 committed by Andrew Bartlett
parent d383cd6f5e
commit 195b9f4c79
3 changed files with 299 additions and 274 deletions

298
python/samba/graph_utils.py Normal file
View File

@ -0,0 +1,298 @@
# Graph topology utilities, used by KCC
#
# Copyright (C) Andrew Bartlett 2015
#
# Copyright goes to Andrew Bartlett, but the actual work was performed
# by Douglas Bagnall and Garming Sam.
#
# 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 sys
import itertools
def write_dot_file(basename, edge_list, vertices=None, label=None, destdir=None,
reformat_labels=True, directed=False, debug=None):
from tempfile import NamedTemporaryFile
if label:
basename += '_' + label.translate(None, ', ') #fix DN, guid labels
f = NamedTemporaryFile(suffix='.dot', prefix=basename + '_', delete=False, dir=destdir)
if debug is not None:
debug(f.name)
graphname = ''.join(x for x in basename if x.isalnum())
print >>f, '%s %s {' % ('digraph' if directed else 'graph', graphname)
print >>f, 'label="%s";\nfontsize=20;' % (label or graphname)
if vertices:
for v in vertices:
if reformat_labels:
v = v.replace(',', '\\n')
print >>f, '"%s";' % (v,)
for a, b in edge_list:
if reformat_labels:
a = a.replace(',', '\\n')
b = b.replace(',', '\\n')
line = '->' if directed else '--'
print >>f, '"%s" %s "%s";' % (a, line, b)
print >>f, '}'
f.close()
class KCCGraphError(Exception):
pass
def verify_graph_complete(edges, vertices, edge_vertices):
"""The graph is complete, which is to say there is an edge between
every pair of nodes."""
for v in vertices:
remotes = set()
for a, b in edges:
if a == v:
remotes.add(b)
elif b == v:
remotes.add(a)
if len(remotes) + 1 != len(vertices):
raise KCCGraphError("graph is not fully connected")
def verify_graph_connected(edges, vertices, edge_vertices):
"""There is a path between any two nodes."""
if not edges:
if len(vertices) <= 1:
return
raise KCCGraphError("disconnected vertices were found:\nvertices: %s\n edges: %s" %
(sorted(vertices), sorted(edges)))
remaining_edges = list(edges)
reached = set(remaining_edges.pop())
while True:
doomed = []
for i, e in enumerate(remaining_edges):
a, b = e
if a in reached:
reached.add(b)
doomed.append(i)
elif b in reached:
reached.add(a)
doomed.append(i)
if not doomed:
break
for i in reversed(doomed):
del remaining_edges[i]
if remaining_edges or reached != vertices:
raise KCCGraphError("graph is not connected:\nvertices: %s\n edges: %s" %
(sorted(vertices), sorted(edges)))
def verify_graph_forest(edges, vertices, edge_vertices):
"""The graph contains no loops. A forest that is also connected is a
tree."""
trees = [set(e) for e in edges]
while True:
for a, b in itertools.combinations(trees, 2):
intersection = a & b
if intersection:
if len(intersection) == 1:
a |= b
trees.remove(b)
break
else:
raise KCCGraphError("there is a loop in the graph")
else:
# no break in itertools.combinations loop means no
# further mergers, so we're done.
#
# XXX here we also know whether it is a tree or a
# forest by len(trees) but the connected test already
# tells us that.
return
def verify_graph_multi_edge_forest(edges, vertices, edge_vertices):
"""This allows a forest with duplicate edges. That is if multiple
edges go between the same two vertices, they are treated as a
single edge by this test.
e.g.:
o
pass: o-o=o o=o (|) fail: o-o
`o o `o'
"""
unique_edges = set(edges)
trees = [set(e) for e in unique_edges]
while True:
for a, b in itertools.combinations(trees, 2):
intersection = a & b
if intersection:
if len(intersection) == 1:
a |= b
trees.remove(b)
break
else:
raise KCCGraphError("there is a loop in the graph")
else:
return
def verify_graph_forest_of_rings(edges, vertices, edge_vertices):
"""The graph should consist of clusters of node connected in rings,
with the connections between the cdlusters forming a forest."""
pass
def verify_graph_no_lonely_vertices(edges, vertices, edge_vertices):
"""There are no vertices without edges."""
lonely = vertices - edge_vertices
if lonely:
raise KCCGraphError("some vertices are not connected:\n%s" % '\n'.join(sorted(lonely)))
def verify_graph_no_unknown_vertices(edges, vertices, edge_vertices):
"""The edge endpoints contain no vertices that are otherwise unknown."""
unknown = edge_vertices - vertices
if unknown:
raise KCCGraphError("some edge vertices are seemingly unknown:\n%s" % '\n'.join(sorted(unknown)))
def verify_graph_directed_double_ring(edges, vertices, edge_vertices):
"""Each node has at least two directed edges leaving it, and two
arriving. The edges work in pairs that have the same end points
but point in opposite directions. The pairs form a path that
touches every vertex and form a loop.
There might be other connections that *aren't* part of the ring.
"""
#XXX possibly the 1 and 2 vertex cases are special cases.
if not edges:
return
if len(edges) < 2* len(vertices):
raise KCCGraphError("directed double ring requires at least twice as many edges as vertices")
exits = {}
for start, end in edges:
s = exits.setdefault(start, [])
s.append(end)
try:
#follow both paths at once -- they should be the same length
#XXX there is probably a simpler way.
forwards, backwards = exits[start]
fprev, bprev = (start, start)
f_path = [start]
b_path = [start]
for i in range(len(vertices)):
a, b = exits[forwards]
if a == fprev:
fnext = b
else:
fnext = a
f_path.append(forwards)
fprev = forwards
forwards = fnext
a, b = exits[backwards]
if a == bprev:
bnext = b
else:
bnext = a
b_path.append(backwards)
bprev = backwards
backwards = bnext
except ValueError, e:
raise KCCGraphError("wrong number of exits '%s'" % e)
f_set = set(f_path)
b_set = set(b_path)
if (f_path != list(reversed(b_path)) or
len(f_path) != len(f_set) + 1 or
len(f_set) != len(vertices)):
raise KCCGraphError("doesn't seem like a double ring to me!")
def verify_graph_directed_double_ring_or_small(edges, vertices, edge_vertices):
if len(vertices) < 3:
return
return verify_graph_directed_double_ring(edges, vertices, edge_vertices)
def verify_graph(title, edges, vertices=None, directed=False, properties=(), fatal=True,
debug=None):
errors = []
if debug is None:
def debug(*args): pass
debug("%sStarting verify_graph for %s%s%s" % (PURPLE, MAGENTA, title, C_NORMAL))
properties = [x.replace(' ', '_') for x in properties]
edge_vertices = set()
for a, b in edges:
edge_vertices.add(a)
edge_vertices.add(b)
if vertices is None:
vertices = edge_vertices
else:
vertices = set(vertices)
if vertices != edge_vertices:
debug("vertices in edges don't match given vertices:\n %s != %s" %
(sorted(edge_vertices), sorted(vertices)))
for p in properties:
fn = 'verify_graph_%s' % p
try:
f = globals()[fn]
except KeyError:
errors.append((p, "There is no verification check for '%s'" % p))
try:
f(edges, vertices, edge_vertices)
debug(" %s%18s:%s verified!" % (DARK_GREEN, p, C_NORMAL))
except KCCGraphError, e:
errors.append((p, e))
if errors:
if fatal:
raise KCCGraphError("The '%s' graph lacks the following properties:\n%s" %
(title, '\n'.join('%s: %s' % x for x in errors)))
debug(("%s%s%s FAILED:" % (MAGENTA, title, RED)))
for p, e in errors:
debug(" %18s: %s%s%s" %(p, DARK_YELLOW, e, RED))
debug(C_NORMAL)
def verify_and_dot(basename, edges, vertices=None, label=None, destdir=None,
reformat_labels=True, directed=False, properties=(), fatal=True,
debug=None, verify=True, dot_files=False):
title = '%s %s' % (basename, label or '')
if verify:
verify_graph(title, edges, vertices, properties=properties, fatal=fatal,
debug=debug)
if dot_files:
write_dot_file(basename, edges, vertices=vertices, label=label, destdir=destdir,
reformat_labels=reformat_labels, directed=directed, debug=debug)
def list_verify_tests():
for k, v in sorted(globals().items()):
if k.startswith('verify_graph_'):
print k.replace('verify_graph_', '')
if v.__doc__:
print ' %s%s%s' %(GREY, v.__doc__, C_NORMAL)
else:
print

View File

@ -2358,277 +2358,3 @@ def combine_repl_info(info_a, info_b, info_c):
return True
def write_dot_file(basename, edge_list, vertices=None, label=None, destdir=None,
reformat_labels=True, directed=False, debug=None):
from tempfile import NamedTemporaryFile
if label:
basename += '_' + label.translate(None, ', ') #fix DN, guid labels
f = NamedTemporaryFile(suffix='.dot', prefix=basename + '_', delete=False, dir=destdir)
if debug is not None:
debug(f.name)
graphname = ''.join(x for x in basename if x.isalnum())
print >>f, '%s %s {' % ('digraph' if directed else 'graph', graphname)
print >>f, 'label="%s";\nfontsize=20;' % (label or graphname)
if vertices:
for v in vertices:
if reformat_labels:
v = v.replace(',', '\\n')
print >>f, '"%s";' % (v,)
for a, b in edge_list:
if reformat_labels:
a = a.replace(',', '\\n')
b = b.replace(',', '\\n')
line = '->' if directed else '--'
print >>f, '"%s" %s "%s";' % (a, line, b)
print >>f, '}'
f.close()
class KCCGraphError(Exception):
pass
def verify_graph_complete(edges, vertices, edge_vertices):
"""The graph is complete, which is to say there is an edge between
every pair of nodes."""
for v in vertices:
remotes = set()
for a, b in edges:
if a == v:
remotes.add(b)
elif b == v:
remotes.add(a)
if len(remotes) + 1 != len(vertices):
raise KCCGraphError("graph is not fully connected")
def verify_graph_connected(edges, vertices, edge_vertices):
"""There is a path between any two nodes."""
if not edges:
if len(vertices) <= 1:
return
raise KCCGraphError("disconnected vertices were found:\nvertices: %s\n edges: %s" %
(sorted(vertices), sorted(edges)))
remaining_edges = list(edges)
reached = set(remaining_edges.pop())
while True:
doomed = []
for i, e in enumerate(remaining_edges):
a, b = e
if a in reached:
reached.add(b)
doomed.append(i)
elif b in reached:
reached.add(a)
doomed.append(i)
if not doomed:
break
for i in reversed(doomed):
del remaining_edges[i]
if remaining_edges or reached != vertices:
raise KCCGraphError("graph is not connected:\nvertices: %s\n edges: %s" %
(sorted(vertices), sorted(edges)))
def verify_graph_forest(edges, vertices, edge_vertices):
"""The graph contains no loops. A forest that is also connected is a
tree."""
trees = [set(e) for e in edges]
while True:
for a, b in itertools.combinations(trees, 2):
intersection = a & b
if intersection:
if len(intersection) == 1:
a |= b
trees.remove(b)
break
else:
raise KCCGraphError("there is a loop in the graph")
else:
# no break in itertools.combinations loop means no
# further mergers, so we're done.
#
# XXX here we also know whether it is a tree or a
# forest by len(trees) but the connected test already
# tells us that.
return
def verify_graph_multi_edge_forest(edges, vertices, edge_vertices):
"""This allows a forest with duplicate edges. That is if multiple
edges go between the same two vertices, they are treated as a
single edge by this test.
e.g.:
o
pass: o-o=o o=o (|) fail: o-o
`o o `o'
"""
unique_edges = set(edges)
trees = [set(e) for e in unique_edges]
while True:
for a, b in itertools.combinations(trees, 2):
intersection = a & b
if intersection:
if len(intersection) == 1:
a |= b
trees.remove(b)
break
else:
raise KCCGraphError("there is a loop in the graph")
else:
return
def verify_graph_forest_of_rings(edges, vertices, edge_vertices):
"""The graph should consist of clusters of node connected in rings,
with the connections between the cdlusters forming a forest."""
pass
def verify_graph_no_lonely_vertices(edges, vertices, edge_vertices):
"""There are no vertices without edges."""
lonely = vertices - edge_vertices
if lonely:
raise KCCGraphError("some vertices are not connected:\n%s" % '\n'.join(sorted(lonely)))
def verify_graph_no_unknown_vertices(edges, vertices, edge_vertices):
"""The edge endpoints contain no vertices that are otherwise unknown."""
unknown = edge_vertices - vertices
if unknown:
raise KCCGraphError("some edge vertices are seemingly unknown:\n%s" % '\n'.join(sorted(unknown)))
def verify_graph_directed_double_ring(edges, vertices, edge_vertices):
"""Each node has at least two directed edges leaving it, and two
arriving. The edges work in pairs that have the same end points
but point in opposite directions. The pairs form a path that
touches every vertex and form a loop.
There might be other connections that *aren't* part of the ring.
"""
#XXX possibly the 1 and 2 vertex cases are special cases.
if not edges:
return
if len(edges) < 2* len(vertices):
raise KCCGraphError("directed double ring requires at least twice as many edges as vertices")
exits = {}
for start, end in edges:
s = exits.setdefault(start, [])
s.append(end)
try:
#follow both paths at once -- they should be the same length
#XXX there is probably a simpler way.
forwards, backwards = exits[start]
fprev, bprev = (start, start)
f_path = [start]
b_path = [start]
for i in range(len(vertices)):
a, b = exits[forwards]
if a == fprev:
fnext = b
else:
fnext = a
f_path.append(forwards)
fprev = forwards
forwards = fnext
a, b = exits[backwards]
if a == bprev:
bnext = b
else:
bnext = a
b_path.append(backwards)
bprev = backwards
backwards = bnext
except ValueError, e:
raise KCCGraphError("wrong number of exits '%s'" % e)
f_set = set(f_path)
b_set = set(b_path)
if (f_path != list(reversed(b_path)) or
len(f_path) != len(f_set) + 1 or
len(f_set) != len(vertices)):
raise KCCGraphError("doesn't seem like a double ring to me!")
def verify_graph_directed_double_ring_or_small(edges, vertices, edge_vertices):
if len(vertices) < 3:
return
return verify_graph_directed_double_ring(edges, vertices, edge_vertices)
def verify_graph(title, edges, vertices=None, directed=False, properties=(), fatal=True,
debug=None):
errors = []
if debug is None:
def debug(*args): pass
debug("%sStarting verify_graph for %s%s%s" % (PURPLE, MAGENTA, title, C_NORMAL))
properties = [x.replace(' ', '_') for x in properties]
edge_vertices = set()
for a, b in edges:
edge_vertices.add(a)
edge_vertices.add(b)
if vertices is None:
vertices = edge_vertices
else:
vertices = set(vertices)
if vertices != edge_vertices:
debug("vertices in edges don't match given vertices:\n %s != %s" %
(sorted(edge_vertices), sorted(vertices)))
for p in properties:
fn = 'verify_graph_%s' % p
try:
f = globals()[fn]
except KeyError:
errors.append((p, "There is no verification check for '%s'" % p))
try:
f(edges, vertices, edge_vertices)
debug(" %s%18s:%s verified!" % (DARK_GREEN, p, C_NORMAL))
except KCCGraphError, e:
errors.append((p, e))
if errors:
if fatal:
raise KCCGraphError("The '%s' graph lacks the following properties:\n%s" %
(title, '\n'.join('%s: %s' % x for x in errors)))
debug(("%s%s%s FAILED:" % (MAGENTA, title, RED)))
for p, e in errors:
debug(" %18s: %s%s%s" %(p, DARK_YELLOW, e, RED))
debug(C_NORMAL)
def verify_and_dot(basename, edges, vertices=None, label=None, destdir=None,
reformat_labels=True, directed=False, properties=(), fatal=True,
debug=None, verify=True, dot_files=False):
title = '%s %s' % (basename, label or '')
if verify:
verify_graph(title, edges, vertices, properties=properties, fatal=fatal,
debug=debug)
if dot_files:
write_dot_file(basename, edges, vertices=vertices, label=label, destdir=destdir,
reformat_labels=reformat_labels, directed=directed, debug=debug)
def list_verify_tests():
for k, v in sorted(globals().items()):
if k.startswith('verify_graph_'):
print k.replace('verify_graph_', '')
if v.__doc__:
print ' %s%s%s' %(GREY, v.__doc__, C_NORMAL)
else:
print

View File

@ -56,6 +56,7 @@ from samba.auth import system_session
from samba.samdb import SamDB
from samba.dcerpc import drsuapi
from samba.kcc_utils import *
from samba.graph_utils import *
class KCC(object):
"""The Knowledge Consistency Checker class.