Statistics
| Branch: | Tag: | Revision:

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

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
def nic_to_dict(nic):
111
    network = nic.network
112
    network_id = str(network.id) if not network.public else 'public'
113
    ipv4 = nic.ipv4 if nic.ipv4 else None
114
    ipv6 = nic.ipv6 if nic.ipv6 else None
115

    
116
    d = {'id': util.construct_nic_id(nic), 'network_id': network_id, 'mac_address': nic.mac, 'ipv4': ipv4, 'ipv6': ipv6}
117
    if nic.firewall_profile:
118
        d['firewallProfile'] = nic.firewall_profile
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
        attachments = [nic_to_dict(nic) for nic in vm.nics.all()]
139
        if attachments:
140
            d['attachments'] = {'values': attachments}
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
    req = util.get_request_dict(request)
198
    log.debug('create_server %s', req)
199

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

    
212
    if len(personality) > settings.MAX_PERSONALITY:
213
        raise faults.OverLimit("Maximum number of personalities exceeded")
214

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

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

    
239
    flavor = util.get_flavor(flavor_id)
240
    password = util.random_password()
241

    
242
    count = VirtualMachine.objects.filter(userid=request.user_uniq,
243
                                          deleted=False).count()
244

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

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

    
253
    backend_allocator = BackendAllocator()
254
    backend = backend_allocator.allocate(flavor)
255
    if backend is None:
256
        raise Exception
257

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

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

    
273
    vm.backendjobid = jobID
274
    vm.save()
275

    
276
    for key, val in metadata.items():
277
        VirtualMachineMetadata.objects.create(
278
            meta_key=key,
279
            meta_value=val,
280
            vm=vm)
281

    
282
    log.info('User %s created vm with %s cpus, %s ram and %s storage',
283
             request.user_uniq, flavor.cpu, flavor.ram, flavor.disk)
284

    
285
    server = vm_to_dict(vm, detail=True)
286
    server['status'] = 'BUILD'
287
    server['adminPass'] = password
288
    return render_server(request, server, status=202)
289

    
290

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

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

    
306

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

    
319
    req = util.get_request_dict(request)
320
    log.debug('update_server_name %s %s', server_id, req)
321

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

    
327
    vm = util.get_vm(server_id, request.user_uniq)
328
    vm.name = name
329
    vm.save()
330

    
331
    return HttpResponse(status=204)
332

    
333

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

    
345
    log.debug('delete_server %s', server_id)
346
    vm = util.get_vm(server_id, request.user_uniq)
347
    delete_instance(vm)
348
    return HttpResponse(status=204)
349

    
350

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

    
359
    key = req.keys()[0]
360
    val = req[key]
361

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

    
370

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

    
380
    log.debug('list_addresses %s', server_id)
381
    vm = util.get_vm(server_id, request.user_uniq)
382
    addresses = [nic_to_dict(nic) for nic in vm.nics.all()]
383

    
384
    if request.serialization == 'xml':
385
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
386
    else:
387
        data = json.dumps({'addresses': {'values': addresses}})
388

    
389
    return HttpResponse(data, status=200)
390

    
391

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

    
402
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
403
    machine = util.get_vm(server_id, request.user_uniq)
404
    network = util.get_network(network_id, request.user_uniq)
405
    nic = util.get_nic(machine, network)
406
    address = nic_to_dict(nic)
407

    
408
    if request.serialization == 'xml':
409
        data = render_to_string('address.xml', {'address': address})
410
    else:
411
        data = json.dumps({'network': address})
412

    
413
    return HttpResponse(data, status=200)
414

    
415

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

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

    
430

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

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

    
451
    for key, val in metadata.items():
452
        meta, created = vm.metadata.get_or_create(meta_key=key)
453
        meta.meta_value = val
454
        meta.save()
455

    
456
    vm.save()
457
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
458
    return util.render_metadata(request, vm_meta, status=201)
459

    
460

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

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

    
477

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

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

    
501
    meta, created = VirtualMachineMetadata.objects.get_or_create(
502
        meta_key=key,
503
        vm=vm)
504

    
505
    meta.meta_value = metadict[key]
506
    meta.save()
507
    vm.save()
508
    d = {meta.meta_key: meta.meta_value}
509
    return util.render_meta(request, d, status=201)
510

    
511

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

    
524
    log.debug('delete_server_metadata_item %s %s', server_id, key)
525
    vm = util.get_vm(server_id, request.user_uniq)
526
    meta = util.get_vm_meta(vm, key)
527
    meta.delete()
528
    vm.save()
529
    return HttpResponse(status=204)
530

    
531

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

    
542
    log.debug('server_stats %s', server_id)
543
    vm = util.get_vm(server_id, request.user_uniq)
544
    #secret = util.encrypt(vm.backend_vm_id)
545
    secret = vm.backend_vm_id      # XXX disable backend id encryption
546

    
547
    stats = {
548
        'serverRef': vm.id,
549
        'refresh': settings.STATS_REFRESH_PERIOD,
550
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
551
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
552
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
553
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
554

    
555
    if request.serialization == 'xml':
556
        data = render_to_string('server_stats.xml', stats)
557
    else:
558
        data = json.dumps({'stats': stats})
559

    
560
    return HttpResponse(data, status=200)