Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (10.5 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, pooled_rapi_client)
75
from synnefo.logic.rapi import GanetiApiError
76
from synnefo.logic.backend import get_ganeti_instances, get_backends
77
from synnefo.logic import utils
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
                with pooled_rapi_client(vm) as c:
94
                    try:
95
                        job_status = c.GetJobStatus(vm.backendjobid)['status']
96
                        if job_status in ('queued', 'waiting', 'running'):
97
                            # Server is still building in Ganeti
98
                            continue
99
                        else:
100
                            c.GetInstance(utils.id_to_instance_name(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
                with pooled_rapi_client(vm) as c:
132
                    try:
133
                        job_info = c.GetJobStatus(job_id=vm.backendjobid)
134
                        if job_info['status'] == 'success':
135
                            unsynced.add((i, D[i], G[i]))
136
                    except GanetiApiError:
137
                        pass
138

    
139
    return unsynced
140

    
141

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

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

    
160
    return failed
161

    
162

    
163
def get_servers_from_db(backend=None):
164
    backends = get_backends(backend)
165
    vms = VirtualMachine.objects.filter(deleted=False, backend__in=backends)
166
    return dict(map(lambda x: (x.id, x.operstate), vms))
167

    
168

    
169
def get_instances_from_ganeti(backend=None):
170
    ganeti_instances = get_ganeti_instances(backend=backend, bulk=True)
171
    snf_instances = {}
172
    snf_nics = {}
173

    
174
    prefix = settings.BACKEND_PREFIX_ID
175
    for i in ganeti_instances:
176
        if i['name'].startswith(prefix):
177
            try:
178
                id = utils.id_from_instance_name(i['name'])
179
            except Exception:
180
                log.error("Ignoring instance with malformed name %s",
181
                          i['name'])
182
                continue
183

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

    
189
            snf_instances[id] = i['oper_state']
190
            snf_nics[id] = get_nics_from_instance(i)
191

    
192
    return snf_instances, snf_nics
193

    
194

    
195
#
196
# Nics
197
#
198
def get_nics_from_ganeti(backend=None):
199
    """Get network interfaces for each ganeti instance.
200

201
    """
202
    instances = get_ganeti_instances(backend=backend, bulk=True)
203
    prefix = settings.BACKEND_PREFIX_ID
204

    
205
    snf_instances_nics = {}
206
    for i in instances:
207
        if i['name'].startswith(prefix):
208
            try:
209
                id = utils.id_from_instance_name(i['name'])
210
            except Exception:
211
                log.error("Ignoring instance with malformed name %s",
212
                          i['name'])
213
                continue
214
            if id in snf_instances_nics:
215
                log.error("Ignoring instance with duplicate Synnefo id %s",
216
                          i['name'])
217
                continue
218

    
219
            snf_instances_nics[id] = get_nics_from_instance(i)
220

    
221
    return snf_instances_nics
222

    
223

    
224
def get_nics_from_instance(i):
225
    ips = zip(itertools.repeat('ipv4'), i['nic.ips'])
226
    macs = zip(itertools.repeat('mac'), i['nic.macs'])
227
    networks = zip(itertools.repeat('network'), i['nic.networks'])
228
    # modes = zip(itertools.repeat('mode'), i['nic.modes'])
229
    # links = zip(itertools.repeat('link'), i['nic.links'])
230
    # nics = zip(ips,macs,modes,networks,links)
231
    nics = zip(ips, macs, networks)
232
    nics = map(lambda x: dict(x), nics)
233
    nics = dict(enumerate(nics))
234
    return nics
235

    
236

    
237
def get_nics_from_db(backend=None):
238
    """Get network interfaces for each vm in DB.
239

240
    """
241
    backends = get_backends(backend)
242
    instances = VirtualMachine.objects.filter(deleted=False,
243
                                              backend__in=backends)
244
    instances_nics = {}
245
    for instance in instances:
246
        nics = {}
247
        for n in instance.nics.all():
248
            ipv4 = n.ipv4
249
            nic = {'mac':      n.mac,
250
                   'network':  n.network.backend_id,
251
                   'ipv4':     ipv4 if ipv4 != '' else None
252
                   }
253
            nics[n.index] = nic
254
        instances_nics[instance.id] = nics
255
    return instances_nics
256

    
257

    
258
def unsynced_nics(DBNics, GNics):
259
    """Find unsynced network interfaces between DB and Ganeti.
260

261
    @ rtype: dict; {instance_id: ganeti_nics}
262
    @ return Dictionary containing the instances ids that have unsynced network
263
    interfaces between DB and Ganeti and the network interfaces in Ganeti.
264

265
    """
266
    idD = set(DBNics.keys())
267
    idG = set(GNics.keys())
268

    
269
    unsynced = {}
270
    for i in idD & idG:
271
        nicsD = DBNics[i]
272
        nicsG = GNics[i]
273
        if len(nicsD) != len(nicsG):
274
            unsynced[i] = (nicsD, nicsG)
275
            continue
276
        for index in nicsG.keys():
277
            nicD = nicsD[index]
278
            nicG = nicsG[index]
279
            if (nicD['ipv4'] != nicG['ipv4'] or
280
                nicD['mac'] != nicG['mac'] or
281
                nicD['network'] != nicG['network']):
282
                    unsynced[i] = (nicsD, nicsG)
283
                    break
284

    
285
    return unsynced
286

    
287
#
288
# Networks
289
#
290

    
291

    
292
def get_networks_from_ganeti(backend):
293
    prefix = settings.BACKEND_PREFIX_ID + 'net-'
294

    
295
    networks = {}
296
    with pooled_rapi_client(backend) as c:
297
        for net in c.GetNetworks(bulk=True):
298
            if net['name'].startswith(prefix):
299
                id = utils.id_from_network_name(net['name'])
300
                networks[id] = net
301

    
302
    return networks
303

    
304

    
305
def hanging_networks(backend, GNets):
306
    """Get networks that are not connected to all Nodegroups.
307

308
    """
309
    def get_network_groups(group_list):
310
        groups = set()
311
        for g in group_list:
312
            g_name = g.split('(')[0]
313
            groups.add(g_name)
314
        return groups
315

    
316
    with pooled_rapi_client(backend) as c:
317
        groups = set(c.GetGroups())
318

    
319
    hanging = {}
320
    for id, info in GNets.items():
321
        group_list = get_network_groups(info['group_list'])
322
        if group_list != groups:
323
            hanging[id] = groups - group_list
324
    return hanging
325

    
326

    
327
# Only for testing this module individually
328
def main():
329
    print get_instances_from_ganeti()
330

    
331

    
332
if __name__ == "__main__":
333
    sys.exit(main())