Statistics
| Branch: | Tag: | Revision:

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

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

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

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

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

    
262
    try:
263
        create_instance(vm, flavor, image, password, personality)
264
    except GanetiApiError:
265
        vm.delete()
266
        raise
267

    
268
    for key, val in metadata.items():
269
        VirtualMachineMetadata.objects.create(
270
            meta_key=key,
271
            meta_value=val,
272
            vm=vm)
273

    
274
    log.info('User %s created vm with %s cpus, %s ram and %s storage',
275
             request.user_uniq, flavor.cpu, flavor.ram, flavor.disk)
276

    
277
    server = vm_to_dict(vm, detail=True)
278
    server['status'] = 'BUILD'
279
    server['adminPass'] = password
280
    return render_server(request, server, status=202)
281

    
282

    
283
@util.api_method('GET')
284
def get_server_details(request, server_id):
285
    # Normal Response Codes: 200, 203
286
    # Error Response Codes: computeFault (400, 500),
287
    #                       serviceUnavailable (503),
288
    #                       unauthorized (401),
289
    #                       badRequest (400),
290
    #                       itemNotFound (404),
291
    #                       overLimit (413)
292

    
293
    log.debug('get_server_details %s', server_id)
294
    vm = util.get_vm(server_id, request.user_uniq)
295
    server = vm_to_dict(vm, detail=True)
296
    return render_server(request, server)
297

    
298

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

    
311
    req = util.get_request_dict(request)
312
    log.debug('update_server_name %s %s', server_id, req)
313

    
314
    try:
315
        name = req['server']['name']
316
    except (TypeError, KeyError):
317
        raise faults.BadRequest("Malformed request")
318

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

    
323
    return HttpResponse(status=204)
324

    
325

    
326
@util.api_method('DELETE')
327
def delete_server(request, server_id):
328
    # Normal Response Codes: 204
329
    # Error Response Codes: computeFault (400, 500),
330
    #                       serviceUnavailable (503),
331
    #                       unauthorized (401),
332
    #                       itemNotFound (404),
333
    #                       unauthorized (401),
334
    #                       buildInProgress (409),
335
    #                       overLimit (413)
336

    
337
    log.debug('delete_server %s', server_id)
338
    vm = util.get_vm(server_id, request.user_uniq)
339
    delete_instance(vm)
340
    return HttpResponse(status=204)
341

    
342

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

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

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

    
362

    
363
@util.api_method('GET')
364
def list_addresses(request, server_id):
365
    # Normal Response Codes: 200, 203
366
    # Error Response Codes: computeFault (400, 500),
367
    #                       serviceUnavailable (503),
368
    #                       unauthorized (401),
369
    #                       badRequest (400),
370
    #                       overLimit (413)
371

    
372
    log.debug('list_addresses %s', server_id)
373
    vm = util.get_vm(server_id, request.user_uniq)
374
    addresses = [nic_to_dict(nic) for nic in vm.nics.all()]
375

    
376
    if request.serialization == 'xml':
377
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
378
    else:
379
        data = json.dumps({'addresses': {'values': addresses}})
380

    
381
    return HttpResponse(data, status=200)
382

    
383

    
384
@util.api_method('GET')
385
def list_addresses_by_network(request, server_id, network_id):
386
    # Normal Response Codes: 200, 203
387
    # Error Response Codes: computeFault (400, 500),
388
    #                       serviceUnavailable (503),
389
    #                       unauthorized (401),
390
    #                       badRequest (400),
391
    #                       itemNotFound (404),
392
    #                       overLimit (413)
393

    
394
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
395
    machine = util.get_vm(server_id, request.user_uniq)
396
    network = util.get_network(network_id, request.user_uniq)
397
    nic = util.get_nic(machine, network)
398
    address = nic_to_dict(nic)
399

    
400
    if request.serialization == 'xml':
401
        data = render_to_string('address.xml', {'address': address})
402
    else:
403
        data = json.dumps({'network': address})
404

    
405
    return HttpResponse(data, status=200)
406

    
407

    
408
@util.api_method('GET')
409
def list_metadata(request, server_id):
410
    # Normal Response Codes: 200, 203
411
    # Error Response Codes: computeFault (400, 500),
412
    #                       serviceUnavailable (503),
413
    #                       unauthorized (401),
414
    #                       badRequest (400),
415
    #                       overLimit (413)
416

    
417
    log.debug('list_server_metadata %s', server_id)
418
    vm = util.get_vm(server_id, request.user_uniq)
419
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
420
    return util.render_metadata(request, metadata, use_values=True, status=200)
421

    
422

    
423
@util.api_method('POST')
424
def update_metadata(request, server_id):
425
    # Normal Response Code: 201
426
    # Error Response Codes: computeFault (400, 500),
427
    #                       serviceUnavailable (503),
428
    #                       unauthorized (401),
429
    #                       badRequest (400),
430
    #                       buildInProgress (409),
431
    #                       badMediaType(415),
432
    #                       overLimit (413)
433

    
434
    req = util.get_request_dict(request)
435
    log.debug('update_server_metadata %s %s', server_id, req)
436
    vm = util.get_vm(server_id, request.user_uniq)
437
    try:
438
        metadata = req['metadata']
439
        assert isinstance(metadata, dict)
440
    except (KeyError, AssertionError):
441
        raise faults.BadRequest("Malformed request")
442

    
443
    for key, val in metadata.items():
444
        meta, created = vm.metadata.get_or_create(meta_key=key)
445
        meta.meta_value = val
446
        meta.save()
447

    
448
    vm.save()
449
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
450
    return util.render_metadata(request, vm_meta, status=201)
451

    
452

    
453
@util.api_method('GET')
454
def get_metadata_item(request, server_id, key):
455
    # Normal Response Codes: 200, 203
456
    # Error Response Codes: computeFault (400, 500),
457
    #                       serviceUnavailable (503),
458
    #                       unauthorized (401),
459
    #                       itemNotFound (404),
460
    #                       badRequest (400),
461
    #                       overLimit (413)
462

    
463
    log.debug('get_server_metadata_item %s %s', server_id, key)
464
    vm = util.get_vm(server_id, request.user_uniq)
465
    meta = util.get_vm_meta(vm, key)
466
    d = {meta.meta_key: meta.meta_value}
467
    return util.render_meta(request, d, status=200)
468

    
469

    
470
@util.api_method('PUT')
471
def create_metadata_item(request, server_id, key):
472
    # Normal Response Code: 201
473
    # Error Response Codes: computeFault (400, 500),
474
    #                       serviceUnavailable (503),
475
    #                       unauthorized (401),
476
    #                       itemNotFound (404),
477
    #                       badRequest (400),
478
    #                       buildInProgress (409),
479
    #                       badMediaType(415),
480
    #                       overLimit (413)
481

    
482
    req = util.get_request_dict(request)
483
    log.debug('create_server_metadata_item %s %s %s', server_id, key, req)
484
    vm = util.get_vm(server_id, request.user_uniq)
485
    try:
486
        metadict = req['meta']
487
        assert isinstance(metadict, dict)
488
        assert len(metadict) == 1
489
        assert key in metadict
490
    except (KeyError, AssertionError):
491
        raise faults.BadRequest("Malformed request")
492

    
493
    meta, created = VirtualMachineMetadata.objects.get_or_create(
494
        meta_key=key,
495
        vm=vm)
496

    
497
    meta.meta_value = metadict[key]
498
    meta.save()
499
    vm.save()
500
    d = {meta.meta_key: meta.meta_value}
501
    return util.render_meta(request, d, status=201)
502

    
503

    
504
@util.api_method('DELETE')
505
def delete_metadata_item(request, server_id, key):
506
    # Normal Response Code: 204
507
    # Error Response Codes: computeFault (400, 500),
508
    #                       serviceUnavailable (503),
509
    #                       unauthorized (401),
510
    #                       itemNotFound (404),
511
    #                       badRequest (400),
512
    #                       buildInProgress (409),
513
    #                       badMediaType(415),
514
    #                       overLimit (413),
515

    
516
    log.debug('delete_server_metadata_item %s %s', server_id, key)
517
    vm = util.get_vm(server_id, request.user_uniq)
518
    meta = util.get_vm_meta(vm, key)
519
    meta.delete()
520
    vm.save()
521
    return HttpResponse(status=204)
522

    
523

    
524
@util.api_method('GET')
525
def server_stats(request, server_id):
526
    # Normal Response Codes: 200
527
    # Error Response Codes: computeFault (400, 500),
528
    #                       serviceUnavailable (503),
529
    #                       unauthorized (401),
530
    #                       badRequest (400),
531
    #                       itemNotFound (404),
532
    #                       overLimit (413)
533

    
534
    log.debug('server_stats %s', server_id)
535
    vm = util.get_vm(server_id, request.user_uniq)
536
    #secret = util.encrypt(vm.backend_vm_id)
537
    secret = vm.backend_vm_id      # XXX disable backend id encryption
538

    
539
    stats = {
540
        'serverRef': vm.id,
541
        'refresh': settings.STATS_REFRESH_PERIOD,
542
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
543
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
544
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
545
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
546

    
547
    if request.serialization == 'xml':
548
        data = render_to_string('server_stats.xml', stats)
549
    else:
550
        data = json.dumps({'stats': stats})
551

    
552
    return HttpResponse(data, status=200)