1 # Graph topology utilities, used by KCC
3 # Copyright (C) Andrew Bartlett 2015
5 # Copyright goes to Andrew Bartlett, but the actual work was performed
6 # by Douglas Bagnall and Garming Sam.
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 from samba.kcc.debug import null_debug, PURPLE, MAGENTA, DARK_YELLOW, RED
25 from samba.kcc.debug import DARK_GREEN, C_NORMAL, GREY
26 from samba.graph import dot_graph
29 def write_dot_file(basename, edge_list, vertices=None, label=None,
30 dot_file_dir=None, debug=None, **kwargs):
31 s = dot_graph(vertices, edge_list, title=label, **kwargs)
33 # sanitise DN and guid labels
34 basename += '_' + label.translate(None, ', ')
36 filename = os.path.join(dot_file_dir, "%s.dot" % basename)
38 debug("writing graph to %s" % filename)
39 f = open(filename, 'w')
44 class GraphError(Exception):
48 def verify_graph_complete(edges, vertices, edge_vertices):
49 """The graph is complete, which is to say there is an edge between
50 every pair of nodes."""
58 if len(remotes) + 1 != len(vertices):
59 raise GraphError("graph is not fully connected")
62 def verify_graph_connected(edges, vertices, edge_vertices):
63 """There is a path between any two nodes."""
65 if len(vertices) <= 1:
67 raise GraphError("disconnected vertices were found:\n"
68 "vertices: %s\n edges: %s" %
69 (sorted(vertices), sorted(edges)))
71 remaining_edges = list(edges)
72 reached = set(remaining_edges.pop())
75 for i, e in enumerate(remaining_edges):
85 for i in reversed(doomed):
86 del remaining_edges[i]
88 if remaining_edges or reached != set(vertices):
89 raise GraphError("graph is not connected:\n vertices: %s\n edges: %s\n"
90 " reached: %s\n remaining edges: %s" %
91 (sorted(vertices), sorted(edges),
92 sorted(reached), sorted(remaining_edges)))
95 def verify_graph_connected_under_edge_failures(edges, vertices, edge_vertices):
96 """The graph stays connected when any single edge is removed."""
97 for subset in itertools.combinations(edges, len(edges) - 1):
98 verify_graph_connected(subset, vertices, edge_vertices)
101 def verify_graph_connected_under_vertex_failures(edges, vertices,
103 """The graph stays connected when any single vertex is removed."""
105 sub_vertices = [x for x in vertices if x is not v]
106 sub_edges = [x for x in edges if v not in x]
107 verify_graph_connected(sub_edges, sub_vertices, sub_vertices)
110 def verify_graph_forest(edges, vertices, edge_vertices):
111 """The graph contains no loops. A forest that is also connected is a
113 trees = [set(e) for e in edges]
115 for a, b in itertools.combinations(trees, 2):
118 if len(intersection) == 1:
123 raise GraphError("there is a loop in the graph\n"
124 " vertices %s\n edges %s\n"
126 (vertices, edges, intersection))
128 # no break in itertools.combinations loop means no
129 # further mergers, so we're done.
131 # XXX here we also know whether it is a tree or a
132 # forest by len(trees) but the connected test already
137 def verify_graph_multi_edge_forest(edges, vertices, edge_vertices):
138 """This allows a forest with duplicate edges. That is if multiple
139 edges go between the same two vertices, they are treated as a
140 single edge by this test.
144 pass: o-o=o o=o (|) fail: o-o
147 unique_edges = set(edges)
148 trees = [set(e) for e in unique_edges]
150 for a, b in itertools.combinations(trees, 2):
153 if len(intersection) == 1:
158 raise GraphError("there is a loop in the graph")
163 def verify_graph_no_lonely_vertices(edges, vertices, edge_vertices):
164 """There are no vertices without edges."""
165 lonely = set(vertices) - set(edge_vertices)
167 raise GraphError("some vertices are not connected:\n%s" %
168 '\n'.join(sorted(lonely)))
171 def verify_graph_no_unknown_vertices(edges, vertices, edge_vertices):
172 """The edge endpoints contain no vertices that are otherwise unknown."""
173 unknown = set(edge_vertices) - set(vertices)
175 raise GraphError("some edge vertices are seemingly unknown:\n%s" %
176 '\n'.join(sorted(unknown)))
179 def verify_graph_directed_double_ring(edges, vertices, edge_vertices):
180 """Each node has at least two directed edges leaving it, and two
181 arriving. The edges work in pairs that have the same end points
182 but point in opposite directions. The pairs form a path that
183 touches every vertex and form a loop.
185 There might be other connections that *aren't* part of the ring.
187 Deciding this for sure is NP-complete (the Hamiltonian path
188 problem), but there are some easy failures that can be detected.
192 - robustness against edge and vertex failure
194 # a zero or one node graph is OK with no edges.
195 # The two vertex case is special. Use
196 # verify_graph_directed_double_ring_or_small() to allow that.
197 if not edges and len(vertices) <= 1:
199 if len(edges) < 2 * len(vertices):
200 raise GraphError("directed double ring requires at least twice "
201 "as many edges as vertices")
203 # Reduce the problem space by looking only at bi-directional links.
204 half_duplex = set(edges)
207 rev_edge = (edge[1], edge[0])
208 if edge in half_duplex and rev_edge in half_duplex:
209 duplex_links.add(edge)
210 half_duplex.remove(edge)
211 half_duplex.remove(rev_edge)
213 # the Hamiltonian cycle problem is NP-complete in general, but we
214 # can cheat a bit and prove a less strong result.
216 # We declutter the graph by replacing nodes with edges connecting
224 # In the end there should be a single 2 vertex graph.
227 for a, b in duplex_links:
228 edge_map.setdefault(a, set()).add(b)
229 edge_map.setdefault(b, set()).add(a)
231 # an easy to detect failure is a lonely leaf node
232 for vertex, neighbours in edge_map.items():
233 if len(neighbours) == 1:
234 raise GraphError("wanted double directed ring, found a leaf node"
237 for vertex in edge_map.keys():
238 nset = edge_map[vertex]
242 n_neighbours = edge_map[n]
243 n_neighbours.remove(vertex)
244 n_neighbours.update(x for x in nset if x != n)
247 if len(edge_map) > 1:
248 raise GraphError("wanted double directed ring, but "
249 "this looks like a split graph\n"
250 "(%s can't reach each other)" %
251 ', '.join(edge_map.keys()))
253 verify_graph_connected_under_edge_failures(duplex_links, vertices,
255 verify_graph_connected_under_vertex_failures(duplex_links, vertices,
259 def verify_graph_directed_double_ring_or_small(edges, vertices, edge_vertices):
260 """This performs the directed_double_ring test but makes special
261 concessions for small rings where the strict rules don't really
263 if len(vertices) < 2:
265 if len(vertices) == 2:
266 """With 2 nodes there should be a single link in each directions."""
267 if (len(edges) == 2 and
268 edges[0][0] == edges[1][1] and
269 edges[0][1] == edges[1][0]):
271 raise GraphError("A two vertex graph should have an edge each way.")
273 return verify_graph_directed_double_ring(edges, vertices, edge_vertices)
276 def verify_graph(title, edges, vertices=None, directed=False, properties=(),
277 fatal=True, debug=null_debug):
279 debug("%sStarting verify_graph for %s%s%s" % (PURPLE, MAGENTA, title,
282 properties = [x.replace(' ', '_') for x in properties]
284 edge_vertices = set()
290 vertices = edge_vertices
292 vertices = set(vertices)
293 if vertices != edge_vertices:
294 debug("vertices in edges don't match given vertices:\n %s != %s" %
295 (sorted(edge_vertices), sorted(vertices)))
298 fn = 'verify_graph_%s' % p
302 errors.append((p, "There is no verification check for '%s'" % p))
304 f(edges, vertices, edge_vertices)
305 debug(" %s%18s:%s verified!" % (DARK_GREEN, p, C_NORMAL))
306 except GraphError, e:
307 errors.append((p, e))
311 raise GraphError("The '%s' graph lacks the following properties:"
313 (title, '\n'.join('%s: %s' % x for x in errors)))
314 debug(("%s%s%s FAILED:" % (MAGENTA, title, RED)))
316 debug(" %18s: %s%s%s" % (p, DARK_YELLOW, e, RED))
320 def verify_and_dot(basename, edges, vertices=None, label=None,
321 reformat_labels=True, directed=False,
322 properties=(), fatal=True, debug=None,
323 verify=True, dot_file_dir=None,
324 edge_colors=None, edge_labels=None,
327 title = '%s %s' % (basename, label or '')
329 verify_graph(title, edges, vertices, properties=properties,
330 fatal=fatal, debug=debug)
331 if dot_file_dir is not None:
332 write_dot_file(basename, edges, vertices=vertices, label=label,
333 dot_file_dir=dot_file_dir,
334 reformat_labels=reformat_labels, directed=directed,
335 debug=debug, edge_colors=edge_colors,
336 edge_labels=edge_labels, vertex_colors=vertex_colors)
339 def list_verify_tests():
340 for k, v in sorted(globals().items()):
341 if k.startswith('verify_graph_'):
342 print k.replace('verify_graph_', '')
344 print ' %s%s%s' % (GREY, v.__doc__.rstrip(), C_NORMAL)