Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / api / servers.py @ ae562340

History | View | Annotate | Download (20.7 kB)

1
# Copyright 2011-2012 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
from base64 import b64decode
35

    
36
from django.conf import settings
37
from django.conf.urls.defaults import patterns
38
from django.db import transaction
39
from django.http import HttpResponse
40
from django.template.loader import render_to_string
41
from django.utils import simplejson as json
42

    
43
from synnefo.api import faults, util
44
from synnefo.api.actions import server_actions
45
from synnefo.api.common import method_not_allowed
46
from synnefo.db.models import VirtualMachine, VirtualMachineMetadata
47
from synnefo.logic.backend import create_instance, delete_instance
48
from synnefo.logic.utils import get_rsapi_state
49
from synnefo.logic.rapi import GanetiApiError
50
from synnefo.logic.backend_allocator import BackendAllocator
51
from random import choice
52

    
53

    
54
from logging import getLogger
55
log = getLogger('synnefo.api')
56

    
57
urlpatterns = patterns('synnefo.api.servers',
58
    (r'^(?:/|.json|.xml)?$', 'demux'),
59
    (r'^/detail(?:.json|.xml)?$', 'list_servers', {'detail': True}),
60
    (r'^/(\d+)(?:.json|.xml)?$', 'server_demux'),
61
    (r'^/(\d+)/action(?:.json|.xml)?$', 'server_action'),
62
    (r'^/(\d+)/ips(?:.json|.xml)?$', 'list_addresses'),
63
    (r'^/(\d+)/ips/(.+?)(?:.json|.xml)?$', 'list_addresses_by_network'),
64
    (r'^/(\d+)/meta(?:.json|.xml)?$', 'metadata_demux'),
65
    (r'^/(\d+)/meta/(.+?)(?:.json|.xml)?$', 'metadata_item_demux'),
66
    (r'^/(\d+)/stats(?:.json|.xml)?$', 'server_stats'),
67
)
68

    
69

    
70
def demux(request):
71
    if request.method == 'GET':
72
        return list_servers(request)
73
    elif request.method == 'POST':
74
        return create_server(request)
75
    else:
76
        return method_not_allowed(request)
77

    
78

    
79
def server_demux(request, server_id):
80
    if request.method == 'GET':
81
        return get_server_details(request, server_id)
82
    elif request.method == 'PUT':
83
        return update_server_name(request, server_id)
84
    elif request.method == 'DELETE':
85
        return delete_server(request, server_id)
86
    else:
87
        return method_not_allowed(request)
88

    
89

    
90
def metadata_demux(request, server_id):
91
    if request.method == 'GET':
92
        return list_metadata(request, server_id)
93
    elif request.method == 'POST':
94
        return update_metadata(request, server_id)
95
    else:
96
        return method_not_allowed(request)
97

    
98

    
99
def metadata_item_demux(request, server_id, key):
100
    if request.method == 'GET':
101
        return get_metadata_item(request, server_id, key)
102
    elif request.method == 'PUT':
103
        return create_metadata_item(request, server_id, key)
104
    elif request.method == 'DELETE':
105
        return delete_metadata_item(request, server_id, key)
106
    else:
107
        return method_not_allowed(request)
108

    
109

    
110
def nic_to_dict(nic):
111
    d = {'id': util.construct_nic_id(nic),
112
         'network_id': str(nic.network.id),
113
         'mac_address': nic.mac,
114
         'ipv4': nic.ipv4 if nic.ipv4 else None,
115
         'ipv6': nic.ipv6 if nic.ipv6 else None}
116

    
117
    if nic.firewall_profile:
118
        d['firewallProfile'] = nic.firewall_profile
119
    return d
120

    
121

    
122
def vm_to_dict(vm, detail=False):
123
    d = dict(id=vm.id, name=vm.name)
124
    if detail:
125
        d['status'] = get_rsapi_state(vm)
126
        d['progress'] = 100 if get_rsapi_state(vm) == 'ACTIVE' \
127
                        else vm.buildpercentage
128
        d['hostId'] = vm.hostid
129
        d['updated'] = util.isoformat(vm.updated)
130
        d['created'] = util.isoformat(vm.created)
131
        d['flavorRef'] = vm.flavor.id
132
        d['imageRef'] = vm.imageid
133

    
134
        metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
135
        if metadata:
136
            d['metadata'] = {'values': metadata}
137

    
138
        attachments = [nic_to_dict(nic) for nic in vm.nics.all()]
139
        if attachments:
140
            d['attachments'] = {'values': attachments}
141
    return d
142

    
143

    
144
def render_server(request, server, status=200):
145
    if request.serialization == 'xml':
146
        data = render_to_string('server.xml', {
147
            'server': server,
148
            'is_root': True})
149
    else:
150
        data = json.dumps({'server': server})
151
    return HttpResponse(data, status=status)
152

    
153

    
154
@util.api_method('GET')
155
def list_servers(request, detail=False):
156
    # Normal Response Codes: 200, 203
157
    # Error Response Codes: computeFault (400, 500),
158
    #                       serviceUnavailable (503),
159
    #                       unauthorized (401),
160
    #                       badRequest (400),
161
    #                       overLimit (413)
162

    
163
    log.debug('list_servers detail=%s', detail)
164
    user_vms = VirtualMachine.objects.filter(userid=request.user_uniq)
165

    
166
    since = util.isoparse(request.GET.get('changes-since'))
167
    if since:
168
        user_vms = user_vms.filter(updated__gte=since)
169
        if not user_vms:
170
            return HttpResponse(status=304)
171
    else:
172
        user_vms = user_vms.filter(deleted=False)
173

    
174
    servers = [vm_to_dict(server, detail) for server in user_vms]
175

    
176
    if request.serialization == 'xml':
177
        data = render_to_string('list_servers.xml', {
178
            'servers': servers,
179
            'detail': detail})
180
    else:
181
        data = json.dumps({'servers': {'values': servers}})
182

    
183
    return HttpResponse(data, status=200)
184

    
185

    
186
@util.api_method('POST')
187
# Use manual transactions. Backend and IP pool allocations need exclusive
188
# access (SELECT..FOR UPDATE). Running create_server with commit_on_success
189
# would result in backends and public networks to be locked until the job is
190
# sent to the Ganeti backend.
191
@transaction.commit_manually
192
def create_server(request):
193
    # Normal Response Code: 202
194
    # Error Response Codes: computeFault (400, 500),
195
    #                       serviceUnavailable (503),
196
    #                       unauthorized (401),
197
    #                       badMediaType(415),
198
    #                       itemNotFound (404),
199
    #                       badRequest (400),
200
    #                       serverCapacityUnavailable (503),
201
    #                       overLimit (413)
202
    req = util.get_request_dict(request)
203
    log.info('create_server %s', req)
204

    
205
    try:
206
        server = req['server']
207
        name = server['name']
208
        metadata = server.get('metadata', {})
209
        assert isinstance(metadata, dict)
210
        image_id = server['imageRef']
211
        flavor_id = server['flavorRef']
212
        personality = server.get('personality', [])
213
        assert isinstance(personality, list)
214
    except (KeyError, AssertionError):
215
        raise faults.BadRequest("Malformed request")
216

    
217
    if len(personality) > settings.MAX_PERSONALITY:
218
        raise faults.OverLimit("Maximum number of personalities exceeded")
219

    
220
    for p in personality:
221
        # Verify that personalities are well-formed
222
        try:
223
            assert isinstance(p, dict)
224
            keys = set(p.keys())
225
            allowed = set(['contents', 'group', 'mode', 'owner', 'path'])
226
            assert keys.issubset(allowed)
227
            contents = p['contents']
228
            if len(contents) > settings.MAX_PERSONALITY_SIZE:
229
                # No need to decode if contents already exceed limit
230
                raise faults.OverLimit("Maximum size of personality exceeded")
231
            if len(b64decode(contents)) > settings.MAX_PERSONALITY_SIZE:
232
                raise faults.OverLimit("Maximum size of personality exceeded")
233
        except AssertionError:
234
            raise faults.BadRequest("Malformed personality in request")
235

    
236
    image = {}
237
    img = util.get_image(image_id, request.user_uniq)
238
    properties = img.get('properties', {})
239
    image['backend_id'] = img['location']
240
    image['format'] = img['disk_format']
241
    image['metadata'] = dict((key.upper(), val) \
242
                             for key, val in properties.items())
243

    
244
    flavor = util.get_flavor(flavor_id)
245
    password = util.random_password()
246

    
247
    count = VirtualMachine.objects.filter(userid=request.user_uniq,
248
                                          deleted=False).count()
249

    
250
    # get user limit
251
    vms_limit_for_user = \
252
        settings.VMS_USER_QUOTA.get(request.user_uniq,
253
                settings.MAX_VMS_PER_USER)
254

    
255
    if count >= vms_limit_for_user:
256
        raise faults.OverLimit("Server count limit exceeded for your account.")
257

    
258
    backend_allocator = BackendAllocator()
259
    backend = backend_allocator.allocate(flavor)
260

    
261
    if backend is None:
262
        transaction.rollback()
263
        log.error("No available backends for VM with flavor %s", flavor)
264
        raise Exception("No available backends")
265
    transaction.commit()
266

    
267
    if settings.PUBLIC_ROUTED_USE_POOL:
268
        (network, address) = util.allocate_public_address(backend)
269
        if address is None:
270
            transaction.rollback()
271
            log.error("Public networks of backend %s are full", backend)
272
            raise faults.OverLimit("Can not allocate IP for new machine."
273
                                   " Public networks are full.")
274
        transaction.commit()
275
        nic = {'ip': address, 'network': network.backend_id}
276
    else:
277
        network = choice(list(util.backend_public_networks(backend)))
278
        nic = {'ip': 'pool', 'network': network.backend_id}
279

    
280
    # We must save the VM instance now, so that it gets a valid
281
    # vm.backend_vm_id.
282
    vm = VirtualMachine.objects.create(
283
        name=name,
284
        backend=backend,
285
        userid=request.user_uniq,
286
        imageid=image_id,
287
        flavor=flavor)
288

    
289
    try:
290
        jobID = create_instance(vm, nic, flavor, image, password, personality)
291
    except GanetiApiError:
292
        vm.delete()
293
        raise
294

    
295
    log.info("User %s created VM %s, NIC %s, Backend %s, JobID %s",
296
            request.user_uniq, vm, nic, backend, str(jobID))
297

    
298
    vm.backendjobid = jobID
299
    vm.save()
300

    
301
    for key, val in metadata.items():
302
        VirtualMachineMetadata.objects.create(
303
            meta_key=key,
304
            meta_value=val,
305
            vm=vm)
306

    
307
    server = vm_to_dict(vm, detail=True)
308
    server['status'] = 'BUILD'
309
    server['adminPass'] = password
310

    
311
    respsone = render_server(request, server, status=202)
312
    transaction.commit()
313

    
314
    return respsone
315

    
316

    
317
@util.api_method('GET')
318
def get_server_details(request, server_id):
319
    # Normal Response Codes: 200, 203
320
    # Error Response Codes: computeFault (400, 500),
321
    #                       serviceUnavailable (503),
322
    #                       unauthorized (401),
323
    #                       badRequest (400),
324
    #                       itemNotFound (404),
325
    #                       overLimit (413)
326

    
327
    log.debug('get_server_details %s', server_id)
328
    vm = util.get_vm(server_id, request.user_uniq)
329
    server = vm_to_dict(vm, detail=True)
330
    return render_server(request, server)
331

    
332

    
333
@util.api_method('PUT')
334
def update_server_name(request, server_id):
335
    # Normal Response Code: 204
336
    # Error Response Codes: computeFault (400, 500),
337
    #                       serviceUnavailable (503),
338
    #                       unauthorized (401),
339
    #                       badRequest (400),
340
    #                       badMediaType(415),
341
    #                       itemNotFound (404),
342
    #                       buildInProgress (409),
343
    #                       overLimit (413)
344

    
345
    req = util.get_request_dict(request)
346
    log.info('update_server_name %s %s', server_id, req)
347

    
348
    try:
349
        name = req['server']['name']
350
    except (TypeError, KeyError):
351
        raise faults.BadRequest("Malformed request")
352

    
353
    vm = util.get_vm(server_id, request.user_uniq)
354
    vm.name = name
355
    vm.save()
356

    
357
    return HttpResponse(status=204)
358

    
359

    
360
@util.api_method('DELETE')
361
@transaction.commit_on_success
362
def delete_server(request, server_id):
363
    # Normal Response Codes: 204
364
    # Error Response Codes: computeFault (400, 500),
365
    #                       serviceUnavailable (503),
366
    #                       unauthorized (401),
367
    #                       itemNotFound (404),
368
    #                       unauthorized (401),
369
    #                       buildInProgress (409),
370
    #                       overLimit (413)
371

    
372
    log.info('delete_server %s', server_id)
373
    vm = util.get_vm(server_id, request.user_uniq)
374
    delete_instance(vm)
375
    return HttpResponse(status=204)
376

    
377

    
378
@util.api_method('POST')
379
def server_action(request, server_id):
380
    req = util.get_request_dict(request)
381
    log.debug('server_action %s %s', server_id, req)
382
    vm = util.get_vm(server_id, request.user_uniq)
383
    if len(req) != 1:
384
        raise faults.BadRequest("Malformed request")
385

    
386
    key = req.keys()[0]
387
    val = req[key]
388

    
389
    try:
390
        assert isinstance(val, dict)
391
        return server_actions[key](request, vm, req[key])
392
    except KeyError:
393
        raise faults.BadRequest("Unknown action")
394
    except AssertionError:
395
        raise faults.BadRequest("Invalid argument")
396

    
397

    
398
@util.api_method('GET')
399
def list_addresses(request, server_id):
400
    # Normal Response Codes: 200, 203
401
    # Error Response Codes: computeFault (400, 500),
402
    #                       serviceUnavailable (503),
403
    #                       unauthorized (401),
404
    #                       badRequest (400),
405
    #                       overLimit (413)
406

    
407
    log.debug('list_addresses %s', server_id)
408
    vm = util.get_vm(server_id, request.user_uniq)
409
    addresses = [nic_to_dict(nic) for nic in vm.nics.all()]
410

    
411
    if request.serialization == 'xml':
412
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
413
    else:
414
        data = json.dumps({'addresses': {'values': addresses}})
415

    
416
    return HttpResponse(data, status=200)
417

    
418

    
419
@util.api_method('GET')
420
def list_addresses_by_network(request, server_id, network_id):
421
    # Normal Response Codes: 200, 203
422
    # Error Response Codes: computeFault (400, 500),
423
    #                       serviceUnavailable (503),
424
    #                       unauthorized (401),
425
    #                       badRequest (400),
426
    #                       itemNotFound (404),
427
    #                       overLimit (413)
428

    
429
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
430
    machine = util.get_vm(server_id, request.user_uniq)
431
    network = util.get_network(network_id, request.user_uniq)
432
    nic = util.get_nic(machine, network)
433
    address = nic_to_dict(nic)
434

    
435
    if request.serialization == 'xml':
436
        data = render_to_string('address.xml', {'address': address})
437
    else:
438
        data = json.dumps({'network': address})
439

    
440
    return HttpResponse(data, status=200)
441

    
442

    
443
@util.api_method('GET')
444
def list_metadata(request, server_id):
445
    # Normal Response Codes: 200, 203
446
    # Error Response Codes: computeFault (400, 500),
447
    #                       serviceUnavailable (503),
448
    #                       unauthorized (401),
449
    #                       badRequest (400),
450
    #                       overLimit (413)
451

    
452
    log.debug('list_server_metadata %s', server_id)
453
    vm = util.get_vm(server_id, request.user_uniq)
454
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
455
    return util.render_metadata(request, metadata, use_values=True, status=200)
456

    
457

    
458
@util.api_method('POST')
459
def update_metadata(request, server_id):
460
    # Normal Response Code: 201
461
    # Error Response Codes: computeFault (400, 500),
462
    #                       serviceUnavailable (503),
463
    #                       unauthorized (401),
464
    #                       badRequest (400),
465
    #                       buildInProgress (409),
466
    #                       badMediaType(415),
467
    #                       overLimit (413)
468

    
469
    req = util.get_request_dict(request)
470
    log.info('update_server_metadata %s %s', server_id, req)
471
    vm = util.get_vm(server_id, request.user_uniq)
472
    try:
473
        metadata = req['metadata']
474
        assert isinstance(metadata, dict)
475
    except (KeyError, AssertionError):
476
        raise faults.BadRequest("Malformed request")
477

    
478
    for key, val in metadata.items():
479
        meta, created = vm.metadata.get_or_create(meta_key=key)
480
        meta.meta_value = val
481
        meta.save()
482

    
483
    vm.save()
484
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
485
    return util.render_metadata(request, vm_meta, status=201)
486

    
487

    
488
@util.api_method('GET')
489
def get_metadata_item(request, server_id, key):
490
    # Normal Response Codes: 200, 203
491
    # Error Response Codes: computeFault (400, 500),
492
    #                       serviceUnavailable (503),
493
    #                       unauthorized (401),
494
    #                       itemNotFound (404),
495
    #                       badRequest (400),
496
    #                       overLimit (413)
497

    
498
    log.debug('get_server_metadata_item %s %s', server_id, key)
499
    vm = util.get_vm(server_id, request.user_uniq)
500
    meta = util.get_vm_meta(vm, key)
501
    d = {meta.meta_key: meta.meta_value}
502
    return util.render_meta(request, d, status=200)
503

    
504

    
505
@util.api_method('PUT')
506
@transaction.commit_on_success
507
def create_metadata_item(request, server_id, key):
508
    # Normal Response Code: 201
509
    # Error Response Codes: computeFault (400, 500),
510
    #                       serviceUnavailable (503),
511
    #                       unauthorized (401),
512
    #                       itemNotFound (404),
513
    #                       badRequest (400),
514
    #                       buildInProgress (409),
515
    #                       badMediaType(415),
516
    #                       overLimit (413)
517

    
518
    req = util.get_request_dict(request)
519
    log.info('create_server_metadata_item %s %s %s', server_id, key, req)
520
    vm = util.get_vm(server_id, request.user_uniq)
521
    try:
522
        metadict = req['meta']
523
        assert isinstance(metadict, dict)
524
        assert len(metadict) == 1
525
        assert key in metadict
526
    except (KeyError, AssertionError):
527
        raise faults.BadRequest("Malformed request")
528

    
529
    meta, created = VirtualMachineMetadata.objects.get_or_create(
530
        meta_key=key,
531
        vm=vm)
532

    
533
    meta.meta_value = metadict[key]
534
    meta.save()
535
    vm.save()
536
    d = {meta.meta_key: meta.meta_value}
537
    return util.render_meta(request, d, status=201)
538

    
539

    
540
@util.api_method('DELETE')
541
@transaction.commit_on_success
542
def delete_metadata_item(request, server_id, key):
543
    # Normal Response Code: 204
544
    # Error Response Codes: computeFault (400, 500),
545
    #                       serviceUnavailable (503),
546
    #                       unauthorized (401),
547
    #                       itemNotFound (404),
548
    #                       badRequest (400),
549
    #                       buildInProgress (409),
550
    #                       badMediaType(415),
551
    #                       overLimit (413),
552

    
553
    log.info('delete_server_metadata_item %s %s', server_id, key)
554
    vm = util.get_vm(server_id, request.user_uniq)
555
    meta = util.get_vm_meta(vm, key)
556
    meta.delete()
557
    vm.save()
558
    return HttpResponse(status=204)
559

    
560

    
561
@util.api_method('GET')
562
def server_stats(request, server_id):
563
    # Normal Response Codes: 200
564
    # Error Response Codes: computeFault (400, 500),
565
    #                       serviceUnavailable (503),
566
    #                       unauthorized (401),
567
    #                       badRequest (400),
568
    #                       itemNotFound (404),
569
    #                       overLimit (413)
570

    
571
    log.debug('server_stats %s', server_id)
572
    vm = util.get_vm(server_id, request.user_uniq)
573
    #secret = util.encrypt(vm.backend_vm_id)
574
    secret = vm.backend_vm_id      # XXX disable backend id encryption
575

    
576
    stats = {
577
        'serverRef': vm.id,
578
        'refresh': settings.STATS_REFRESH_PERIOD,
579
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
580
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
581
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
582
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
583

    
584
    if request.serialization == 'xml':
585
        data = render_to_string('server_stats.xml', stats)
586
    else:
587
        data = json.dumps({'stats': stats})
588

    
589
    return HttpResponse(data, status=200)