Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (19.3 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.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.util.rapi import GanetiApiError
50

    
51

    
52
log = getLogger('synnefo.api')
53

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

    
66

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

    
75

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

    
86

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

    
95

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

    
106

    
107
def nic_to_dict(nic):
108
    network = nic.network
109
    network_id = str(network.id) if not network.public else 'public'
110
    d = {'id': network_id, 'name': network.name, 'mac': nic.mac}
111
    if nic.firewall_profile:
112
        d['firewallProfile'] = nic.firewall_profile
113
    if nic.ipv4 or nic.ipv6:
114
        d['values'] = []
115
        if nic.ipv4:
116
            d['values'].append({'version': 4, 'addr': nic.ipv4})
117
        if nic.ipv6:
118
            d['values'].append({'version': 6, 'addr': nic.ipv6})
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
        addresses = [nic_to_dict(nic) for nic in vm.nics.all()]
139
        if addresses:
140
            d['addresses'] = {'values': addresses}
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
    since = util.isoparse(request.GET.get('changes-since'))
166

    
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
def create_server(request):
188
    # Normal Response Code: 202
189
    # Error Response Codes: computeFault (400, 500),
190
    #                       serviceUnavailable (503),
191
    #                       unauthorized (401),
192
    #                       badMediaType(415),
193
    #                       itemNotFound (404),
194
    #                       badRequest (400),
195
    #                       serverCapacityUnavailable (503),
196
    #                       overLimit (413)
197

    
198
    req = util.get_request_dict(request)
199
    log.debug('create_server %s', req)
200

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

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

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

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

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

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

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

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

    
254
    # We must save the VM instance now, so that it gets a valid vm.backend_id.
255
    vm = VirtualMachine.objects.create(
256
        name=name,
257
        userid=request.user_uniq,
258
        imageid=image_id,
259
        flavor=flavor)
260

    
261
    try:
262
        create_instance(vm, flavor, image, password, personality)
263
    except GanetiApiError:
264
        vm.delete()
265
        raise
266

    
267
    for key, val in metadata.items():
268
        VirtualMachineMetadata.objects.create(
269
            meta_key=key,
270
            meta_value=val,
271
            vm=vm)
272

    
273
    log.info('User %s created vm with %s cpus, %s ram and %s storage',
274
             request.user_uniq, flavor.cpu, flavor.ram, flavor.disk)
275

    
276
    server = vm_to_dict(vm, detail=True)
277
    server['status'] = 'BUILD'
278
    server['adminPass'] = password
279
    return render_server(request, server, status=202)
280

    
281

    
282
@util.api_method('GET')
283
def get_server_details(request, server_id):
284
    # Normal Response Codes: 200, 203
285
    # Error Response Codes: computeFault (400, 500),
286
    #                       serviceUnavailable (503),
287
    #                       unauthorized (401),
288
    #                       badRequest (400),
289
    #                       itemNotFound (404),
290
    #                       overLimit (413)
291

    
292
    log.debug('get_server_details %s', server_id)
293
    vm = util.get_vm(server_id, request.user_uniq)
294
    server = vm_to_dict(vm, detail=True)
295
    return render_server(request, server)
296

    
297

    
298
@util.api_method('PUT')
299
def update_server_name(request, server_id):
300
    # Normal Response Code: 204
301
    # Error Response Codes: computeFault (400, 500),
302
    #                       serviceUnavailable (503),
303
    #                       unauthorized (401),
304
    #                       badRequest (400),
305
    #                       badMediaType(415),
306
    #                       itemNotFound (404),
307
    #                       buildInProgress (409),
308
    #                       overLimit (413)
309

    
310
    req = util.get_request_dict(request)
311
    log.debug('update_server_name %s %s', server_id, req)
312

    
313
    try:
314
        name = req['server']['name']
315
    except (TypeError, KeyError):
316
        raise faults.BadRequest("Malformed request")
317

    
318
    vm = util.get_vm(server_id, request.user_uniq)
319
    vm.name = name
320
    vm.save()
321

    
322
    return HttpResponse(status=204)
323

    
324

    
325
@util.api_method('DELETE')
326
def delete_server(request, server_id):
327
    # Normal Response Codes: 204
328
    # Error Response Codes: computeFault (400, 500),
329
    #                       serviceUnavailable (503),
330
    #                       unauthorized (401),
331
    #                       itemNotFound (404),
332
    #                       unauthorized (401),
333
    #                       buildInProgress (409),
334
    #                       overLimit (413)
335

    
336
    log.debug('delete_server %s', server_id)
337
    vm = util.get_vm(server_id, request.user_uniq)
338
    delete_instance(vm)
339
    return HttpResponse(status=204)
340

    
341

    
342
@util.api_method('POST')
343
def server_action(request, server_id):
344
    req = util.get_request_dict(request)
345
    log.debug('server_action %s %s', server_id, req)
346
    vm = util.get_vm(server_id, request.user_uniq)
347
    if len(req) != 1:
348
        raise faults.BadRequest("Malformed request")
349

    
350
    key = req.keys()[0]
351
    val = req[key]
352

    
353
    try:
354
        assert isinstance(val, dict)
355
        return server_actions[key](request, vm, req[key])
356
    except KeyError:
357
        raise faults.BadRequest("Unknown action")
358
    except AssertionError:
359
        raise faults.BadRequest("Invalid argument")
360

    
361

    
362
@util.api_method('GET')
363
def list_addresses(request, server_id):
364
    # Normal Response Codes: 200, 203
365
    # Error Response Codes: computeFault (400, 500),
366
    #                       serviceUnavailable (503),
367
    #                       unauthorized (401),
368
    #                       badRequest (400),
369
    #                       overLimit (413)
370

    
371
    log.debug('list_addresses %s', server_id)
372
    vm = util.get_vm(server_id, request.user_uniq)
373
    addresses = [nic_to_dict(nic) for nic in vm.nics.all()]
374

    
375
    if request.serialization == 'xml':
376
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
377
    else:
378
        data = json.dumps({'addresses': {'values': addresses}})
379

    
380
    return HttpResponse(data, status=200)
381

    
382

    
383
@util.api_method('GET')
384
def list_addresses_by_network(request, server_id, network_id):
385
    # Normal Response Codes: 200, 203
386
    # Error Response Codes: computeFault (400, 500),
387
    #                       serviceUnavailable (503),
388
    #                       unauthorized (401),
389
    #                       badRequest (400),
390
    #                       itemNotFound (404),
391
    #                       overLimit (413)
392

    
393
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
394
    machine = util.get_vm(server_id, request.user_uniq)
395
    network = util.get_network(network_id, request.user_uniq)
396
    nic = util.get_nic(machine, network)
397
    address = nic_to_dict(nic)
398

    
399
    if request.serialization == 'xml':
400
        data = render_to_string('address.xml', {'address': address})
401
    else:
402
        data = json.dumps({'network': address})
403

    
404
    return HttpResponse(data, status=200)
405

    
406

    
407
@util.api_method('GET')
408
def list_metadata(request, server_id):
409
    # Normal Response Codes: 200, 203
410
    # Error Response Codes: computeFault (400, 500),
411
    #                       serviceUnavailable (503),
412
    #                       unauthorized (401),
413
    #                       badRequest (400),
414
    #                       overLimit (413)
415

    
416
    log.debug('list_server_metadata %s', server_id)
417
    vm = util.get_vm(server_id, request.user_uniq)
418
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
419
    return util.render_metadata(request, metadata, use_values=True, status=200)
420

    
421

    
422
@util.api_method('POST')
423
def update_metadata(request, server_id):
424
    # Normal Response Code: 201
425
    # Error Response Codes: computeFault (400, 500),
426
    #                       serviceUnavailable (503),
427
    #                       unauthorized (401),
428
    #                       badRequest (400),
429
    #                       buildInProgress (409),
430
    #                       badMediaType(415),
431
    #                       overLimit (413)
432

    
433
    req = util.get_request_dict(request)
434
    log.debug('update_server_metadata %s %s', server_id, req)
435
    vm = util.get_vm(server_id, request.user_uniq)
436
    try:
437
        metadata = req['metadata']
438
        assert isinstance(metadata, dict)
439
    except (KeyError, AssertionError):
440
        raise faults.BadRequest("Malformed request")
441

    
442
    for key, val in metadata.items():
443
        meta, created = vm.metadata.get_or_create(meta_key=key)
444
        meta.meta_value = val
445
        meta.save()
446

    
447
    vm.save()
448
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
449
    return util.render_metadata(request, vm_meta, status=201)
450

    
451

    
452
@util.api_method('GET')
453
def get_metadata_item(request, server_id, key):
454
    # Normal Response Codes: 200, 203
455
    # Error Response Codes: computeFault (400, 500),
456
    #                       serviceUnavailable (503),
457
    #                       unauthorized (401),
458
    #                       itemNotFound (404),
459
    #                       badRequest (400),
460
    #                       overLimit (413)
461

    
462
    log.debug('get_server_metadata_item %s %s', server_id, key)
463
    vm = util.get_vm(server_id, request.user_uniq)
464
    meta = util.get_vm_meta(vm, key)
465
    d = {meta.meta_key: meta.meta_value}
466
    return util.render_meta(request, d, status=200)
467

    
468

    
469
@util.api_method('PUT')
470
def create_metadata_item(request, server_id, key):
471
    # Normal Response Code: 201
472
    # Error Response Codes: computeFault (400, 500),
473
    #                       serviceUnavailable (503),
474
    #                       unauthorized (401),
475
    #                       itemNotFound (404),
476
    #                       badRequest (400),
477
    #                       buildInProgress (409),
478
    #                       badMediaType(415),
479
    #                       overLimit (413)
480

    
481
    req = util.get_request_dict(request)
482
    log.debug('create_server_metadata_item %s %s %s', server_id, key, req)
483
    vm = util.get_vm(server_id, request.user_uniq)
484
    try:
485
        metadict = req['meta']
486
        assert isinstance(metadict, dict)
487
        assert len(metadict) == 1
488
        assert key in metadict
489
    except (KeyError, AssertionError):
490
        raise faults.BadRequest("Malformed request")
491

    
492
    meta, created = VirtualMachineMetadata.objects.get_or_create(
493
        meta_key=key,
494
        vm=vm)
495

    
496
    meta.meta_value = metadict[key]
497
    meta.save()
498
    vm.save()
499
    d = {meta.meta_key: meta.meta_value}
500
    return util.render_meta(request, d, status=201)
501

    
502

    
503
@util.api_method('DELETE')
504
def delete_metadata_item(request, server_id, key):
505
    # Normal Response Code: 204
506
    # Error Response Codes: computeFault (400, 500),
507
    #                       serviceUnavailable (503),
508
    #                       unauthorized (401),
509
    #                       itemNotFound (404),
510
    #                       badRequest (400),
511
    #                       buildInProgress (409),
512
    #                       badMediaType(415),
513
    #                       overLimit (413),
514

    
515
    log.debug('delete_server_metadata_item %s %s', server_id, key)
516
    vm = util.get_vm(server_id, request.user_uniq)
517
    meta = util.get_vm_meta(vm, key)
518
    meta.delete()
519
    vm.save()
520
    return HttpResponse(status=204)
521

    
522

    
523
@util.api_method('GET')
524
def server_stats(request, server_id):
525
    # Normal Response Codes: 200
526
    # Error Response Codes: computeFault (400, 500),
527
    #                       serviceUnavailable (503),
528
    #                       unauthorized (401),
529
    #                       badRequest (400),
530
    #                       itemNotFound (404),
531
    #                       overLimit (413)
532

    
533
    log.debug('server_stats %s', server_id)
534
    vm = util.get_vm(server_id, request.user_uniq)
535
    #secret = util.encrypt(vm.backend_id)
536
    secret = vm.backend_id      # XXX disable backend id encryption
537

    
538
    stats = {
539
        'serverRef': vm.id,
540
        'refresh': settings.STATS_REFRESH_PERIOD,
541
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
542
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
543
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
544
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
545

    
546
    if request.serialization == 'xml':
547
        data = render_to_string('server_stats.xml', stats)
548
    else:
549
        data = json.dumps({'stats': stats})
550

    
551
    return HttpResponse(data, status=200)