summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ipalib/util.py50
-rw-r--r--ipaserver/install/replication.py3
-rw-r--r--ipaserver/plugins/topology.py3
-rw-r--r--ipaserver/topology.py195
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)