summaryrefslogtreecommitdiffstats
path: root/ipaserver/topology.py
blob: 27c3b29a4d3c2fe477e6e519b4006b3d96f0eeae (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
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)