Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / logic / backend.py @ fd65ab41

History | View | Annotate | Download (16.2 kB)

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

    
34
import json
35

    
36
from logging import getLogger
37
from django.conf import settings
38
from django.db import transaction
39
from datetime import datetime
40

    
41
from synnefo.db.models import (Backend, VirtualMachine, Network, NetworkLink,
42
                               BACKEND_STATUSES)
43
from synnefo.logic import utils
44
from synnefo.util.rapi import GanetiRapiClient
45

    
46

    
47

    
48
log = getLogger('synnefo.logic')
49

    
50

    
51
_firewall_tags = {
52
    'ENABLED': settings.GANETI_FIREWALL_ENABLED_TAG,
53
    'DISABLED': settings.GANETI_FIREWALL_DISABLED_TAG,
54
    'PROTECTED': settings.GANETI_FIREWALL_PROTECTED_TAG}
55

    
56
_reverse_tags = dict((v.split(':')[3], k) for k, v in _firewall_tags.items())
57

    
58

    
59
def create_client(hostname, port=5080, username=None, password=None):
60
    return GanetiRapiClient(hostname, port, username, password)
61

    
62
@transaction.commit_on_success
63
def process_op_status(vm, etime, jobid, opcode, status, logmsg):
64
    """Process a job progress notification from the backend
65

66
    Process an incoming message from the backend (currently Ganeti).
67
    Job notifications with a terminating status (sucess, error, or canceled),
68
    also update the operating state of the VM.
69

70
    """
71
    # See #1492, #1031, #1111 why this line has been removed
72
    #if (opcode not in [x[0] for x in VirtualMachine.BACKEND_OPCODES] or
73
    if status not in [x[0] for x in BACKEND_STATUSES]:
74
        raise VirtualMachine.InvalidBackendMsgError(opcode, status)
75

    
76
    vm.backendjobid = jobid
77
    vm.backendjobstatus = status
78
    vm.backendopcode = opcode
79
    vm.backendlogmsg = logmsg
80

    
81
    # Notifications of success change the operating state
82
    state_for_success = VirtualMachine.OPER_STATE_FROM_OPCODE.get(opcode, None)
83
    if status == 'success' and state_for_success is not None:
84
        utils.update_state(vm, state_for_success)
85
        # Set the deleted flag explicitly, cater for admin-initiated removals
86
        if opcode == 'OP_INSTANCE_REMOVE':
87
            vm.deleted = True
88
            vm.nics.all().delete()
89

    
90
    # Special case: if OP_INSTANCE_CREATE fails --> ERROR
91
    if status in ('canceled', 'error') and opcode == 'OP_INSTANCE_CREATE':
92
        utils.update_state(vm, 'ERROR')
93

    
94
    # Special case: OP_INSTANCE_REMOVE fails for machines in ERROR,
95
    # when no instance exists at the Ganeti backend.
96
    # See ticket #799 for all the details.
97
    #
98
    if (status == 'error' and opcode == 'OP_INSTANCE_REMOVE' and
99
        vm.operstate == 'ERROR'):
100
        vm.deleted = True
101
        vm.nics.all().delete()
102

    
103
    vm.backendtime = etime
104
    # Any other notification of failure leaves the operating state unchanged
105

    
106
    vm.save()
107

    
108

    
109
@transaction.commit_on_success
110
def process_net_status(vm, etime, nics):
111
    """Process a net status notification from the backend
112

113
    Process an incoming message from the Ganeti backend,
114
    detailing the NIC configuration of a VM instance.
115

116
    Update the state of the VM in the DB accordingly.
117
    """
118

    
119
    vm.nics.all().delete()
120
    for i, nic in enumerate(nics):
121
        if i == 0:
122
            net = Network.objects.get(public=True)
123
        else:
124
            try:
125
                link = NetworkLink.objects.get(name=nic['link'])
126
            except NetworkLink.DoesNotExist:
127
                # Cannot find an instance of NetworkLink for
128
                # the link attribute specified in the notification
129
                raise NetworkLink.DoesNotExist("Cannot find a NetworkLink "
130
                    "object for link='%s'" % nic['link'])
131
            net = link.network
132
            if net is None:
133
                raise Network.DoesNotExist("NetworkLink for link='%s' not "
134
                    "associated with an existing Network instance." %
135
                    nic['link'])
136

    
137
        firewall = nic.get('firewall', '')
138
        firewall_profile = _reverse_tags.get(firewall, '')
139
        if not firewall_profile and net.public:
140
            firewall_profile = settings.DEFAULT_FIREWALL_PROFILE
141

    
142
        vm.nics.create(
143
            network=net,
144
            index=i,
145
            mac=nic.get('mac', ''),
146
            ipv4=nic.get('ip', ''),
147
            ipv6=nic.get('ipv6', ''),
148
            firewall_profile=firewall_profile)
149

    
150
        # network nics modified, update network object
151
        net.save()
152

    
153
    vm.backendtime = etime
154
    vm.save()
155

    
156

    
157
@transaction.commit_on_success
158
def process_create_progress(vm, etime, rprogress, wprogress):
159

    
160
    # XXX: This only uses the read progress for now.
161
    #      Explore whether it would make sense to use the value of wprogress
162
    #      somewhere.
163
    percentage = int(rprogress)
164

    
165
    # The percentage may exceed 100%, due to the way
166
    # snf-progress-monitor tracks bytes read by image handling processes
167
    percentage = 100 if percentage > 100 else percentage
168
    if percentage < 0:
169
        raise ValueError("Percentage cannot be negative")
170

    
171
    # FIXME: log a warning here, see #1033
172
#   if last_update > percentage:
173
#       raise ValueError("Build percentage should increase monotonically " \
174
#                        "(old = %d, new = %d)" % (last_update, percentage))
175

    
176
    # This assumes that no message of type 'ganeti-create-progress' is going to
177
    # arrive once OP_INSTANCE_CREATE has succeeded for a Ganeti instance and
178
    # the instance is STARTED.  What if the two messages are processed by two
179
    # separate dispatcher threads, and the 'ganeti-op-status' message for
180
    # successful creation gets processed before the 'ganeti-create-progress'
181
    # message? [vkoukis]
182
    #
183
    #if not vm.operstate == 'BUILD':
184
    #    raise VirtualMachine.IllegalState("VM is not in building state")
185

    
186
    vm.buildpercentage = percentage
187
    vm.backendtime = etime
188
    vm.save()
189

    
190

    
191
def start_action(vm, action):
192
    """Update the state of a VM when a new action is initiated."""
193
    if not action in [x[0] for x in VirtualMachine.ACTIONS]:
194
        raise VirtualMachine.InvalidActionError(action)
195

    
196
    # No actions to deleted and no actions beside destroy to suspended VMs
197
    if vm.deleted:
198
        raise VirtualMachine.DeletedError
199

    
200
    # No actions to machines being built. They may be destroyed, however.
201
    if vm.operstate == 'BUILD' and action != 'DESTROY':
202
        raise VirtualMachine.BuildingError
203

    
204
    vm.action = action
205
    vm.backendjobid = None
206
    vm.backendopcode = None
207
    vm.backendjobstatus = None
208
    vm.backendlogmsg = None
209

    
210
    # Update the relevant flags if the VM is being suspended or destroyed.
211
    # Do not set the deleted flag here, see ticket #721.
212
    #
213
    # The deleted flag is set asynchronously, when an OP_INSTANCE_REMOVE
214
    # completes successfully. Hence, a server may be visible for some time
215
    # after a DELETE /servers/id returns HTTP 204.
216
    #
217
    if action == "DESTROY":
218
        # vm.deleted = True
219
        pass
220
    elif action == "SUSPEND":
221
        vm.suspended = True
222
    elif action == "START":
223
        vm.suspended = False
224
    vm.save()
225

    
226

    
227
def create_instance(vm, flavor, image, password, personality):
228
    """`image` is a dictionary which should contain the keys:
229
            'backend_id', 'format' and 'metadata'
230

231
        metadata value should be a dictionary.
232
    """
233
    nic = {'ip': 'pool', 'network': settings.GANETI_PUBLIC_NETWORK}
234

    
235
    if settings.IGNORE_FLAVOR_DISK_SIZES:
236
        if image['backend_id'].find("windows") >= 0:
237
            sz = 14000
238
        else:
239
            sz = 4000
240
    else:
241
        sz = flavor.disk * 1024
242

    
243
    # Handle arguments to CreateInstance() as a dictionary,
244
    # initialize it based on a deployment-specific value.
245
    # This enables the administrator to override deployment-specific
246
    # arguments, such as the disk template to use, name of os provider
247
    # and hypervisor-specific parameters at will (see Synnefo #785, #835).
248
    #
249
    kw = settings.GANETI_CREATEINSTANCE_KWARGS
250
    kw['mode'] = 'create'
251
    kw['name'] = vm.backend_vm_id
252
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
253
    kw['disk_template'] = flavor.disk_template
254
    kw['disks'] = [{"size": sz}]
255
    kw['nics'] = [nic]
256
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
257
    # kw['os'] = settings.GANETI_OS_PROVIDER
258
    kw['ip_check'] = False
259
    kw['name_check'] = False
260
    # Do not specific a node explicitly, have
261
    # Ganeti use an iallocator instead
262
    #
263
    # kw['pnode']=rapi.GetNodes()[0]
264
    kw['dry_run'] = settings.TEST
265

    
266
    kw['beparams'] = {
267
        'auto_balance': True,
268
        'vcpus': flavor.cpu,
269
        'memory': flavor.ram}
270

    
271
    kw['osparams'] = {
272
        'img_id': image['backend_id'],
273
        'img_passwd': password,
274
        'img_format': image['format']}
275
    if personality:
276
        kw['osparams']['img_personality'] = json.dumps(personality)
277

    
278
    kw['osparams']['img_properties'] = json.dumps(image['metadata'])
279

    
280
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
281
    # kw['hvparams'] = dict(serial_console=False)
282

    
283
    return vm.client.CreateInstance(**kw)
284

    
285

    
286
def delete_instance(vm):
287
    start_action(vm, 'DESTROY')
288
    vm.client.DeleteInstance(vm.backend_vm_id, dry_run=settings.TEST)
289

    
290

    
291
def reboot_instance(vm, reboot_type):
292
    assert reboot_type in ('soft', 'hard')
293
    vm.client.RebootInstance(vm.backend_vm_id, reboot_type, dry_run=settings.TEST)
294
    log.info('Rebooting instance %s', vm.backend_vm_id)
295

    
296

    
297
def startup_instance(vm):
298
    start_action(vm, 'START')
299
    vm.client.StartupInstance(vm.backend_vm_id, dry_run=settings.TEST)
300

    
301

    
302
def shutdown_instance(vm):
303
    start_action(vm, 'STOP')
304
    vm.client.ShutdownInstance(vm.backend_vm_id, dry_run=settings.TEST)
305

    
306

    
307
def get_instance_console(vm):
308
    # RAPI GetInstanceConsole() returns endpoints to the vnc_bind_address,
309
    # which is a cluster-wide setting, either 0.0.0.0 or 127.0.0.1, and pretty
310
    # useless (see #783).
311
    #
312
    # Until this is fixed on the Ganeti side, construct a console info reply
313
    # directly.
314
    #
315
    # WARNING: This assumes that VNC runs on port network_port on
316
    #          the instance's primary node, and is probably
317
    #          hypervisor-specific.
318
    #
319
    console = {}
320
    console['kind'] = 'vnc'
321
    i = vm.client.GetInstance(vm.backend_vm_id)
322
    if i['hvparams']['serial_console']:
323
        raise Exception("hv parameter serial_console cannot be true")
324
    console['host'] = i['pnode']
325
    console['port'] = i['network_port']
326

    
327
    return console
328
    # return rapi.GetInstanceConsole(vm.backend_vm_id)
329

    
330

    
331
def request_status_update(vm):
332
    return vm.client.GetInstanceInfo(vm.backend_vm_id)
333

    
334

    
335
def update_status(vm, status):
336
    utils.update_state(vm, status)
337

    
338

    
339
def create_network_link():
340
    try:
341
        last = NetworkLink.objects.order_by('-index')[0]
342
        index = last.index + 1
343
    except IndexError:
344
        index = 1
345

    
346
    if index <= settings.GANETI_MAX_LINK_NUMBER:
347
        name = '%s%d' % (settings.GANETI_LINK_PREFIX, index)
348
        return NetworkLink.objects.create(index=index, name=name,
349
                                            available=True)
350
    return None     # All link slots are filled
351

    
352

    
353
@transaction.commit_on_success
354
def create_network(name, user_id):
355
    try:
356
        link = NetworkLink.objects.filter(available=True)[0]
357
    except IndexError:
358
        link = create_network_link()
359
        if not link:
360
            raise NetworkLink.NotAvailable
361

    
362
    network = Network.objects.create(
363
        name=name,
364
        userid=user_id,
365
        state='ACTIVE',
366
        link=link)
367

    
368
    link.network = network
369
    link.available = False
370
    link.save()
371

    
372
    return network
373

    
374

    
375
@transaction.commit_on_success
376
def delete_network(net):
377
    link = net.link
378
    if link.name != settings.GANETI_NULL_LINK:
379
        link.available = True
380
        link.network = None
381
        link.save()
382

    
383
    for vm in net.machines.all():
384
        disconnect_from_network(vm, net)
385
        vm.save()
386
    net.state = 'DELETED'
387
    net.save()
388

    
389

    
390
def connect_to_network(vm, net):
391
    nic = {'mode': 'bridged', 'link': net.link.name}
392
    vm.client.ModifyInstance(vm.backend_vm_id, nics=[('add', -1, nic)],
393
                        hotplug=True, dry_run=settings.TEST)
394

    
395

    
396
def disconnect_from_network(vm, net):
397
    nics = vm.nics.filter(network__public=False).order_by('index')
398
    ops = [('remove', nic.index, {}) for nic in nics if nic.network == net]
399
    if not ops:  # Vm not connected to network
400
        return
401
    vm.client.ModifyInstance(vm.backend_vm_id, nics=ops[::-1],
402
                        hotplug=True, dry_run=settings.TEST)
403

    
404

    
405
def set_firewall_profile(vm, profile):
406
    try:
407
        tag = _firewall_tags[profile]
408
    except KeyError:
409
        raise ValueError("Unsopported Firewall Profile: %s" % profile)
410

    
411
    client = vm.client
412
    # Delete all firewall tags
413
    for t in _firewall_tags.values():
414
        client.DeleteInstanceTags(vm.backend_vm_id, [t], dry_run=settings.TEST)
415

    
416
    client.AddInstanceTags(vm.backend_vm_id, [tag], dry_run=settings.TEST)
417

    
418
    # XXX NOP ModifyInstance call to force process_net_status to run
419
    # on the dispatcher
420
    vm.client.ModifyInstance(vm.backend_vm_id,
421
                        os_name=settings.GANETI_CREATEINSTANCE_KWARGS['os'])
422

    
423

    
424
def get_ganeti_instances(backend=None, bulk=False):
425
    Instances = [c.client.GetInstances(bulk=bulk)\
426
                 for c in get_backends(backend)]
427
    return reduce(list.__add__, Instances, [])
428

    
429

    
430
def get_ganeti_nodes(backend=None, bulk=False):
431
    Nodes = [c.client.GetNodes(bulk=bulk) for c in get_backends(backend)]
432
    return reduce(list.__add__, Nodes, [])
433

    
434

    
435
def get_ganeti_jobs(backend=None, bulk=False):
436
    Jobs = [c.client.GetJobs(bulk=bulk) for c in get_backends(backend)]
437
    return reduce(list.__add__, Jobs, [])
438

    
439
##
440
##
441
##
442

    
443

    
444
def get_backends(backend=None):
445
    if backend:
446
        return [backend]
447
    return Backend.objects.filter(offline=False)
448

    
449

    
450
def get_physical_resources(backend):
451
    """ Get the physical resources of a backend.
452

453
    Get the resources of a backend as reported by the backend (not the db).
454

455
    """
456
    nodes = get_ganeti_nodes(backend, bulk=True)
457
    attr = ['mfree', 'mtotal', 'dfree', 'dtotal', 'pinst_cnt', 'ctotal']
458
    res = {}
459
    for a in attr:
460
        res[a] = 0
461
    for n in nodes:
462
        # Filter out drained, offline and not vm_capable nodes since they will
463
        # not take part in the vm allocation process
464
        if n['vm_capable'] and not n['drained'] and not n['offline']\
465
           and n['cnodes']:
466
            for a in attr:
467
                res[a] += int(n[a])
468
    return res
469

    
470

    
471
def update_resources(backend, resources=None):
472
    """ Update the state of the backend resources in db.
473

474
    """
475

    
476
    if not resources:
477
        resources = get_physical_resources(backend)
478

    
479
    backend.mfree = resources['mfree']
480
    backend.mtotal = resources['mtotal']
481
    backend.dfree = resources['dfree']
482
    backend.dtotal = resources['dtotal']
483
    backend.pinst_cnt = resources['pinst_cnt']
484
    backend.ctotal = resources['ctotal']
485
    backend.updated = datetime.now()
486
    backend.save()
487

    
488

    
489
def get_memory_from_instances(backend):
490
    """ Get the memory that is used from instances.
491

492
    Get the used memory of a backend. Note: This is different for
493
    the real memory used, due to kvm's memory de-duplication.
494

495
    """
496
    instances = backend.client.GetInstances(bulk=True)
497
    mem = 0
498
    for i in instances:
499
        mem += i['oper_ram']
500
    return mem