Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (10.8 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
            if not vm.backendjobid:  # VM has not been enqueued in the backend
151
                if datetime.now() > vm.created + timedelta(seconds=120):
152
                    # If a job has not been enqueued after 2 minutues, then
153
                    # it must be a stale entry..
154
                    failed.add(i)
155
            elif datetime.now() > vm.backendtime + timedelta(seconds=30):
156
                # Check time to avoid many rapi calls
157
                with pooled_rapi_client(vm) as c:
158
                    try:
159
                        job_info = c.GetJobStatus(job_id=vm.backendjobid)
160
                        if job_info['status'] == 'error':
161
                            failed.add(i)
162
                    except GanetiApiError:
163
                        failed.add(i)
164

    
165
    return failed
166

    
167

    
168
def get_servers_from_db(backend=None):
169
    backends = get_backends(backend)
170
    vms = VirtualMachine.objects.filter(deleted=False, backend__in=backends)
171
    return dict(map(lambda x: (x.id, x.operstate), vms))
172

    
173

    
174
def get_instances_from_ganeti(backend=None):
175
    ganeti_instances = get_ganeti_instances(backend=backend, bulk=True)
176
    snf_instances = {}
177
    snf_nics = {}
178

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

    
189
            if id in snf_instances:
190
                log.error("Ignoring instance with duplicate Synnefo id %s",
191
                          i['name'])
192
                continue
193

    
194
            snf_instances[id] = i['oper_state']
195
            snf_nics[id] = get_nics_from_instance(i)
196

    
197
    return snf_instances, snf_nics
198

    
199

    
200
#
201
# Nics
202
#
203
def get_nics_from_ganeti(backend=None):
204
    """Get network interfaces for each ganeti instance.
205

206
    """
207
    instances = get_ganeti_instances(backend=backend, bulk=True)
208
    prefix = settings.BACKEND_PREFIX_ID
209

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

    
224
            snf_instances_nics[id] = get_nics_from_instance(i)
225

    
226
    return snf_instances_nics
227

    
228

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

    
241

    
242
def get_nics_from_db(backend=None):
243
    """Get network interfaces for each vm in DB.
244

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

    
262

    
263
def unsynced_nics(DBNics, GNics):
264
    """Find unsynced network interfaces between DB and Ganeti.
265

266
    @ rtype: dict; {instance_id: ganeti_nics}
267
    @ return Dictionary containing the instances ids that have unsynced network
268
    interfaces between DB and Ganeti and the network interfaces in Ganeti.
269

270
    """
271
    idD = set(DBNics.keys())
272
    idG = set(GNics.keys())
273

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

    
290
    return unsynced
291

    
292
#
293
# Networks
294
#
295

    
296

    
297
def get_networks_from_ganeti(backend):
298
    prefix = settings.BACKEND_PREFIX_ID + 'net-'
299

    
300
    networks = {}
301
    with pooled_rapi_client(backend) as c:
302
        for net in c.GetNetworks(bulk=True):
303
            if net['name'].startswith(prefix):
304
                id = utils.id_from_network_name(net['name'])
305
                networks[id] = net
306

    
307
    return networks
308

    
309

    
310
def hanging_networks(backend, GNets):
311
    """Get networks that are not connected to all Nodegroups.
312

313
    """
314
    def get_network_groups(group_list):
315
        groups = set()
316
        for g in group_list:
317
            g_name = g.split('(')[0]
318
            groups.add(g_name)
319
        return groups
320

    
321
    with pooled_rapi_client(backend) as c:
322
        groups = set(c.GetGroups())
323

    
324
    hanging = {}
325
    for id, info in GNets.items():
326
        group_list = get_network_groups(info['group_list'])
327
        if group_list != groups:
328
            hanging[id] = groups - group_list
329
    return hanging
330

    
331

    
332
# Only for testing this module individually
333
def main():
334
    print get_instances_from_ganeti()
335

    
336

    
337
if __name__ == "__main__":
338
    sys.exit(main())