summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPetr Vobornik <pvoborni@redhat.com>2015-06-17 13:33:24 +0200
committerPetr Vobornik <pvoborni@redhat.com>2015-06-29 17:11:08 +0200
commit659b88b8205ef403aa9162453472e4731d93d13b (patch)
tree0ce64c9147f4f29fcb6c641fdd6ec933dc67f759
parentdcb6916a3b0601e33b08e12aeb25357efed6812b (diff)
downloadfreeipa-659b88b8205ef403aa9162453472e4731d93d13b.tar.gz
freeipa-659b88b8205ef403aa9162453472e4731d93d13b.tar.xz
freeipa-659b88b8205ef403aa9162453472e4731d93d13b.zip
topology: check topology in ipa-replica-manage del
ipa-replica-manage del now: - checks the whole current topology(before deletion), reports issues - simulates deletion of server and checks the topology again, reports issues Asks admin if he wants to continue with the deletion if any errors are found. https://fedorahosted.org/freeipa/ticket/4302 Reviewed-By: David Kupka <dkupka@redhat.com>
-rwxr-xr-xinstall/tools/ipa-replica-manage48
-rw-r--r--ipalib/util.py51
-rw-r--r--ipapython/graph.py73
3 files changed, 166 insertions, 6 deletions
diff --git a/install/tools/ipa-replica-manage b/install/tools/ipa-replica-manage
index 57e30bc54..71eb992f9 100755
--- a/install/tools/ipa-replica-manage
+++ b/install/tools/ipa-replica-manage
@@ -35,6 +35,7 @@ from ipaserver.plugins import ldap2
from ipapython import version, ipaldap
from ipalib import api, errors, util
from ipalib.constants import CACERT
+from ipalib.util import create_topology_graph, get_topology_connection_errors
from ipapython.ipa_log_manager import *
from ipapython.dn import DN
from ipapython.config import IPAOptionParser
@@ -566,11 +567,46 @@ def check_last_link(delrepl, realm, dirman_passwd, force):
return None
def check_last_link_managed(api, masters, hostname, force):
- # segments = api.Command.topologysegment_find(u'realm', sizelimit=0).get('result')
- # replica_names = [m.single_value('cn') for m in masters]
- # orphaned = []
- # TODO add proper graph traversing algorithm here
- return None
+ """
+ Check if 'hostname' is safe to delete.
+
+ :returns: list of errors after future deletion
+ """
+
+ segments = api.Command.topologysegment_find(u'realm', sizelimit=0).get('result')
+ graph = create_topology_graph(masters, segments)
+
+ # check topology before removal
+ orig_errors = get_topology_connection_errors(graph)
+ if orig_errors:
+ print "Current topology is disconnected:"
+ print "Changes are not replicated to all servers and data are probably inconsistent."
+ print "You need to add segments to reconnect the topology."
+ print_connect_errors(orig_errors)
+
+ # after removal
+ graph.remove_vertex(hostname)
+ new_errors = get_topology_connection_errors(graph)
+ if new_errors:
+ print "WARNING: Topology after removal of %s will be disconnected." % hostname
+ print "Changes will not be replicated to all servers and data will become inconsistent."
+ print "You need to add segments to prevent disconnection of the topology."
+ print "Errors in topology after removal:"
+ print_connect_errors(new_errors)
+
+ if orig_errors or new_errors:
+ if not force:
+ sys.exit("Aborted")
+ else:
+ print "Forcing removal of %s" % hostname
+
+ return new_errors
+
+def print_connect_errors(errors):
+ for error in errors:
+ print "Topology does not allow server %s to replicate with servers:" % error[0]
+ for srv in error[2]:
+ print " %s" % srv
def enforce_host_existence(host, message=None):
if host is not None and not ipautil.host_exists(host):
@@ -680,7 +716,7 @@ def del_master_managed(realm, hostname, options):
masters = api.Command.server_find('', sizelimit=0)['result']
# 3. Check topology
- orphans = check_last_link_managed(api, masters, hostname, options.force)
+ check_last_link_managed(api, masters, hostname, options.force)
# 4. Check that we are not leaving the installation without CA and/or DNS
# And pick new CA master.
diff --git a/ipalib/util.py b/ipalib/util.py
index 44478a2d1..75797229b 100644
--- a/ipalib/util.py
+++ b/ipalib/util.py
@@ -42,6 +42,7 @@ from ipalib.text import _
from ipapython.ssh import SSHPublicKey
from ipapython.dn import DN, RDN
from ipapython.dnsutil import DNSName
+from ipapython.graph import Graph
def json_serialize(obj):
@@ -780,3 +781,53 @@ def validate_idna_domain(value):
if error:
raise ValueError(error)
+
+
+def create_topology_graph(masters, segments):
+ """
+ Create an oriented graph from topology defined by masters and segments.
+
+ :param masters
+ :param segments
+ :returns: Graph
+ """
+ graph = Graph()
+
+ for m in masters:
+ graph.add_vertex(m['cn'][0])
+
+ for s in segments:
+ direction = s['iparepltoposegmentdirection'][0]
+ left = s['iparepltoposegmentleftnode'][0]
+ right = s['iparepltoposegmentrightnode'][0]
+ try:
+ if direction == u'both':
+ graph.add_edge(left, right)
+ graph.add_edge(right, left)
+ elif direction == u'left-right':
+ graph.add_edge(left, right)
+ elif direction == u'right-left':
+ graph.add_edge(right, left)
+ except ValueError: # ignore segments with deleted master
+ pass
+
+ return graph
+
+
+def get_topology_connection_errors(graph):
+ """
+ Traverse graph from each master and find out which masters are not
+ reachable.
+
+ :param graph: topology graph where vertices are masters
+ :returns: list of errors, error is: (master, visited, not_visited)
+ """
+ connect_errors = []
+ master_cns = list(graph.vertices)
+ master_cns.sort()
+ for m in master_cns:
+ visited = graph.bfs(m)
+ not_visited = graph.vertices - visited
+ if not_visited:
+ connect_errors.append((m, list(visited), list(not_visited)))
+ return connect_errors
diff --git a/ipapython/graph.py b/ipapython/graph.py
new file mode 100644
index 000000000..20b612548
--- /dev/null
+++ b/ipapython/graph.py
@@ -0,0 +1,73 @@
+#
+# Copyright (C) 2015 FreeIPA Contributors see COPYING for license
+#
+
+
+class Graph():
+ """
+ Simple oriented graph structure
+
+ G = (V, E) where G is graph, V set of vertices and E list of edges.
+ E = (tail, head) where tail and head are vertices
+ """
+
+ def __init__(self):
+ self.vertices = set()
+ self.edges = []
+ self._adj = dict()
+
+ def add_vertex(self, vertex):
+ self.vertices.add(vertex)
+ self._adj[vertex] = []
+
+ def add_edge(self, tail, head):
+ if tail not in self.vertices:
+ raise ValueError("tail is not a vertex")
+ if head not in self.vertices:
+ raise ValueError("head is not a vertex")
+ self.edges.append((tail, head))
+ self._adj[tail].append(head)
+
+ def remove_edge(self, tail, head):
+ self.edges.remove((tail, head))
+ self._adj[tail].remove(head)
+
+ def remove_vertex(self, vertex):
+ self.vertices.remove(vertex)
+
+ # delete _adjacencies
+ del self._adj[vertex]
+ for key, _adj in self._adj.iteritems():
+ _adj[:] = [v for v in _adj if v != vertex]
+
+ # delete edges
+ edges = [e for e in self.edges if e[0] != vertex and e[1] != vertex]
+ self.edges[:] = edges
+
+ def get_tails(self, head):
+ """
+ Get list of vertices where a vertex is on the right side of an edge
+ """
+ return [e[0] for e in self.edges if e[1] == head]
+
+ def get_heads(self, tail):
+ """
+ Get list of vertices where a vertex is on the left side of an edge
+ """
+ return [e[1] for e in self.edges if e[0] == tail]
+
+ def bfs(self, start=None):
+ """
+ Breadth-first search traversal of the graph from `start` vertex.
+ Return a set of all visited vertices
+ """
+ if not start:
+ start = list(self.vertices)[0]
+ visited = set()
+ queue = [start]
+ while queue:
+ vertex = queue.pop(0)
+ if vertex not in visited:
+ visited.add(vertex)
+ queue.extend(set(self._adj.get(vertex, [])) - visited)
+ return visited