On 06/26/2015 02:15 PM, Petr Vobornik wrote:
On 06/17/2015 02:00 PM, Petr Vobornik wrote:
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



Patch with
* changed error messages
* removed question to force removal (--force is needed)
attached.



Fixed bug, in a broken topology, where there was a segment with removed replica, building a graph failed.
--
Petr Vobornik
From cd3ed940d809c4c859b6a9082d46cbd4d234f53a Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Wed, 17 Jun 2015 13:33:24 +0200
Subject: [PATCH] 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
---
 install/tools/ipa-replica-manage | 48 ++++++++++++++++++++++----
 ipalib/util.py                   | 51 ++++++++++++++++++++++++++++
 ipapython/graph.py               | 73 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 166 insertions(+), 6 deletions(-)
 create mode 100644 ipapython/graph.py

diff --git a/install/tools/ipa-replica-manage b/install/tools/ipa-replica-manage
index 57e30bc54ae030a4620660d1fa7539626721ebbd..71eb992f969666cadfb9e0025b177cb3696abddc 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 44478a2d1eed6d66e54949e0840e6d62310830c5..75797229b5800037e352ddf02257d0b4157743d0 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 0000000000000000000000000000000000000000..20b612548018faa6010be2f653060cb9d5ae065d
--- /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
-- 
2.4.3

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to