mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
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:
parent
dcb6916a3b
commit
659b88b820
@ -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.
|
||||
|
@ -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
73
ipapython/graph.py
Normal 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
|
Loading…
Reference in New Issue
Block a user