Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (19.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
from logging import getLogger
36

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

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

    
53
from django.utils import importlib
54

    
55

    
56

    
57
log = getLogger('synnefo.api')
58

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

    
71

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

    
80

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

    
91

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

    
100

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

    
111
def nic_to_dict(nic):
112
    network = nic.network
113
    network_id = str(network.id) if not network.public else 'public'
114
    ipv4 = nic.ipv4 if nic.ipv4 else None
115
    ipv6 = nic.ipv6 if nic.ipv6 else None
116

    
117
    d = {'id': util.construct_nic_id(nic), 'network_id': network_id, 'mac_address': nic.mac, 'ipv4': ipv4, 'ipv6': ipv6}
118
    if nic.firewall_profile:
119
        d['firewallProfile'] = nic.firewall_profile
120
    return d
121

    
122

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

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

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

    
144

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

    
154

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

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

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

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

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

    
184
    return HttpResponse(data, status=200)
185

    
186

    
187
@util.api_method('POST')
188
@transaction.commit_on_success
189
def create_server(request):
190
    # Normal Response Code: 202
191
    # Error Response Codes: computeFault (400, 500),
192
    #                       serviceUnavailable (503),
193
    #                       unauthorized (401),
194
    #                       badMediaType(415),
195
    #                       itemNotFound (404),
196
    #                       badRequest (400),
197
    #                       serverCapacityUnavailable (503),
198
    #                       overLimit (413)
199
    req = util.get_request_dict(request)
200
    log.debug('create_server %s', req)
201

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

    
214
    if len(personality) > settings.MAX_PERSONALITY:
215
        raise faults.OverLimit("Maximum number of personalities exceeded")
216

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

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

    
241
    flavor = util.get_flavor(flavor_id)
242
    password = util.random_password()
243

    
244
    count = VirtualMachine.objects.filter(userid=request.user_uniq,
245
                                          deleted=False).count()
246

    
247
    # get user limit
248
    vms_limit_for_user = \
249
        settings.VMS_USER_QUOTA.get(request.user_uniq,
250
                settings.MAX_VMS_PER_USER)
251

    
252
    if count >= vms_limit_for_user:
253
        raise faults.OverLimit("Server count limit exceeded for your account.")
254

    
255
    backend_allocator = BackendAllocator()
256
    backend = backend_allocator.allocate(flavor)
257
    if backend is None:
258
        raise Exception
259

    
260
    # We must save the VM instance now, so that it gets a valid
261
    # vm.backend_vm_id.
262
    vm = VirtualMachine.objects.create(
263
        name=name,
264
        backend=backend,
265
        userid=request.user_uniq,
266
        imageid=image_id,
267
        flavor=flavor)
268

    
269
    try:
270
        jobID = create_instance(vm, flavor, image, password, personality)
271
    except GanetiApiError:
272
        vm.delete()
273
        raise
274

    
275
    vm.backendjobid = jobID
276
    vm.save()
277

    
278
    for key, val in metadata.items():
279
        VirtualMachineMetadata.objects.create(
280
            meta_key=key,
281
            meta_value=val,
282
            vm=vm)
283

    
284
    log.info('User %s created vm with %s cpus, %s ram and %s storage',
285
             request.user_uniq, flavor.cpu, flavor.ram, flavor.disk)
286

    
287
    server = vm_to_dict(vm, detail=True)
288
    server['status'] = 'BUILD'
289
    server['adminPass'] = password
290
    return render_server(request, server, status=202)
291

    
292

    
293
@util.api_method('GET')
294
def get_server_details(request, server_id):
295
    # Normal Response Codes: 200, 203
296
    # Error Response Codes: computeFault (400, 500),
297
    #                       serviceUnavailable (503),
298
    #                       unauthorized (401),
299
    #                       badRequest (400),
300
    #                       itemNotFound (404),
301
    #                       overLimit (413)
302

    
303
    log.debug('get_server_details %s', server_id)
304
    vm = util.get_vm(server_id, request.user_uniq)
305
    server = vm_to_dict(vm, detail=True)
306
    return render_server(request, server)
307

    
308

    
309
@util.api_method('PUT')
310
def update_server_name(request, server_id):
311
    # Normal Response Code: 204
312
    # Error Response Codes: computeFault (400, 500),
313
    #                       serviceUnavailable (503),
314
    #                       unauthorized (401),
315
    #                       badRequest (400),
316
    #                       badMediaType(415),
317
    #                       itemNotFound (404),
318
    #                       buildInProgress (409),
319
    #                       overLimit (413)
320

    
321
    req = util.get_request_dict(request)
322
    log.debug('update_server_name %s %s', server_id, req)
323

    
324
    try:
325
        name = req['server']['name']
326
    except (TypeError, KeyError):
327
        raise faults.BadRequest("Malformed request")
328

    
329
    vm = util.get_vm(server_id, request.user_uniq)
330
    vm.name = name
331
    vm.save()
332

    
333
    return HttpResponse(status=204)
334

    
335

    
336
@util.api_method('DELETE')
337
@transaction.commit_on_success
338
def delete_server(request, server_id):
339
    # Normal Response Codes: 204
340
    # Error Response Codes: computeFault (400, 500),
341
    #                       serviceUnavailable (503),
342
    #                       unauthorized (401),
343
    #                       itemNotFound (404),
344
    #                       unauthorized (401),
345
    #                       buildInProgress (409),
346
    #                       overLimit (413)
347

    
348
    log.debug('delete_server %s', server_id)
349
    vm = util.get_vm(server_id, request.user_uniq)
350
    delete_instance(vm)
351
    return HttpResponse(status=204)
352

    
353

    
354
@util.api_method('POST')
355
def server_action(request, server_id):
356
    req = util.get_request_dict(request)
357
    log.debug('server_action %s %s', server_id, req)
358
    vm = util.get_vm(server_id, request.user_uniq)
359
    if len(req) != 1:
360
        raise faults.BadRequest("Malformed request")
361

    
362
    key = req.keys()[0]
363
    val = req[key]
364

    
365
    try:
366
        assert isinstance(val, dict)
367
        return server_actions[key](request, vm, req[key])
368
    except KeyError:
369
        raise faults.BadRequest("Unknown action")
370
    except AssertionError:
371
        raise faults.BadRequest("Invalid argument")
372

    
373

    
374
@util.api_method('GET')
375
def list_addresses(request, server_id):
376
    # Normal Response Codes: 200, 203
377
    # Error Response Codes: computeFault (400, 500),
378
    #                       serviceUnavailable (503),
379
    #                       unauthorized (401),
380
    #                       badRequest (400),
381
    #                       overLimit (413)
382

    
383
    log.debug('list_addresses %s', server_id)
384
    vm = util.get_vm(server_id, request.user_uniq)
385
    addresses = [nic_to_dict(nic) for nic in vm.nics.all()]
386

    
387
    if request.serialization == 'xml':
388
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
389
    else:
390
        data = json.dumps({'addresses': {'values': addresses}})
391

    
392
    return HttpResponse(data, status=200)
393

    
394

    
395
@util.api_method('GET')
396
def list_addresses_by_network(request, server_id, network_id):
397
    # Normal Response Codes: 200, 203
398
    # Error Response Codes: computeFault (400, 500),
399
    #                       serviceUnavailable (503),
400
    #                       unauthorized (401),
401
    #                       badRequest (400),
402
    #                       itemNotFound (404),
403
    #                       overLimit (413)
404

    
405
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
406
    machine = util.get_vm(server_id, request.user_uniq)
407
    network = util.get_network(network_id, request.user_uniq)
408
    nic = util.get_nic(machine, network)
409
    address = nic_to_dict(nic)
410

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

    
416
    return HttpResponse(data, status=200)
417

    
418

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

    
428
    log.debug('list_server_metadata %s', server_id)
429
    vm = util.get_vm(server_id, request.user_uniq)
430
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
431
    return util.render_metadata(request, metadata, use_values=True, status=200)
432

    
433

    
434
@util.api_method('POST')
435
def update_metadata(request, server_id):
436
    # Normal Response Code: 201
437
    # Error Response Codes: computeFault (400, 500),
438
    #                       serviceUnavailable (503),
439
    #                       unauthorized (401),
440
    #                       badRequest (400),
441
    #                       buildInProgress (409),
442
    #                       badMediaType(415),
443
    #                       overLimit (413)
444

    
445
    req = util.get_request_dict(request)
446
    log.debug('update_server_metadata %s %s', server_id, req)
447
    vm = util.get_vm(server_id, request.user_uniq)
448
    try:
449
        metadata = req['metadata']
450
        assert isinstance(metadata, dict)
451
    except (KeyError, AssertionError):
452
        raise faults.BadRequest("Malformed request")
453

    
454
    for key, val in metadata.items():
455
        meta, created = vm.metadata.get_or_create(meta_key=key)
456
        meta.meta_value = val
457
        meta.save()
458

    
459
    vm.save()
460
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
461
    return util.render_metadata(request, vm_meta, status=201)
462

    
463

    
464
@util.api_method('GET')
465
def get_metadata_item(request, server_id, key):
466
    # Normal Response Codes: 200, 203
467
    # Error Response Codes: computeFault (400, 500),
468
    #                       serviceUnavailable (503),
469
    #                       unauthorized (401),
470
    #                       itemNotFound (404),
471
    #                       badRequest (400),
472
    #                       overLimit (413)
473

    
474
    log.debug('get_server_metadata_item %s %s', server_id, key)
475
    vm = util.get_vm(server_id, request.user_uniq)
476
    meta = util.get_vm_meta(vm, key)
477
    d = {meta.meta_key: meta.meta_value}
478
    return util.render_meta(request, d, status=200)
479

    
480

    
481
@util.api_method('PUT')
482
@transaction.commit_on_success
483
def create_metadata_item(request, server_id, key):
484
    # Normal Response Code: 201
485
    # Error Response Codes: computeFault (400, 500),
486
    #                       serviceUnavailable (503),
487
    #                       unauthorized (401),
488
    #                       itemNotFound (404),
489
    #                       badRequest (400),
490
    #                       buildInProgress (409),
491
    #                       badMediaType(415),
492
    #                       overLimit (413)
493

    
494
    req = util.get_request_dict(request)
495
    log.debug('create_server_metadata_item %s %s %s', server_id, key, req)
496
    vm = util.get_vm(server_id, request.user_uniq)
497
    try:
498
        metadict = req['meta']
499
        assert isinstance(metadict, dict)
500
        assert len(metadict) == 1
501
        assert key in metadict
502
    except (KeyError, AssertionError):
503
        raise faults.BadRequest("Malformed request")
504

    
505
    meta, created = VirtualMachineMetadata.objects.get_or_create(
506
        meta_key=key,
507
        vm=vm)
508

    
509
    meta.meta_value = metadict[key]
510
    meta.save()
511
    vm.save()
512
    d = {meta.meta_key: meta.meta_value}
513
    return util.render_meta(request, d, status=201)
514

    
515

    
516
@util.api_method('DELETE')
517
@transaction.commit_on_success
518
def delete_metadata_item(request, server_id, key):
519
    # Normal Response Code: 204
520
    # Error Response Codes: computeFault (400, 500),
521
    #                       serviceUnavailable (503),
522
    #                       unauthorized (401),
523
    #                       itemNotFound (404),
524
    #                       badRequest (400),
525
    #                       buildInProgress (409),
526
    #                       badMediaType(415),
527
    #                       overLimit (413),
528

    
529
    log.debug('delete_server_metadata_item %s %s', server_id, key)
530
    vm = util.get_vm(server_id, request.user_uniq)
531
    meta = util.get_vm_meta(vm, key)
532
    meta.delete()
533
    vm.save()
534
    return HttpResponse(status=204)
535

    
536

    
537
@util.api_method('GET')
538
def server_stats(request, server_id):
539
    # Normal Response Codes: 200
540
    # Error Response Codes: computeFault (400, 500),
541
    #                       serviceUnavailable (503),
542
    #                       unauthorized (401),
543
    #                       badRequest (400),
544
    #                       itemNotFound (404),
545
    #                       overLimit (413)
546

    
547
    log.debug('server_stats %s', server_id)
548
    vm = util.get_vm(server_id, request.user_uniq)
549
    #secret = util.encrypt(vm.backend_vm_id)
550
    secret = vm.backend_vm_id      # XXX disable backend id encryption
551

    
552
    stats = {
553
        'serverRef': vm.id,
554
        'refresh': settings.STATS_REFRESH_PERIOD,
555
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
556
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
557
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
558
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
559

    
560
    if request.serialization == 'xml':
561
        data = render_to_string('server_stats.xml', stats)
562
    else:
563
        data = json.dumps({'stats': stats})
564

    
565
    return HttpResponse(data, status=200)