diff options
-rw-r--r-- | ipalib/util.py | 50 | ||||
-rw-r--r-- | ipaserver/install/replication.py | 3 | ||||
-rw-r--r-- | ipaserver/plugins/topology.py | 3 | ||||
-rw-r--r-- | ipaserver/topology.py | 195 |
4 files changed, 199 insertions, 52 deletions
diff --git a/ipalib/util.py b/ipalib/util.py index 68d11fc6c..8435f7ab6 100644 --- a/ipalib/util.py +++ b/ipalib/util.py @@ -45,7 +45,6 @@ from ipapython.ssh import SSHPublicKey from ipapython.dn import DN, RDN from ipapython.dnsutil import DNSName from ipapython.dnsutil import resolve_ip_addresses -from ipapython.graph import Graph if six.PY3: unicode = str @@ -765,55 +764,6 @@ def validate_idna_domain(value): 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 - def detect_dns_zone_realm_type(api, domain): """ Detects the type of the realm that the given DNS zone belongs to. diff --git a/ipaserver/install/replication.py b/ipaserver/install/replication.py index 30af20c76..cbef796ac 100644 --- a/ipaserver/install/replication.py +++ b/ipaserver/install/replication.py @@ -30,7 +30,8 @@ import ldap from ipalib import api, errors from ipalib.constants import CACERT -from ipalib.util import create_topology_graph, get_topology_connection_errors +from ipaserver.topology import ( + create_topology_graph, get_topology_connection_errors) from ipapython.ipa_log_manager import root_logger from ipapython import ipautil, ipaldap from ipapython.dn import DN diff --git a/ipaserver/plugins/topology.py b/ipaserver/plugins/topology.py index a6e638479..c1848f0cc 100644 --- a/ipaserver/plugins/topology.py +++ b/ipaserver/plugins/topology.py @@ -13,7 +13,8 @@ from .baseldap import ( from ipalib import _, ngettext from ipalib import output from ipalib.constants import DOMAIN_LEVEL_1 -from ipalib.util import create_topology_graph, get_topology_connection_errors +from ipaserver.topology import ( + create_topology_graph, get_topology_connection_errors) from ipapython.dn import DN if six.PY3: diff --git a/ipaserver/topology.py b/ipaserver/topology.py new file mode 100644 index 000000000..27c3b29a4 --- /dev/null +++ b/ipaserver/topology.py @@ -0,0 +1,195 @@ +# +# Copyright (C) 2016 FreeIPA Contributors see COPYING for license +# + +""" +set of functions and classes useful for management of domain level 1 topology +""" + +from copy import deepcopy + +from ipalib import _ +from ipapython.graph import Graph + +CURR_TOPOLOGY_DISCONNECTED = _(""" +Replication topology in suffix '%(suffix)s' is disconnected: +%(errors)s""") + +REMOVAL_DISCONNECTS_TOPOLOGY = _(""" +Removal of '%(hostname)s' leads to disconnected topology in suffix '%(suffix)s': +%(errors)s""") + + +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 + + +def _map_masters_to_suffixes(masters): + masters_to_suffix = {} + + for master in masters: + try: + managed_suffixes = master.get( + 'iparepltopomanagedsuffix_topologysuffix') + except KeyError: + continue + + for suffix_name in managed_suffixes: + try: + masters_to_suffix[suffix_name].append(master) + except KeyError: + masters_to_suffix[suffix_name] = [master] + + return masters_to_suffix + + +def _create_topology_graphs(api_instance): + """ + Construct a topology graph for each topology suffix + :param api_instance: instance of IPA API + """ + masters = api_instance.Command.server_find( + u'', sizelimit=0, no_members=False)['result'] + + suffix_to_masters = _map_masters_to_suffixes(masters) + + topology_graphs = {} + + for suffix_name in suffix_to_masters: + segments = api_instance.Command.topologysegment_find( + suffix_name, sizelimit=0).get('result') + + topology_graphs[suffix_name] = create_topology_graph( + suffix_to_masters[suffix_name], segments) + + return topology_graphs + + +def _format_topology_errors(topo_errors): + msg_lines = [] + for error in topo_errors: + msg_lines.append( + _("Topology does not allow server %(server)s to replicate with " + "servers:") + % {'server': error[0]} + ) + for srv in error[2]: + msg_lines.append(" %s" % srv) + + return "\n".join(msg_lines) + + +class TopologyConnectivity(object): + """ + a simple class abstracting the replication connectivity in managed topology + """ + + def __init__(self, api_instance): + self.api = api_instance + + self.graphs = _create_topology_graphs(self.api) + + @property + def errors(self): + errors_by_suffix = {} + for suffix in self.graphs: + errors_by_suffix[suffix] = get_topology_connection_errors( + self.graphs[suffix] + ) + + return errors_by_suffix + + def errors_after_master_removal(self, master_cn): + graphs_before = deepcopy(self.graphs) + + for s in self.graphs: + try: + self.graphs[s].remove_vertex(master_cn) + except ValueError: + pass + + errors_after_removal = self.errors + + self.graphs = graphs_before + + return errors_after_removal + + def check_current_state(self): + err_msg = "" + for suffix in self.errors: + errors = self.errors[suffix] + if errors: + err_msg = "\n".join([ + err_msg, + CURR_TOPOLOGY_DISCONNECTED % dict( + suffix=suffix, + errors=_format_topology_errors(errors) + )]) + + if err_msg: + raise ValueError(err_msg) + + def check_state_after_removal(self, master_cn): + err_msg = "" + errors_after_removal = self.errors_after_master_removal(master_cn) + + for suffix in errors_after_removal: + errors = errors_after_removal[suffix] + if errors: + err_msg = "\n".join([ + err_msg, + REMOVAL_DISCONNECTS_TOPOLOGY % dict( + hostname=master_cn, + suffix=suffix, + errors=_format_topology_errors(errors) + ) + ]) + + if err_msg: + raise ValueError(err_msg) |