Statistics
| Branch: | Tag: | Revision:

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

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
    if count >= settings.MAX_VMS_PER_USER:
246
        raise faults.OverLimit("Server count limit exceeded for your account.")
247
    
248
    # We must save the VM instance now, so that it gets a valid vm.backend_id.
249
    vm = VirtualMachine.objects.create(
250
        name=name,
251
        userid=request.user_uniq,
252
        imageid=image_id,
253
        flavor=flavor)
254
    
255
    try:
256
        create_instance(vm, flavor, image, password, personality)
257
    except GanetiApiError:
258
        vm.delete()
259
        raise
260

    
261
    for key, val in metadata.items():
262
        VirtualMachineMetadata.objects.create(
263
            meta_key=key,
264
            meta_value=val,
265
            vm=vm)
266
    
267
    log.info('User %s created vm with %s cpus, %s ram and %s storage',
268
             request.user_uniq, flavor.cpu, flavor.ram, flavor.disk)
269
    
270
    server = vm_to_dict(vm, detail=True)
271
    server['status'] = 'BUILD'
272
    server['adminPass'] = password
273
    return render_server(request, server, status=202)
274

    
275

    
276
@util.api_method('GET')
277
def get_server_details(request, server_id):
278
    # Normal Response Codes: 200, 203
279
    # Error Response Codes: computeFault (400, 500),
280
    #                       serviceUnavailable (503),
281
    #                       unauthorized (401),
282
    #                       badRequest (400),
283
    #                       itemNotFound (404),
284
    #                       overLimit (413)
285
    
286
    log.debug('get_server_details %s', server_id)
287
    vm = util.get_vm(server_id, request.user_uniq)
288
    server = vm_to_dict(vm, detail=True)
289
    return render_server(request, server)
290

    
291

    
292
@util.api_method('PUT')
293
def update_server_name(request, server_id):
294
    # Normal Response Code: 204
295
    # Error Response Codes: computeFault (400, 500),
296
    #                       serviceUnavailable (503),
297
    #                       unauthorized (401),
298
    #                       badRequest (400),
299
    #                       badMediaType(415),
300
    #                       itemNotFound (404),
301
    #                       buildInProgress (409),
302
    #                       overLimit (413)
303

    
304
    req = util.get_request_dict(request)
305
    log.debug('update_server_name %s %s', server_id, req)
306
    
307
    try:
308
        name = req['server']['name']
309
    except (TypeError, KeyError):
310
        raise faults.BadRequest("Malformed request")
311

    
312
    vm = util.get_vm(server_id, request.user_uniq)
313
    vm.name = name
314
    vm.save()
315

    
316
    return HttpResponse(status=204)
317

    
318

    
319
@util.api_method('DELETE')
320
def delete_server(request, server_id):
321
    # Normal Response Codes: 204
322
    # Error Response Codes: computeFault (400, 500),
323
    #                       serviceUnavailable (503),
324
    #                       unauthorized (401),
325
    #                       itemNotFound (404),
326
    #                       unauthorized (401),
327
    #                       buildInProgress (409),
328
    #                       overLimit (413)
329
    
330
    log.debug('delete_server %s', server_id)
331
    vm = util.get_vm(server_id, request.user_uniq)
332
    delete_instance(vm)
333
    return HttpResponse(status=204)
334

    
335

    
336
@util.api_method('POST')
337
def server_action(request, server_id):
338
    req = util.get_request_dict(request)
339
    log.debug('server_action %s %s', server_id, req)
340
    vm = util.get_vm(server_id, request.user_uniq)
341
    if len(req) != 1:
342
        raise faults.BadRequest("Malformed request")
343

    
344
    key = req.keys()[0]
345
    val = req[key]
346

    
347
    try:
348
        assert isinstance(val, dict)
349
        return server_actions[key](request, vm, req[key])
350
    except KeyError:
351
        raise faults.BadRequest("Unknown action")
352
    except AssertionError:
353
        raise faults.BadRequest("Invalid argument")
354

    
355

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

    
374
    return HttpResponse(data, status=200)
375

    
376

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

    
398
    return HttpResponse(data, status=200)
399

    
400

    
401
@util.api_method('GET')
402
def list_metadata(request, server_id):
403
    # Normal Response Codes: 200, 203
404
    # Error Response Codes: computeFault (400, 500),
405
    #                       serviceUnavailable (503),
406
    #                       unauthorized (401),
407
    #                       badRequest (400),
408
    #                       overLimit (413)
409
    
410
    log.debug('list_server_metadata %s', server_id)
411
    vm = util.get_vm(server_id, request.user_uniq)
412
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
413
    return util.render_metadata(request, metadata, use_values=True, status=200)
414

    
415

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

    
445

    
446
@util.api_method('GET')
447
def get_metadata_item(request, server_id, key):
448
    # Normal Response Codes: 200, 203
449
    # Error Response Codes: computeFault (400, 500),
450
    #                       serviceUnavailable (503),
451
    #                       unauthorized (401),
452
    #                       itemNotFound (404),
453
    #                       badRequest (400),
454
    #                       overLimit (413)
455
    
456
    log.debug('get_server_metadata_item %s %s', server_id, key)
457
    vm = util.get_vm(server_id, request.user_uniq)
458
    meta = util.get_vm_meta(vm, key)
459
    d = {meta.meta_key: meta.meta_value}
460
    return util.render_meta(request, d, status=200)
461

    
462

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

    
496

    
497
@util.api_method('DELETE')
498
def delete_metadata_item(request, server_id, key):
499
    # Normal Response Code: 204
500
    # Error Response Codes: computeFault (400, 500),
501
    #                       serviceUnavailable (503),
502
    #                       unauthorized (401),
503
    #                       itemNotFound (404),
504
    #                       badRequest (400),
505
    #                       buildInProgress (409),
506
    #                       badMediaType(415),
507
    #                       overLimit (413),
508
    
509
    log.debug('delete_server_metadata_item %s %s', server_id, key)
510
    vm = util.get_vm(server_id, request.user_uniq)
511
    meta = util.get_vm_meta(vm, key)
512
    meta.delete()
513
    vm.save()
514
    return HttpResponse(status=204)
515

    
516

    
517
@util.api_method('GET')
518
def server_stats(request, server_id):
519
    # Normal Response Codes: 200
520
    # Error Response Codes: computeFault (400, 500),
521
    #                       serviceUnavailable (503),
522
    #                       unauthorized (401),
523
    #                       badRequest (400),
524
    #                       itemNotFound (404),
525
    #                       overLimit (413)
526
    
527
    log.debug('server_stats %s', server_id)
528
    vm = util.get_vm(server_id, request.user_uniq)
529
    #secret = util.encrypt(vm.backend_id)
530
    secret = vm.backend_id      # XXX disable backend id encryption
531
    
532
    stats = {
533
        'serverRef': vm.id,
534
        'refresh': settings.STATS_REFRESH_PERIOD,
535
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
536
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
537
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
538
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
539
    
540
    if request.serialization == 'xml':
541
        data = render_to_string('server_stats.xml', stats)
542
    else:
543
        data = json.dumps({'stats': stats})
544

    
545
    return HttpResponse(data, status=200)