# # 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 = {} managed_suffix_attr = 'iparepltopomanagedsuffix_topologysuffix' for master in masters: if managed_suffix_attr not in master: continue managed_suffixes = master[managed_suffix_attr] if managed_suffixes is None: 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, masters in suffix_to_masters.items(): segments = api_instance.Command.topologysegment_find( suffix_name, sizelimit=0).get('result') topology_graphs[suffix_name] = create_topology_graph(masters, 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: """ 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, errors in self.errors.items(): 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, errors in errors_after_removal.items(): 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)