Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / logic / reconciliation.py @ 0e9a423f

History | View | Annotate | Download (9.9 kB)

1
#!/usr/bin/env python
2
# -*- coding: utf-8 -*-
3
#
4
# Copyright 2011 GRNET S.A. All rights reserved.
5
#
6
# Redistribution and use in source and binary forms, with or
7
# without modification, are permitted provided that the following
8
# conditions are met:
9
#
10
#   1. Redistributions of source code must retain the above
11
#      copyright notice, this list of conditions and the following
12
#      disclaimer.
13
#
14
#   2. Redistributions in binary form must reproduce the above
15
#      copyright notice, this list of conditions and the following
16
#      disclaimer in the documentation and/or other materials
17
#      provided with the distribution.
18
#
19
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
20
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
22
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
23
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
26
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
27
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
28
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
29
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30
# POSSIBILITY OF SUCH DAMAGE.
31
#
32
# The views and conclusions contained in the software and
33
# documentation are those of the authors and should not be
34
# interpreted as representing official policies, either expressed
35
# or implied, of GRNET S.A.
36
#
37
"""Business logic for reconciliation
38

39
Reconcile the contents of the DB with the actual state of the
40
Ganeti backend.
41

42
Let D be the set of VMs in the DB, G the set of VMs in Ganeti.
43
RULES:
44
    R1. Stale servers in DB:
45
            For any v in D but not in G:
46
            Set deleted=True.
47
    R2. Orphan instances in Ganet:
48
            For any v in G with deleted=True in D:
49
            Issue OP_INSTANCE_DESTROY.
50
    R3. Unsynced operstate:
51
            For any v whose operating state differs between G and V:
52
            Set the operating state in D based on the state in G.
53
In the code, D, G are Python dicts mapping instance ids to operating state.
54
For D, the operating state is chosen from VirtualMachine.OPER_STATES.
55
For G, the operating state is True if the machine is up, False otherwise.
56

57
"""
58

    
59
import logging
60
import sys
61
import itertools
62

    
63
from django.core.management import setup_environ
64
try:
65
    from synnefo import settings
66
except ImportError:
67
    raise Exception("Cannot import settings, make sure PYTHONPATH contains "
68
                    "the parent directory of the Synnefo Django project.")
69
setup_environ(settings)
70

    
71

    
72
from datetime import datetime, timedelta
73

    
74
from synnefo.db.models import VirtualMachine, Network, BackendNetwork
75
from synnefo.util.dictconfig import dictConfig
76
from synnefo.util.rapi import GanetiApiError
77
from synnefo.logic.backend import get_ganeti_instances
78

    
79

    
80
log = logging.getLogger()
81

    
82

    
83
def stale_servers_in_db(D, G):
84
    idD = set(D.keys())
85
    idG = set(G.keys())
86

    
87
    stale = set()
88
    for i in idD - idG:
89
        if D[i] == 'BUILD':
90
            vm = VirtualMachine.objects.get(id=i)
91
            # Check time to avoid many rapi calls
92
            if datetime.now() > vm.backendtime + timedelta(seconds=5):
93
                try:
94
                    job_status = vm.client.GetJobStatus(vm.backendjobid)['status']
95
                    if job_status in ('queued', 'waiting', 'running'):
96
                        # Server is still building in Ganeti
97
                        continue
98
                    else:
99
                        new_vm = vm.client.GetInstance('%s%d' %
100
                                (settings.BACKEND_PREFIX_ID, i))
101
                        # Server has just been created in Ganeti
102
                        continue
103
                except GanetiApiError:
104
                    stale.add(i)
105
        else:
106
            stale.add(i)
107

    
108
    return stale
109

    
110

    
111
def orphan_instances_in_ganeti(D, G):
112
    idD = set(D.keys())
113
    idG = set(G.keys())
114

    
115
    return idG - idD
116

    
117

    
118
def unsynced_operstate(D, G):
119
    unsynced = set()
120
    idD = set(D.keys())
121
    idG = set(G.keys())
122

    
123
    for i in idD & idG:
124
        if (G[i] and D[i] != 'STARTED' or
125
            not G[i] and D[i] not in ('BUILD', 'ERROR', 'STOPPED')):
126
            unsynced.add((i, D[i], G[i]))
127
        if not G[i] and D[i] == 'BUILD':
128
            vm = VirtualMachine.objects.get(id=i)
129
            # Check time to avoid many rapi calls
130
            if datetime.now() > vm.backendtime + timedelta(seconds=5):
131
                try:
132
                    job_info = vm.client.GetJobStatus(job_id = vm.backendjobid)
133
                    if job_info['status'] == 'success':
134
                        unsynced.add((i, D[i], G[i]))
135
                except GanetiApiError:
136
                    pass
137

    
138
    return unsynced
139

    
140

    
141
def instances_with_build_errors(D, G):
142
    failed = set()
143
    idD = set(D.keys())
144
    idG = set(G.keys())
145

    
146
    for i in idD & idG:
147
        if not G[i] and D[i] == 'BUILD':
148
            vm = VirtualMachine.objects.get(id=i)
149
            # Check time to avoid many rapi calls
150
            if datetime.now() > vm.backendtime + timedelta(seconds=5):
151
                try:
152
                    job_info = vm.client.GetJobStatus(job_id = vm.backendjobid)
153
                    if job_info['status'] == 'error':
154
                        failed.add(i)
155
                except GanetiApiError:
156
                    failed.add(i)
157

    
158
    return failed
159

    
160

    
161

    
162
def get_servers_from_db():
163
    vms = VirtualMachine.objects.filter(deleted=False)
164
    return dict(map(lambda x: (x.id, x.operstate), vms))
165

    
166

    
167
def get_instances_from_ganeti():
168
    ganeti_instances = get_ganeti_instances(bulk=True)
169
    snf_instances = {}
170

    
171
    prefix = settings.BACKEND_PREFIX_ID
172
    for i in ganeti_instances:
173
        if i['name'].startswith(prefix):
174
            try:
175
                id = int(i['name'].split(prefix)[1])
176
            except Exception:
177
                log.error("Ignoring instance with malformed name %s",
178
                              i['name'])
179
                continue
180

    
181
            if id in snf_instances:
182
                log.error("Ignoring instance with duplicate Synnefo id %s",
183
                    i['name'])
184
                continue
185

    
186
            snf_instances[id] = i['oper_state']
187

    
188
    return snf_instances
189

    
190
#
191
# Nics
192
#
193
def get_nics_from_ganeti():
194
    """Get network interfaces for each ganeti instance.
195

196
    """
197
    instances = get_ganeti_instances(bulk=True)
198
    prefix = settings.BACKEND_PREFIX_ID
199

    
200
    snf_instances_nics = {}
201
    for i in instances:
202
        if i['name'].startswith(prefix):
203
            try:
204
                id = int(i['name'].split(prefix)[1])
205
            except Exception:
206
                log.error("Ignoring instance with malformed name %s",
207
                              i['name'])
208
                continue
209
            if id in snf_instances_nics:
210
                log.error("Ignoring instance with duplicate Synnefo id %s",
211
                    i['name'])
212
                continue
213

    
214
            ips = zip(itertools.repeat('ipv4'), i['nic.ips'])
215
            macs = zip(itertools.repeat('mac'), i['nic.macs'])
216
            networks = zip(itertools.repeat('network'), i['nic.networks'])
217
            # modes = zip(itertools.repeat('mode'), i['nic.modes'])
218
            # links = zip(itertools.repeat('link'), i['nic.links'])
219
            # nics = zip(ips,macs,modes,networks,links)
220
            nics = zip(ips, macs, networks)
221
            nics = map(lambda x:dict(x), nics)
222
            nics = dict(enumerate(nics))
223
            snf_instances_nics[id] = nics
224

    
225
    return snf_instances_nics
226

    
227

    
228
def get_nics_from_db():
229
    """Get network interfaces for each vm in DB.
230

231
    """
232
    instances = VirtualMachine.objects.filter(deleted=False)
233
    instances_nics = {}
234
    for instance in instances:
235
        nics = {}
236
        for n in instance.nics.all():
237
            ipv4 = n.ipv4
238
            nic = {'mac':      n.mac,
239
                   'network':  n.network.backend_id,
240
                   'ipv4':     ipv4 if ipv4 != '' else None
241
                   }
242
            nics[n.index] = nic
243
        instances_nics[instance.id] = nics
244
    return instances_nics
245

    
246

    
247
def unsynced_nics(DBNics, GNics):
248
    """Find unsynced network interfaces between DB and Ganeti.
249

250
    @ rtype: dict; {instance_id: ganeti_nics}
251
    @ return Dictionary containing the instances ids that have unsynced network
252
    interfaces between DB and Ganeti and the network interfaces in Ganeti.
253

254
    """
255
    idD = set(DBNics.keys())
256
    idG = set(GNics.keys())
257

    
258
    unsynced = {}
259
    for i in idD & idG:
260
        nicsD = DBNics[i]
261
        nicsG = GNics[i]
262
        if len(nicsD) != len(nicsG):
263
            unsynced[i] = (nicsD, nicsG)
264
            continue
265
        for index in nicsG.keys():
266
            nicD = nicsD[index]
267
            nicG = nicsG[index]
268
            if nicD['ipv4'] != nicG['ipv4'] or \
269
               nicD['mac'] != nicG['mac'] or \
270
               nicD['network'] != nicG['network']:
271
                   unsynced[i] = (nicsD, nicsG)
272
                   break
273

    
274
    return unsynced
275

    
276
#
277
# Networks
278
#
279
def get_networks_from_ganeti(backend):
280
    prefix = settings.BACKEND_PREFIX_ID
281

    
282
    networks = {}
283
    for net in backend.client.GetNetworks(bulk=True):
284
        if net['name'].startswith(prefix):
285
            # TODO: Get it from fun. Catch errors
286
            id = int(net['name'].split(prefix)[1])
287
            networks[id] = net
288

    
289
    return networks
290

    
291

    
292
def hanging_networks(backend, GNets):
293
    """Get networks that are not connected to all Nodegroups.
294

295
    """
296
    def get_network_groups(group_list):
297
        groups = set()
298
        for g in group_list:
299
            g_name = g.split('(')[0]
300
            groups.add(g_name)
301
        return groups
302

    
303
    groups = set(backend.client.GetGroups())
304

    
305
    hanging = {}
306
    for id, info in GNets.items():
307
        group_list = get_network_groups(info['group_list'])
308
        if group_list != groups:
309
            hanging[id] = groups - group_list
310
    return hanging
311

    
312

    
313
# Only for testing this module individually
314
def main():
315
    print get_instances_from_ganeti()
316

    
317

    
318
if __name__ == "__main__":
319
    sys.exit(main())