Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (19.4 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
import random
35

    
36
from base64 import b64decode
37
from logging import getLogger
38

    
39
from django.conf import settings
40
from django.conf.urls.defaults import patterns
41
from django.http import HttpResponse
42
from django.template.loader import render_to_string
43
from django.utils import simplejson as json
44

    
45
from synnefo.api import faults, util
46
from synnefo.api.actions import server_actions
47
from synnefo.api.common import method_not_allowed
48
from synnefo.db.models import Backend, VirtualMachine, VirtualMachineMetadata
49
from synnefo.logic.backend import create_instance, delete_instance
50
from synnefo.logic.utils import get_rsapi_state
51
from synnefo.util.rapi import GanetiApiError
52

    
53

    
54
log = getLogger('synnefo.api')
55

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

    
68

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

    
77

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

    
88

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

    
97

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

    
108

    
109
def nic_to_dict(nic):
110
    network = nic.network
111
    network_id = str(network.id) if not network.public else 'public'
112
    d = {'id': network_id, 'name': network.name, 'mac': nic.mac}
113
    if nic.firewall_profile:
114
        d['firewallProfile'] = nic.firewall_profile
115
    if nic.ipv4 or nic.ipv6:
116
        d['values'] = []
117
        if nic.ipv4:
118
            d['values'].append({'version': 4, 'addr': nic.ipv4})
119
        if nic.ipv6:
120
            d['values'].append({'version': 6, 'addr': nic.ipv6})
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
        addresses = [nic_to_dict(nic) for nic in vm.nics.all()]
141
        if addresses:
142
            d['addresses'] = {'values': addresses}
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
    since = util.isoparse(request.GET.get('changes-since'))
168

    
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
def create_server(request):
190
    # Normal Response Code: 202
191
    # Error Response Codes: computeFault (400, 500),
192
    #                       serviceUnavailable (503),
193
    #                       unauthorized (401),
194
    #                       badMediaType(415),
195
    #                       itemNotFound (404),
196
    #                       badRequest (400),
197
    #                       serverCapacityUnavailable (503),
198
    #                       overLimit (413)
199

    
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 = random.choice(Backend.objects.all())
257
    # We must save the VM instance now, so that it gets a
258
    # valid vm.backend_vm_id.
259
    vm = VirtualMachine.objects.create(
260
        name=name,
261
        backend=backend,
262
        userid=request.user_uniq,
263
        imageid=image_id,
264
        flavor=flavor)
265

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

    
272
    for key, val in metadata.items():
273
        VirtualMachineMetadata.objects.create(
274
            meta_key=key,
275
            meta_value=val,
276
            vm=vm)
277

    
278
    log.info('User %s created vm with %s cpus, %s ram and %s storage',
279
             request.user_uniq, flavor.cpu, flavor.ram, flavor.disk)
280

    
281
    server = vm_to_dict(vm, detail=True)
282
    server['status'] = 'BUILD'
283
    server['adminPass'] = password
284
    return render_server(request, server, status=202)
285

    
286

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

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

    
302

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

    
315
    req = util.get_request_dict(request)
316
    log.debug('update_server_name %s %s', server_id, req)
317

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

    
323
    vm = util.get_vm(server_id, request.user_uniq)
324
    vm.name = name
325
    vm.save()
326

    
327
    return HttpResponse(status=204)
328

    
329

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

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

    
346

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

    
355
    key = req.keys()[0]
356
    val = req[key]
357

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

    
366

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

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

    
380
    if request.serialization == 'xml':
381
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
382
    else:
383
        data = json.dumps({'addresses': {'values': addresses}})
384

    
385
    return HttpResponse(data, status=200)
386

    
387

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

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

    
404
    if request.serialization == 'xml':
405
        data = render_to_string('address.xml', {'address': address})
406
    else:
407
        data = json.dumps({'network': address})
408

    
409
    return HttpResponse(data, status=200)
410

    
411

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

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

    
426

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

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

    
447
    for key, val in metadata.items():
448
        meta, created = vm.metadata.get_or_create(meta_key=key)
449
        meta.meta_value = val
450
        meta.save()
451

    
452
    vm.save()
453
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
454
    return util.render_metadata(request, vm_meta, status=201)
455

    
456

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

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

    
473

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

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

    
497
    meta, created = VirtualMachineMetadata.objects.get_or_create(
498
        meta_key=key,
499
        vm=vm)
500

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

    
507

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

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

    
527

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

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

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

    
551
    if request.serialization == 'xml':
552
        data = render_to_string('server_stats.xml', stats)
553
    else:
554
        data = json.dumps({'stats': stats})
555

    
556
    return HttpResponse(data, status=200)