2015-03-18 07:17:02 +03:00
# 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/>.
2015-04-30 01:39:54 +03:00
import os
2015-03-18 07:17:02 +03:00
import itertools
2017-08-10 06:29:43 +03:00
from samba . graph import dot_graph
2015-06-04 02:16:15 +03:00
2015-03-19 02:03:08 +03:00
2015-04-09 05:45:08 +03:00
def write_dot_file ( basename , edge_list , vertices = None , label = None ,
2017-08-10 06:29:43 +03:00
dot_file_dir = None , debug = None , * * kwargs ) :
s = dot_graph ( vertices , edge_list , title = label , * * kwargs )
2015-03-18 07:17:02 +03:00
if label :
2015-04-30 01:39:54 +03:00
# sanitise DN and guid labels
2018-04-10 03:45:34 +03:00
basename + = ' _ ' + label . replace ( ' , ' , ' ' )
2015-04-30 01:39:54 +03:00
2017-08-10 06:29:43 +03:00
filename = os . path . join ( dot_file_dir , " %s .dot " % basename )
2015-03-18 07:17:02 +03:00
if debug is not None :
2017-08-10 06:29:43 +03:00
debug ( " writing graph to %s " % filename )
f = open ( filename , ' w ' )
f . write ( s )
2015-03-18 07:17:02 +03:00
f . close ( )
2015-03-18 08:23:06 +03:00
class GraphError ( Exception ) :
2015-03-18 07:17:02 +03:00
pass
2015-04-09 05:45:08 +03:00
2015-03-18 07:17:02 +03:00
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 ) :
2015-03-18 08:23:06 +03:00
raise GraphError ( " graph is not fully connected " )
2015-03-18 07:17:02 +03:00
def verify_graph_connected ( edges , vertices , edge_vertices ) :
""" There is a path between any two nodes. """
if not edges :
if len ( vertices ) < = 1 :
return
2018-05-16 06:53:35 +03:00
raise GraphError ( " all vertices are disconnected because "
" there are no edges: " )
2015-03-18 07:17:02 +03:00
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 ]
2015-04-14 05:46:12 +03:00
if remaining_edges or reached != set ( vertices ) :
2018-05-16 06:53:35 +03:00
s = ( " the graph is not connected, "
" as the following vertices are unreachable: \n " )
s + = ' \n ' . join ( v for v in sorted ( vertices )
if v not in reached )
raise GraphError ( s )
2015-03-18 07:17:02 +03:00
2015-04-09 05:31:17 +03:00
def verify_graph_connected_under_edge_failures ( edges , vertices , edge_vertices ) :
""" The graph stays connected when any single edge is removed. """
2018-06-09 11:07:37 +03:00
if len ( edges ) == 0 :
return verify_graph_connected ( edges , vertices , edge_vertices )
2015-04-09 05:31:17 +03:00
for subset in itertools . combinations ( edges , len ( edges ) - 1 ) :
2018-05-16 06:53:35 +03:00
try :
verify_graph_connected ( subset , vertices , edge_vertices )
except GraphError as e :
for edge in edges :
if edge not in subset :
raise GraphError ( " The graph will be disconnected when the "
" connection from %s to %s fails " % edge )
2015-04-09 05:31:17 +03:00
2015-04-09 05:45:08 +03:00
def verify_graph_connected_under_vertex_failures ( edges , vertices ,
edge_vertices ) :
2015-04-09 05:31:17 +03:00
""" The graph stays connected when any single vertex is removed. """
for v in vertices :
sub_vertices = [ x for x in vertices if x is not v ]
sub_edges = [ x for x in edges if v not in x ]
verify_graph_connected ( sub_edges , sub_vertices , sub_vertices )
2015-03-18 07:17:02 +03:00
2015-04-09 05:45:08 +03:00
2015-03-18 07:17:02 +03:00
def verify_graph_forest ( edges , vertices , edge_vertices ) :
2018-05-16 06:53:35 +03:00
""" The graph contains no loops. """
2015-03-18 07:17:02 +03:00
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 :
2015-04-14 05:46:12 +03:00
raise GraphError ( " there is a loop in the graph \n "
" vertices %s \n edges %s \n "
" intersection %s " %
( vertices , edges , intersection ) )
2015-03-18 07:17:02 +03:00
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
2015-04-09 05:45:08 +03:00
2015-03-18 07:17:02 +03:00
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 :
2015-03-18 08:23:06 +03:00
raise GraphError ( " there is a loop in the graph " )
2015-03-18 07:17:02 +03:00
else :
return
def verify_graph_no_lonely_vertices ( edges , vertices , edge_vertices ) :
""" There are no vertices without edges. """
2015-04-14 05:46:12 +03:00
lonely = set ( vertices ) - set ( edge_vertices )
2015-03-18 07:17:02 +03:00
if lonely :
2015-04-09 05:45:08 +03:00
raise GraphError ( " some vertices are not connected: \n %s " %
' \n ' . join ( sorted ( lonely ) ) )
2015-03-18 07:17:02 +03:00
def verify_graph_no_unknown_vertices ( edges , vertices , edge_vertices ) :
""" The edge endpoints contain no vertices that are otherwise unknown. """
2015-04-14 05:46:12 +03:00
unknown = set ( edge_vertices ) - set ( vertices )
2015-03-18 07:17:02 +03:00
if unknown :
2015-04-09 05:45:08 +03:00
raise GraphError ( " some edge vertices are seemingly unknown: \n %s " %
' \n ' . join ( sorted ( unknown ) ) )
2015-03-18 07:17:02 +03:00
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.
2015-04-09 04:55:40 +03:00
Deciding this for sure is NP - complete ( the Hamiltonian path
problem ) , but there are some easy failures that can be detected .
So far we check for :
- leaf nodes
- disjoint subgraphs
2015-04-09 05:31:17 +03:00
- robustness against edge and vertex failure
2015-03-18 07:17:02 +03:00
"""
2015-04-09 04:55:40 +03:00
# a zero or one node graph is OK with no edges.
# The two vertex case is special. Use
# verify_graph_directed_double_ring_or_small() to allow that.
if not edges and len ( vertices ) < = 1 :
2015-03-18 07:17:02 +03:00
return
2015-03-19 02:03:53 +03:00
if len ( edges ) < 2 * len ( vertices ) :
2015-04-09 04:55:40 +03:00
raise GraphError ( " directed double ring requires at least twice "
" as many edges as vertices " )
# Reduce the problem space by looking only at bi-directional links.
half_duplex = set ( edges )
duplex_links = set ( )
for edge in edges :
rev_edge = ( edge [ 1 ] , edge [ 0 ] )
if edge in half_duplex and rev_edge in half_duplex :
duplex_links . add ( edge )
half_duplex . remove ( edge )
half_duplex . remove ( rev_edge )
# the Hamiltonian cycle problem is NP-complete in general, but we
# can cheat a bit and prove a less strong result.
#
# We declutter the graph by replacing nodes with edges connecting
# their neighbours.
#
# A-B-C --> A-C
#
# -A-B-C- --> -A--C-
# `D_ `D'_
#
# In the end there should be a single 2 vertex graph.
edge_map = { }
for a , b in duplex_links :
edge_map . setdefault ( a , set ( ) ) . add ( b )
edge_map . setdefault ( b , set ( ) ) . add ( a )
# an easy to detect failure is a lonely leaf node
for vertex , neighbours in edge_map . items ( ) :
if len ( neighbours ) == 1 :
raise GraphError ( " wanted double directed ring, found a leaf node "
" ( %s ) " % vertex )
2018-04-10 06:42:42 +03:00
for vertex in list ( edge_map . keys ( ) ) :
2015-04-09 04:55:40 +03:00
nset = edge_map [ vertex ]
if not nset :
continue
for n in nset :
n_neighbours = edge_map [ n ]
n_neighbours . remove ( vertex )
n_neighbours . update ( x for x in nset if x != n )
del edge_map [ vertex ]
if len ( edge_map ) > 1 :
raise GraphError ( " wanted double directed ring, but "
" this looks like a split graph \n "
" ( %s can ' t reach each other) " %
' , ' . join ( edge_map . keys ( ) ) )
2015-03-18 07:17:02 +03:00
2015-04-09 05:31:17 +03:00
verify_graph_connected_under_edge_failures ( duplex_links , vertices ,
edge_vertices )
verify_graph_connected_under_vertex_failures ( duplex_links , vertices ,
edge_vertices )
2015-03-18 07:17:02 +03:00
def verify_graph_directed_double_ring_or_small ( edges , vertices , edge_vertices ) :
2015-04-09 05:31:17 +03:00
""" This performs the directed_double_ring test but makes special
concessions for small rings where the strict rules don ' t really
apply . """
2015-04-09 04:55:40 +03:00
if len ( vertices ) < 2 :
2015-03-18 07:17:02 +03:00
return
2015-04-09 04:55:40 +03:00
if len ( vertices ) == 2 :
""" With 2 nodes there should be a single link in each directions. """
if ( len ( edges ) == 2 and
edges [ 0 ] [ 0 ] == edges [ 1 ] [ 1 ] and
edges [ 0 ] [ 1 ] == edges [ 1 ] [ 0 ] ) :
return
raise GraphError ( " A two vertex graph should have an edge each way. " )
2015-03-18 07:17:02 +03:00
return verify_graph_directed_double_ring ( edges , vertices , edge_vertices )
2018-03-15 02:01:10 +03:00
def verify_graph ( edges , vertices = None , directed = False , properties = ( ) ) :
2015-03-18 07:17:02 +03:00
errors = [ ]
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 )
for p in properties :
fn = ' verify_graph_ %s ' % p
2018-03-15 02:01:10 +03:00
f = globals ( ) [ fn ]
2015-03-18 07:17:02 +03:00
try :
f ( edges , vertices , edge_vertices )
2018-02-14 00:18:36 +03:00
except GraphError as e :
2018-05-15 05:40:36 +03:00
errors . append ( ( p , e , f . __doc__ ) )
2015-03-18 07:17:02 +03:00
2018-03-15 02:01:10 +03:00
return errors
2015-03-18 07:17:02 +03:00
2015-04-30 01:39:54 +03:00
def verify_and_dot ( basename , edges , vertices = None , label = None ,
reformat_labels = True , directed = False ,
properties = ( ) , fatal = True , debug = None ,
verify = True , dot_file_dir = None ,
edge_colors = None , edge_labels = None ,
vertex_colors = None ) :
2015-03-18 07:17:02 +03:00
2015-04-30 01:39:54 +03:00
if dot_file_dir is not None :
2015-04-09 05:45:08 +03:00
write_dot_file ( basename , edges , vertices = vertices , label = label ,
2015-04-30 01:39:54 +03:00
dot_file_dir = dot_file_dir ,
reformat_labels = reformat_labels , directed = directed ,
debug = debug , edge_colors = edge_colors ,
2015-04-09 05:45:08 +03:00
edge_labels = edge_labels , vertex_colors = vertex_colors )
2015-03-18 07:17:02 +03:00
2018-03-15 02:01:10 +03:00
if verify :
errors = verify_graph ( edges , vertices ,
properties = properties )
if errors :
title = ' %s %s ' % ( basename , label or ' ' )
2018-04-19 07:39:06 +03:00
debug ( " %s FAILED: " % title )
2018-05-15 05:40:36 +03:00
for p , e , doc in errors :
2018-04-19 07:39:06 +03:00
debug ( " %18s : %s " % ( p , e ) )
2018-03-15 02:01:10 +03:00
if fatal :
raise GraphError ( " The ' %s ' graph lacks the following "
" properties: \n %s " %
2018-05-15 05:40:36 +03:00
( title , ' \n ' . join ( ' %s : %s ' % ( p , e )
for p , e , doc in errors ) ) )
2018-03-15 02:01:10 +03:00
2015-04-09 05:31:17 +03:00
2015-03-18 07:17:02 +03:00
def list_verify_tests ( ) :
for k , v in sorted ( globals ( ) . items ( ) ) :
if k . startswith ( ' verify_graph_ ' ) :
2018-03-09 16:53:45 +03:00
print ( k . replace ( ' verify_graph_ ' , ' ' ) )
2015-03-18 07:17:02 +03:00
if v . __doc__ :
2018-04-19 07:39:06 +03:00
print ( ' %s ' % ( v . __doc__ . rstrip ( ) ) )
2015-03-18 07:17:02 +03:00
else :
2018-03-09 16:53:45 +03:00
print ( )