Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (21.2 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
    (r'^/(\d+)/diagnostics(?:.json)?$', 'get_server_diagnostics'),
70
)
71

    
72

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

    
81

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

    
92

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

    
101

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

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

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

    
144
        # include the latest vm diagnostic, if set
145
        diagnostic = vm.get_last_diagnostic()
146
        if diagnostic:
147
            d['diagnostics'] = diagnostics_to_dict([diagnostic])
148

    
149
    return d
150

    
151

    
152
def diagnostics_to_dict(diagnostics):
153
    """
154
    Extract api data from diagnostics QuerySet.
155
    """
156
    entries = list()
157

    
158
    for diagnostic in diagnostics:
159
        # format source date if set
160
        formatted_source_date = None
161
        if diagnostic.source_date:
162
            formatted_source_date = util.isoformat(diagnostic.source_date)
163

    
164
        entry = {
165
            'source': diagnostic.source,
166
            'created': util.isoformat(diagnostic.created),
167
            'message': diagnostic.message,
168
            'details': diagnostic.details,
169
            'level': diagnostic.level,
170
        }
171

    
172
        if formatted_source_date:
173
            entry['source_date'] = formatted_source_date
174

    
175
        entries.append(entry)
176

    
177
    return entries
178

    
179

    
180
def render_server(request, server, status=200):
181
    if request.serialization == 'xml':
182
        data = render_to_string('server.xml', {
183
            'server': server,
184
            'is_root': True})
185
    else:
186
        data = json.dumps({'server': server})
187
    return HttpResponse(data, status=status)
188

    
189

    
190
def render_diagnostics(request, diagnostics_dict, status=200):
191
    """
192
    Render diagnostics dictionary to json response.
193
    """
194
    return HttpResponse(json.dumps(diagnostics_dict), status=status)
195

    
196

    
197
@util.api_method('GET')
198
def get_server_diagnostics(request, server_id):
199
    """
200
    Virtual machine diagnostics api view.
201
    """
202
    log.debug('server_diagnostics %s', server_id)
203
    vm = util.get_vm(server_id, request.user_uniq)
204
    diagnostics = diagnostics_to_dict(vm.diagnostics.all())
205
    return render_diagnostics(request, diagnostics)
206

    
207

    
208
@util.api_method('GET')
209
def list_servers(request, detail=False):
210
    # Normal Response Codes: 200, 203
211
    # Error Response Codes: computeFault (400, 500),
212
    #                       serviceUnavailable (503),
213
    #                       unauthorized (401),
214
    #                       badRequest (400),
215
    #                       overLimit (413)
216

    
217
    log.debug('list_servers detail=%s', detail)
218
    user_vms = VirtualMachine.objects.filter(userid=request.user_uniq)
219

    
220
    since = util.isoparse(request.GET.get('changes-since'))
221

    
222
    if since:
223
        user_vms = user_vms.filter(updated__gte=since)
224
        if not user_vms:
225
            return HttpResponse(status=304)
226
    else:
227
        user_vms = user_vms.filter(deleted=False)
228

    
229
    servers = [vm_to_dict(server, detail) for server in user_vms]
230

    
231
    if request.serialization == 'xml':
232
        data = render_to_string('list_servers.xml', {
233
            'servers': servers,
234
            'detail': detail})
235
    else:
236
        data = json.dumps({'servers': {'values': servers}})
237

    
238
    return HttpResponse(data, status=200)
239

    
240

    
241
@util.api_method('POST')
242
@transaction.commit_on_success
243
def create_server(request):
244
    # Normal Response Code: 202
245
    # Error Response Codes: computeFault (400, 500),
246
    #                       serviceUnavailable (503),
247
    #                       unauthorized (401),
248
    #                       badMediaType(415),
249
    #                       itemNotFound (404),
250
    #                       badRequest (400),
251
    #                       serverCapacityUnavailable (503),
252
    #                       overLimit (413)
253
    req = util.get_request_dict(request)
254
    log.debug('create_server %s', req)
255

    
256
    try:
257
        server = req['server']
258
        name = server['name']
259
        metadata = server.get('metadata', {})
260
        assert isinstance(metadata, dict)
261
        image_id = server['imageRef']
262
        flavor_id = server['flavorRef']
263
        personality = server.get('personality', [])
264
        assert isinstance(personality, list)
265
    except (KeyError, AssertionError):
266
        raise faults.BadRequest("Malformed request")
267

    
268
    if len(personality) > settings.MAX_PERSONALITY:
269
        raise faults.OverLimit("Maximum number of personalities exceeded")
270

    
271
    for p in personality:
272
        # Verify that personalities are well-formed
273
        try:
274
            assert isinstance(p, dict)
275
            keys = set(p.keys())
276
            allowed = set(['contents', 'group', 'mode', 'owner', 'path'])
277
            assert keys.issubset(allowed)
278
            contents = p['contents']
279
            if len(contents) > settings.MAX_PERSONALITY_SIZE:
280
                # No need to decode if contents already exceed limit
281
                raise faults.OverLimit("Maximum size of personality exceeded")
282
            if len(b64decode(contents)) > settings.MAX_PERSONALITY_SIZE:
283
                raise faults.OverLimit("Maximum size of personality exceeded")
284
        except AssertionError:
285
            raise faults.BadRequest("Malformed personality in request")
286

    
287
    image = {}
288
    img = util.get_image(image_id, request.user_uniq)
289
    properties = img.get('properties', {})
290
    image['backend_id'] = img['location']
291
    image['format'] = img['disk_format']
292
    image['metadata'] = dict((key.upper(), val) \
293
                             for key, val in properties.items())
294

    
295
    flavor = util.get_flavor(flavor_id)
296
    password = util.random_password()
297

    
298
    count = VirtualMachine.objects.filter(userid=request.user_uniq,
299
                                          deleted=False).count()
300

    
301
    # get user limit
302
    vms_limit_for_user = \
303
        settings.VMS_USER_QUOTA.get(request.user_uniq,
304
                settings.MAX_VMS_PER_USER)
305

    
306
    if count >= vms_limit_for_user:
307
        raise faults.OverLimit("Server count limit exceeded for your account.")
308

    
309
    backend_allocator = BackendAllocator()
310
    backend = backend_allocator.allocate(flavor)
311
    if backend is None:
312
        raise Exception
313

    
314
    # We must save the VM instance now, so that it gets a valid
315
    # vm.backend_vm_id.
316
    vm = VirtualMachine.objects.create(
317
        name=name,
318
        backend=backend,
319
        userid=request.user_uniq,
320
        imageid=image_id,
321
        flavor=flavor)
322

    
323
    try:
324
        jobID = create_instance(vm, flavor, image, password, personality)
325
    except GanetiApiError:
326
        vm.delete()
327
        raise
328

    
329
    vm.backendjobid = jobID
330
    vm.save()
331

    
332
    for key, val in metadata.items():
333
        VirtualMachineMetadata.objects.create(
334
            meta_key=key,
335
            meta_value=val,
336
            vm=vm)
337

    
338
    log.info('User %s created vm with %s cpus, %s ram and %s storage',
339
             request.user_uniq, flavor.cpu, flavor.ram, flavor.disk)
340

    
341
    server = vm_to_dict(vm, detail=True)
342
    server['status'] = 'BUILD'
343
    server['adminPass'] = password
344
    return render_server(request, server, status=202)
345

    
346

    
347
@util.api_method('GET')
348
def get_server_details(request, server_id):
349
    # Normal Response Codes: 200, 203
350
    # Error Response Codes: computeFault (400, 500),
351
    #                       serviceUnavailable (503),
352
    #                       unauthorized (401),
353
    #                       badRequest (400),
354
    #                       itemNotFound (404),
355
    #                       overLimit (413)
356

    
357
    log.debug('get_server_details %s', server_id)
358
    vm = util.get_vm(server_id, request.user_uniq)
359
    server = vm_to_dict(vm, detail=True)
360
    return render_server(request, server)
361

    
362

    
363
@util.api_method('PUT')
364
def update_server_name(request, server_id):
365
    # Normal Response Code: 204
366
    # Error Response Codes: computeFault (400, 500),
367
    #                       serviceUnavailable (503),
368
    #                       unauthorized (401),
369
    #                       badRequest (400),
370
    #                       badMediaType(415),
371
    #                       itemNotFound (404),
372
    #                       buildInProgress (409),
373
    #                       overLimit (413)
374

    
375
    req = util.get_request_dict(request)
376
    log.debug('update_server_name %s %s', server_id, req)
377

    
378
    try:
379
        name = req['server']['name']
380
    except (TypeError, KeyError):
381
        raise faults.BadRequest("Malformed request")
382

    
383
    vm = util.get_vm(server_id, request.user_uniq)
384
    vm.name = name
385
    vm.save()
386

    
387
    return HttpResponse(status=204)
388

    
389

    
390
@util.api_method('DELETE')
391
@transaction.commit_on_success
392
def delete_server(request, server_id):
393
    # Normal Response Codes: 204
394
    # Error Response Codes: computeFault (400, 500),
395
    #                       serviceUnavailable (503),
396
    #                       unauthorized (401),
397
    #                       itemNotFound (404),
398
    #                       unauthorized (401),
399
    #                       buildInProgress (409),
400
    #                       overLimit (413)
401

    
402
    log.debug('delete_server %s', server_id)
403
    vm = util.get_vm(server_id, request.user_uniq)
404
    delete_instance(vm)
405
    return HttpResponse(status=204)
406

    
407

    
408
@util.api_method('POST')
409
def server_action(request, server_id):
410
    req = util.get_request_dict(request)
411
    log.debug('server_action %s %s', server_id, req)
412
    vm = util.get_vm(server_id, request.user_uniq)
413
    if len(req) != 1:
414
        raise faults.BadRequest("Malformed request")
415

    
416
    key = req.keys()[0]
417
    val = req[key]
418

    
419
    try:
420
        assert isinstance(val, dict)
421
        return server_actions[key](request, vm, req[key])
422
    except KeyError:
423
        raise faults.BadRequest("Unknown action")
424
    except AssertionError:
425
        raise faults.BadRequest("Invalid argument")
426

    
427

    
428
@util.api_method('GET')
429
def list_addresses(request, server_id):
430
    # Normal Response Codes: 200, 203
431
    # Error Response Codes: computeFault (400, 500),
432
    #                       serviceUnavailable (503),
433
    #                       unauthorized (401),
434
    #                       badRequest (400),
435
    #                       overLimit (413)
436

    
437
    log.debug('list_addresses %s', server_id)
438
    vm = util.get_vm(server_id, request.user_uniq)
439
    addresses = [nic_to_dict(nic) for nic in vm.nics.all()]
440

    
441
    if request.serialization == 'xml':
442
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
443
    else:
444
        data = json.dumps({'addresses': {'values': addresses}})
445

    
446
    return HttpResponse(data, status=200)
447

    
448

    
449
@util.api_method('GET')
450
def list_addresses_by_network(request, server_id, network_id):
451
    # Normal Response Codes: 200, 203
452
    # Error Response Codes: computeFault (400, 500),
453
    #                       serviceUnavailable (503),
454
    #                       unauthorized (401),
455
    #                       badRequest (400),
456
    #                       itemNotFound (404),
457
    #                       overLimit (413)
458

    
459
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
460
    machine = util.get_vm(server_id, request.user_uniq)
461
    network = util.get_network(network_id, request.user_uniq)
462
    nic = util.get_nic(machine, network)
463
    address = nic_to_dict(nic)
464

    
465
    if request.serialization == 'xml':
466
        data = render_to_string('address.xml', {'address': address})
467
    else:
468
        data = json.dumps({'network': address})
469

    
470
    return HttpResponse(data, status=200)
471

    
472

    
473
@util.api_method('GET')
474
def list_metadata(request, server_id):
475
    # Normal Response Codes: 200, 203
476
    # Error Response Codes: computeFault (400, 500),
477
    #                       serviceUnavailable (503),
478
    #                       unauthorized (401),
479
    #                       badRequest (400),
480
    #                       overLimit (413)
481

    
482
    log.debug('list_server_metadata %s', server_id)
483
    vm = util.get_vm(server_id, request.user_uniq)
484
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
485
    return util.render_metadata(request, metadata, use_values=True, status=200)
486

    
487

    
488
@util.api_method('POST')
489
def update_metadata(request, server_id):
490
    # Normal Response Code: 201
491
    # Error Response Codes: computeFault (400, 500),
492
    #                       serviceUnavailable (503),
493
    #                       unauthorized (401),
494
    #                       badRequest (400),
495
    #                       buildInProgress (409),
496
    #                       badMediaType(415),
497
    #                       overLimit (413)
498

    
499
    req = util.get_request_dict(request)
500
    log.debug('update_server_metadata %s %s', server_id, req)
501
    vm = util.get_vm(server_id, request.user_uniq)
502
    try:
503
        metadata = req['metadata']
504
        assert isinstance(metadata, dict)
505
    except (KeyError, AssertionError):
506
        raise faults.BadRequest("Malformed request")
507

    
508
    for key, val in metadata.items():
509
        meta, created = vm.metadata.get_or_create(meta_key=key)
510
        meta.meta_value = val
511
        meta.save()
512

    
513
    vm.save()
514
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
515
    return util.render_metadata(request, vm_meta, status=201)
516

    
517

    
518
@util.api_method('GET')
519
def get_metadata_item(request, server_id, key):
520
    # Normal Response Codes: 200, 203
521
    # Error Response Codes: computeFault (400, 500),
522
    #                       serviceUnavailable (503),
523
    #                       unauthorized (401),
524
    #                       itemNotFound (404),
525
    #                       badRequest (400),
526
    #                       overLimit (413)
527

    
528
    log.debug('get_server_metadata_item %s %s', server_id, key)
529
    vm = util.get_vm(server_id, request.user_uniq)
530
    meta = util.get_vm_meta(vm, key)
531
    d = {meta.meta_key: meta.meta_value}
532
    return util.render_meta(request, d, status=200)
533

    
534

    
535
@util.api_method('PUT')
536
@transaction.commit_on_success
537
def create_metadata_item(request, server_id, key):
538
    # Normal Response Code: 201
539
    # Error Response Codes: computeFault (400, 500),
540
    #                       serviceUnavailable (503),
541
    #                       unauthorized (401),
542
    #                       itemNotFound (404),
543
    #                       badRequest (400),
544
    #                       buildInProgress (409),
545
    #                       badMediaType(415),
546
    #                       overLimit (413)
547

    
548
    req = util.get_request_dict(request)
549
    log.debug('create_server_metadata_item %s %s %s', server_id, key, req)
550
    vm = util.get_vm(server_id, request.user_uniq)
551
    try:
552
        metadict = req['meta']
553
        assert isinstance(metadict, dict)
554
        assert len(metadict) == 1
555
        assert key in metadict
556
    except (KeyError, AssertionError):
557
        raise faults.BadRequest("Malformed request")
558

    
559
    meta, created = VirtualMachineMetadata.objects.get_or_create(
560
        meta_key=key,
561
        vm=vm)
562

    
563
    meta.meta_value = metadict[key]
564
    meta.save()
565
    vm.save()
566
    d = {meta.meta_key: meta.meta_value}
567
    return util.render_meta(request, d, status=201)
568

    
569

    
570
@util.api_method('DELETE')
571
@transaction.commit_on_success
572
def delete_metadata_item(request, server_id, key):
573
    # Normal Response Code: 204
574
    # Error Response Codes: computeFault (400, 500),
575
    #                       serviceUnavailable (503),
576
    #                       unauthorized (401),
577
    #                       itemNotFound (404),
578
    #                       badRequest (400),
579
    #                       buildInProgress (409),
580
    #                       badMediaType(415),
581
    #                       overLimit (413),
582

    
583
    log.debug('delete_server_metadata_item %s %s', server_id, key)
584
    vm = util.get_vm(server_id, request.user_uniq)
585
    meta = util.get_vm_meta(vm, key)
586
    meta.delete()
587
    vm.save()
588
    return HttpResponse(status=204)
589

    
590

    
591
@util.api_method('GET')
592
def server_stats(request, server_id):
593
    # Normal Response Codes: 200
594
    # Error Response Codes: computeFault (400, 500),
595
    #                       serviceUnavailable (503),
596
    #                       unauthorized (401),
597
    #                       badRequest (400),
598
    #                       itemNotFound (404),
599
    #                       overLimit (413)
600

    
601
    log.debug('server_stats %s', server_id)
602
    vm = util.get_vm(server_id, request.user_uniq)
603
    #secret = util.encrypt(vm.backend_vm_id)
604
    secret = vm.backend_vm_id      # XXX disable backend id encryption
605

    
606
    stats = {
607
        'serverRef': vm.id,
608
        'refresh': settings.STATS_REFRESH_PERIOD,
609
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
610
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
611
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
612
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
613

    
614
    if request.serialization == 'xml':
615
        data = render_to_string('server_stats.xml', stats)
616
    else:
617
        data = json.dumps({'stats': stats})
618

    
619
    return HttpResponse(data, status=200)