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
--
Petr Vobornik
From 892672e4841b6b143c953fca0f221c34e146a6ba 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 | 41 ++++++++++++++++++++----
 ipalib/util.py                   | 48 ++++++++++++++++++++++++++++
 ipapython/graph.py               | 69 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 152 insertions(+), 6 deletions(-)
 create mode 100644 ipapython/graph.py

diff --git a/install/tools/ipa-replica-manage b/install/tools/ipa-replica-manage
index 1b93166bce5c1d1fa6cba41cd87bc0833b2efe57..6442a68c8dbc1842ba3ff9ae80f63b688ab506b0 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
@@ -562,11 +563,39 @@ 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_connect_errors(orig_errors)
+
+    # after removal
+    graph.remove_vertex(hostname)
+    new_errors = get_topology_connection_errors(graph)
+    if new_errors:
+        print "Topology after removal of %s will be disconnected:" % hostname
+        print_connect_errors(new_errors)
+
+    if orig_errors or new_errors:
+        if not force and not ipautil.user_input("Continue to delete?", False):
+            sys.exit("Aborted")
+
+    return new_errors
+
+def print_connect_errors(errors):
+    for error in errors:
+        msg = "Server %(srv)s can't contact servers: %(replicas)s"
+        msg = msg % {'srv': error[0], 'replicas': ', '.join(error[2])}
+        print msg
 
 def enforce_host_existence(host, message=None):
     if host is not None and not ipautil.host_exists(host):
@@ -676,7 +705,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..6f7d4a67174aa2f3df8a92f1a25d20a16d3b3f03 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,50 @@ 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]
+        if direction == u'both':
+            graph.add_edge(left, right)
+            graph.add_edge(right, left)
+        if direction == u'left-right':
+            graph.add_edge(left, right)
+        if direction == u'right-left':
+            graph.add_edge(right, left)
+
+    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, visited, not_visited))
+    return connect_errors
diff --git a/ipapython/graph.py b/ipapython/graph.py
new file mode 100644
index 0000000000000000000000000000000000000000..9e7ac5c90848ae21947891ca0bc0871618fbd09e
--- /dev/null
+++ b/ipapython/graph.py
@@ -0,0 +1,69 @@
+#
+# 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 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):
+        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.1.0

-- 
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