Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (19.6 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 Backend, 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
from synnefo.logic.backend_allocator import BackendAllocator
51

    
52
from django.utils import importlib
53

    
54

    
55

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

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

    
70

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

    
79

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

    
90

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

    
99

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

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

    
116
    d = {'id': util.construct_nic_id(nic), 'network_id': network_id, 'mac_address': nic.mac, 'ipv4': ipv4, 'ipv6': ipv6}
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
                                             deleted=False)
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

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

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

    
182
    return HttpResponse(data, status=200)
183

    
184

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

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

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

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

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

    
238
    flavor = util.get_flavor(flavor_id)
239
    password = util.random_password()
240

    
241
    count = VirtualMachine.objects.filter(userid=request.user_uniq,
242
                                          deleted=False).count()
243

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

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

    
252
    backend_allocator = BackendAllocator()
253
    backend = backend_allocator.allocate(flavor)
254
    if backend is None:
255
        raise Exception
256

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

    
266
    try:
267
        jobID = create_instance(vm, flavor, image, password, personality)
268
    except GanetiApiError:
269
        vm.delete()
270
        raise
271

    
272
    vm.backendjobid = jobID
273
    vm.save()
274

    
275
    for key, val in metadata.items():
276
        VirtualMachineMetadata.objects.create(
277
            meta_key=key,
278
            meta_value=val,
279
            vm=vm)
280

    
281
    log.info('User %s created vm with %s cpus, %s ram and %s storage',
282
             request.user_uniq, flavor.cpu, flavor.ram, flavor.disk)
283

    
284
    server = vm_to_dict(vm, detail=True)
285
    server['status'] = 'BUILD'
286
    server['adminPass'] = password
287
    return render_server(request, server, status=202)
288

    
289

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

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

    
305

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

    
318
    req = util.get_request_dict(request)
319
    log.debug('update_server_name %s %s', server_id, req)
320

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

    
326
    vm = util.get_vm(server_id, request.user_uniq)
327
    vm.name = name
328
    vm.save()
329

    
330
    return HttpResponse(status=204)
331

    
332

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

    
344
    log.debug('delete_server %s', server_id)
345
    vm = util.get_vm(server_id, request.user_uniq)
346
    delete_instance(vm)
347
    return HttpResponse(status=204)
348

    
349

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

    
358
    key = req.keys()[0]
359
    val = req[key]
360

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

    
369

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

    
379
    log.debug('list_addresses %s', server_id)
380
    vm = util.get_vm(server_id, request.user_uniq)
381
    addresses = [nic_to_dict(nic) for nic in vm.nics.all()]
382

    
383
    if request.serialization == 'xml':
384
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
385
    else:
386
        data = json.dumps({'addresses': {'values': addresses}})
387

    
388
    return HttpResponse(data, status=200)
389

    
390

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

    
401
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
402
    machine = util.get_vm(server_id, request.user_uniq)
403
    network = util.get_network(network_id, request.user_uniq)
404
    nic = util.get_nic(machine, network)
405
    address = nic_to_dict(nic)
406

    
407
    if request.serialization == 'xml':
408
        data = render_to_string('address.xml', {'address': address})
409
    else:
410
        data = json.dumps({'network': address})
411

    
412
    return HttpResponse(data, status=200)
413

    
414

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

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

    
429

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

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

    
450
    for key, val in metadata.items():
451
        meta, created = vm.metadata.get_or_create(meta_key=key)
452
        meta.meta_value = val
453
        meta.save()
454

    
455
    vm.save()
456
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
457
    return util.render_metadata(request, vm_meta, status=201)
458

    
459

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

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

    
476

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

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

    
500
    meta, created = VirtualMachineMetadata.objects.get_or_create(
501
        meta_key=key,
502
        vm=vm)
503

    
504
    meta.meta_value = metadict[key]
505
    meta.save()
506
    vm.save()
507
    d = {meta.meta_key: meta.meta_value}
508
    return util.render_meta(request, d, status=201)
509

    
510

    
511
@util.api_method('DELETE')
512
def delete_metadata_item(request, server_id, key):
513
    # Normal Response Code: 204
514
    # Error Response Codes: computeFault (400, 500),
515
    #                       serviceUnavailable (503),
516
    #                       unauthorized (401),
517
    #                       itemNotFound (404),
518
    #                       badRequest (400),
519
    #                       buildInProgress (409),
520
    #                       badMediaType(415),
521
    #                       overLimit (413),
522

    
523
    log.debug('delete_server_metadata_item %s %s', server_id, key)
524
    vm = util.get_vm(server_id, request.user_uniq)
525
    meta = util.get_vm_meta(vm, key)
526
    meta.delete()
527
    vm.save()
528
    return HttpResponse(status=204)
529

    
530

    
531
@util.api_method('GET')
532
def server_stats(request, server_id):
533
    # Normal Response Codes: 200
534
    # Error Response Codes: computeFault (400, 500),
535
    #                       serviceUnavailable (503),
536
    #                       unauthorized (401),
537
    #                       badRequest (400),
538
    #                       itemNotFound (404),
539
    #                       overLimit (413)
540

    
541
    log.debug('server_stats %s', server_id)
542
    vm = util.get_vm(server_id, request.user_uniq)
543
    #secret = util.encrypt(vm.backend_vm_id)
544
    secret = vm.backend_vm_id      # XXX disable backend id encryption
545

    
546
    stats = {
547
        'serverRef': vm.id,
548
        'refresh': settings.STATS_REFRESH_PERIOD,
549
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
550
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
551
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
552
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
553

    
554
    if request.serialization == 'xml':
555
        data = render_to_string('server_stats.xml', stats)
556
    else:
557
        data = json.dumps({'stats': stats})
558

    
559
    return HttpResponse(data, status=200)