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>
This commit is contained in:
Petr Vobornik 2015-06-17 13:33:24 +02:00
parent dcb6916a3b
commit 659b88b820
3 changed files with 166 additions and 6 deletions

View File

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

View File

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

73
ipapython/graph.py Normal file
View File

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