Statistics
| Branch: | Tag: | Revision:

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

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

    
111
def nic_to_dict(nic):
112
    network = nic.network
113
    network_id = str(network.id) if not network.public else 'public'
114
    d = {'id': network_id, 'name': network.name, 'mac': nic.mac}
115
    if nic.firewall_profile:
116
        d['firewallProfile'] = nic.firewall_profile
117
    if nic.ipv4 or nic.ipv6:
118
        d['values'] = []
119
        if nic.ipv4:
120
            d['values'].append({'version': 4, 'addr': nic.ipv4})
121
        if nic.ipv6:
122
            d['values'].append({'version': 6, 'addr': nic.ipv6})
123
    return d
124

    
125

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

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

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

    
147

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

    
157

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

    
167
    log.debug('list_servers detail=%s', detail)
168
    user_vms = VirtualMachine.objects.filter(userid=request.user_uniq)
169
    since = util.isoparse(request.GET.get('changes-since'))
170

    
171
    if since:
172
        user_vms = user_vms.filter(updated__gte=since)
173
        if not user_vms:
174
            return HttpResponse(status=304)
175
    else:
176
        user_vms = user_vms.filter(deleted=False)
177

    
178
    servers = [vm_to_dict(server, detail) for server in user_vms]
179

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

    
187
    return HttpResponse(data, status=200)
188

    
189

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
294

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

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

    
310

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

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

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

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

    
335
    return HttpResponse(status=204)
336

    
337

    
338
@util.api_method('DELETE')
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
def create_metadata_item(request, server_id, key):
484
    # Normal Response Code: 201
485
    # Error Response Codes: computeFault (400, 500),
486
    #                       serviceUnavailable (503),
487
    #                       unauthorized (401),
488
    #                       itemNotFound (404),
489
    #                       badRequest (400),
490
    #                       buildInProgress (409),
491
    #                       badMediaType(415),
492
    #                       overLimit (413)
493

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

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

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

    
515

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

    
528
    log.debug('delete_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
    meta.delete()
532
    vm.save()
533
    return HttpResponse(status=204)
534

    
535

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

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

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

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

    
564
    return HttpResponse(data, status=200)