From 659b88b8205ef403aa9162453472e4731d93d13b Mon Sep 17 00:00:00 2001 From: Petr Vobornik Date: Jun 29 2015 15:11:08 +0000 Subject: 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 --- diff --git a/install/tools/ipa-replica-manage b/install/tools/ipa-replica-manage index 57e30bc..71eb992 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 44478a2..7579722 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 0000000..20b6125 --- /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