Statistics
| Branch: | Tag: | Revision:

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

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.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

    
112
def nic_to_dict(nic):
113
    d = {'id': util.construct_nic_id(nic),
114
         'network_id': str(nic.network.id),
115
         'mac_address': nic.mac,
116
         'ipv4': nic.ipv4 if nic.ipv4 else None,
117
         'ipv6': nic.ipv6 if nic.ipv6 else None}
118

    
119
    if nic.firewall_profile:
120
        d['firewallProfile'] = nic.firewall_profile
121
    return d
122

    
123

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

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

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

    
145

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

    
155

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

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

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

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

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

    
185
    return HttpResponse(data, status=200)
186

    
187

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
293

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

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

    
309

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

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

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

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

    
334
    return HttpResponse(status=204)
335

    
336

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

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

    
354

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

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

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

    
374

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

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

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

    
393
    return HttpResponse(data, status=200)
394

    
395

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

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

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

    
417
    return HttpResponse(data, status=200)
418

    
419

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

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

    
434

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

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

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

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

    
464

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

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

    
481

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

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

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

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

    
516

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

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

    
537

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

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

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

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

    
566
    return HttpResponse(data, status=200)