Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (19.4 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
from base64 import b64decode
35

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

    
42
from synnefo.api import faults, util
43
from synnefo.api.actions import server_actions
44
from synnefo.api.common import method_not_allowed
45
from synnefo.db.models import VirtualMachine, VirtualMachineMetadata
46
from synnefo.logic.backend import create_instance, delete_instance
47
from synnefo.logic.utils import get_rsapi_state
48
from synnefo.util.rapi import GanetiApiError
49
from synnefo.util.log import getLogger
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(owner=request.user)
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
    owner = request.user
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
    try:
235
        img = util.get_image(image_id, owner)
236
        image['backend_id'] = img.backend_id
237
        image['format'] = img.format
238
        image['metadata'] = dict((m.meta_key.upper(), m.meta_value)
239
                for m in img.metadata.all())
240
    except faults.ItemNotFound:
241
        img = util.get_backend_image(image_id, owner)
242
        properties = img.get('properties', {})
243
        image['backend_id'] = img['location']
244
        image['format'] = img['disk_format']
245
        image['metadata'] = dict((key.upper(), val)
246
                for key, val in properties.items())
247
    
248
    flavor = util.get_flavor(flavor_id)
249
    password = util.random_password()
250
    
251
    count = VirtualMachine.objects.filter(owner=owner, deleted=False).count()
252
    if count >= settings.MAX_VMS_PER_USER:
253
        raise faults.OverLimit("Server count limit exceeded for your account.")
254
    
255
    # We must save the VM instance now, so that it gets a valid vm.backend_id.
256
    vm = VirtualMachine.objects.create(
257
        name=name,
258
        owner=owner,
259
        imageid=image_id,
260
        flavor=flavor)
261
    
262
    try:
263
        create_instance(vm, flavor, image, password, personality)
264
    except GanetiApiError:
265
        vm.delete()
266
        raise
267

    
268
    for key, val in metadata.items():
269
        VirtualMachineMetadata.objects.create(
270
            meta_key=key,
271
            meta_value=val,
272
            vm=vm)
273
    
274
    log.info('User %d created vm with %s cpus, %s ram and %s storage',
275
                    owner.id, flavor.cpu, flavor.ram, flavor.disk)
276
    
277
    server = vm_to_dict(vm, detail=True)
278
    server['status'] = 'BUILD'
279
    server['adminPass'] = password
280
    return render_server(request, server, status=202)
281

    
282

    
283
@util.api_method('GET')
284
def get_server_details(request, server_id):
285
    # Normal Response Codes: 200, 203
286
    # Error Response Codes: computeFault (400, 500),
287
    #                       serviceUnavailable (503),
288
    #                       unauthorized (401),
289
    #                       badRequest (400),
290
    #                       itemNotFound (404),
291
    #                       overLimit (413)
292
    
293
    log.debug('get_server_details %s', server_id)
294
    vm = util.get_vm(server_id, request.user)
295
    server = vm_to_dict(vm, detail=True)
296
    return render_server(request, server)
297

    
298

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

    
311
    req = util.get_request_dict(request)
312
    log.debug('update_server_name %s %s', server_id, req)
313
    
314
    try:
315
        name = req['server']['name']
316
    except (TypeError, KeyError):
317
        raise faults.BadRequest("Malformed request")
318

    
319
    vm = util.get_vm(server_id, request.user)
320
    vm.name = name
321
    vm.save()
322

    
323
    return HttpResponse(status=204)
324

    
325

    
326
@util.api_method('DELETE')
327
def delete_server(request, server_id):
328
    # Normal Response Codes: 204
329
    # Error Response Codes: computeFault (400, 500),
330
    #                       serviceUnavailable (503),
331
    #                       unauthorized (401),
332
    #                       itemNotFound (404),
333
    #                       unauthorized (401),
334
    #                       buildInProgress (409),
335
    #                       overLimit (413)
336
    
337
    log.debug('delete_server %s', server_id)
338
    vm = util.get_vm(server_id, request.user)
339
    delete_instance(vm)
340
    return HttpResponse(status=204)
341

    
342

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

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

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

    
362

    
363
@util.api_method('GET')
364
def list_addresses(request, server_id):
365
    # Normal Response Codes: 200, 203
366
    # Error Response Codes: computeFault (400, 500),
367
    #                       serviceUnavailable (503),
368
    #                       unauthorized (401),
369
    #                       badRequest (400),
370
    #                       overLimit (413)
371
    
372
    log.debug('list_addresses %s', server_id)
373
    vm = util.get_vm(server_id, request.user)
374
    addresses = [nic_to_dict(nic) for nic in vm.nics.all()]
375
    
376
    if request.serialization == 'xml':
377
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
378
    else:
379
        data = json.dumps({'addresses': {'values': addresses}})
380

    
381
    return HttpResponse(data, status=200)
382

    
383

    
384
@util.api_method('GET')
385
def list_addresses_by_network(request, server_id, network_id):
386
    # Normal Response Codes: 200, 203
387
    # Error Response Codes: computeFault (400, 500),
388
    #                       serviceUnavailable (503),
389
    #                       unauthorized (401),
390
    #                       badRequest (400),
391
    #                       itemNotFound (404),
392
    #                       overLimit (413)
393
    
394
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
395
    owner = request.user
396
    machine = util.get_vm(server_id, owner)
397
    network = util.get_network(network_id, owner)
398
    nic = util.get_nic(machine, network)
399
    address = nic_to_dict(nic)
400
    
401
    if request.serialization == 'xml':
402
        data = render_to_string('address.xml', {'address': address})
403
    else:
404
        data = json.dumps({'network': address})
405

    
406
    return HttpResponse(data, status=200)
407

    
408

    
409
@util.api_method('GET')
410
def list_metadata(request, server_id):
411
    # Normal Response Codes: 200, 203
412
    # Error Response Codes: computeFault (400, 500),
413
    #                       serviceUnavailable (503),
414
    #                       unauthorized (401),
415
    #                       badRequest (400),
416
    #                       overLimit (413)
417
    
418
    log.debug('list_server_metadata %s', server_id)
419
    vm = util.get_vm(server_id, request.user)
420
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
421
    return util.render_metadata(request, metadata, use_values=True, status=200)
422

    
423

    
424
@util.api_method('POST')
425
def update_metadata(request, server_id):
426
    # Normal Response Code: 201
427
    # Error Response Codes: computeFault (400, 500),
428
    #                       serviceUnavailable (503),
429
    #                       unauthorized (401),
430
    #                       badRequest (400),
431
    #                       buildInProgress (409),
432
    #                       badMediaType(415),
433
    #                       overLimit (413)
434
    
435
    req = util.get_request_dict(request)
436
    log.debug('update_server_metadata %s %s', server_id, req)
437
    vm = util.get_vm(server_id, request.user)
438
    try:
439
        metadata = req['metadata']
440
        assert isinstance(metadata, dict)
441
    except (KeyError, AssertionError):
442
        raise faults.BadRequest("Malformed request")
443
    
444
    for key, val in metadata.items():
445
        meta, created = vm.metadata.get_or_create(meta_key=key)
446
        meta.meta_value = val
447
        meta.save()
448
    
449
    vm.save()
450
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
451
    return util.render_metadata(request, vm_meta, status=201)
452

    
453

    
454
@util.api_method('GET')
455
def get_metadata_item(request, server_id, key):
456
    # Normal Response Codes: 200, 203
457
    # Error Response Codes: computeFault (400, 500),
458
    #                       serviceUnavailable (503),
459
    #                       unauthorized (401),
460
    #                       itemNotFound (404),
461
    #                       badRequest (400),
462
    #                       overLimit (413)
463
    
464
    log.debug('get_server_metadata_item %s %s', server_id, key)
465
    vm = util.get_vm(server_id, request.user)
466
    meta = util.get_vm_meta(vm, key)
467
    return util.render_meta(request, meta, status=200)
468

    
469

    
470
@util.api_method('PUT')
471
def create_metadata_item(request, server_id, key):
472
    # Normal Response Code: 201
473
    # Error Response Codes: computeFault (400, 500),
474
    #                       serviceUnavailable (503),
475
    #                       unauthorized (401),
476
    #                       itemNotFound (404),
477
    #                       badRequest (400),
478
    #                       buildInProgress (409),
479
    #                       badMediaType(415),
480
    #                       overLimit (413)
481
    
482
    req = util.get_request_dict(request)
483
    log.debug('create_server_metadata_item %s %s %s', server_id, key, req)
484
    vm = util.get_vm(server_id, request.user)
485
    try:
486
        metadict = req['meta']
487
        assert isinstance(metadict, dict)
488
        assert len(metadict) == 1
489
        assert key in metadict
490
    except (KeyError, AssertionError):
491
        raise faults.BadRequest("Malformed request")
492
    
493
    meta, created = VirtualMachineMetadata.objects.get_or_create(
494
        meta_key=key,
495
        vm=vm)
496
    
497
    meta.meta_value = metadict[key]
498
    meta.save()
499
    vm.save()
500
    return util.render_meta(request, meta, 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)
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)
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)