Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (20.5 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
)
70

    
71

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

    
80

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

    
91

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

    
100

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

    
111

    
112
def nic_to_dict(nic):
113
    d = {'id': util.construct_nic_id(nic),
114
         'network_id': str(nic.network.id),
115
         'mac_address': nic.mac,
116
         'ipv4': nic.ipv4 if nic.ipv4 else None,
117
         'ipv6': nic.ipv6 if nic.ipv6 else None}
118

    
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
    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

    
168
    since = util.isoparse(request.GET.get('changes-since'))
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
# Use manual transactions. Backend and IP pool allocations need exclusive
190
# access (SELECT..FOR UPDATE). Running create_server with commit_on_success
191
# would result in backends and public networks to be locked until the job is
192
# sent to the Ganeti backend.
193
@transaction.commit_manually
194
def create_server(request):
195
    # Normal Response Code: 202
196
    # Error Response Codes: computeFault (400, 500),
197
    #                       serviceUnavailable (503),
198
    #                       unauthorized (401),
199
    #                       badMediaType(415),
200
    #                       itemNotFound (404),
201
    #                       badRequest (400),
202
    #                       serverCapacityUnavailable (503),
203
    #                       overLimit (413)
204
    req = util.get_request_dict(request)
205
    log.debug('create_server %s', req)
206

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

    
219
    if len(personality) > settings.MAX_PERSONALITY:
220
        raise faults.OverLimit("Maximum number of personalities exceeded")
221

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

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

    
246
    flavor = util.get_flavor(flavor_id)
247
    password = util.random_password()
248

    
249
    count = VirtualMachine.objects.filter(userid=request.user_uniq,
250
                                          deleted=False).count()
251

    
252
    # get user limit
253
    vms_limit_for_user = \
254
        settings.VMS_USER_QUOTA.get(request.user_uniq,
255
                settings.MAX_VMS_PER_USER)
256

    
257
    if count >= vms_limit_for_user:
258
        raise faults.OverLimit("Server count limit exceeded for your account.")
259

    
260
    backend_allocator = BackendAllocator()
261
    backend = backend_allocator.allocate(flavor)
262

    
263
    if backend is None:
264
        transaction.rollback()
265
        raise Exception
266
    transaction.commit()
267

    
268
    if settings.PUBLIC_ROUTED_USE_POOL:
269
        (network, address) = util.allocate_public_address(backend)
270
        if address is None:
271
            transaction.rollback()
272
            raise faults.OverLimit("Can not allocate IP for new machine."
273
                                   " Public networks are full.")
274
        transaction.commit()
275
        nic = {'ip': address, 'network': network.backend_id}
276
    else:
277
        nic = {'ip': 'pool', 'network': network.backend_id}
278

    
279
    # We must save the VM instance now, so that it gets a valid
280
    # vm.backend_vm_id.
281
    vm = VirtualMachine.objects.create(
282
        name=name,
283
        backend=backend,
284
        userid=request.user_uniq,
285
        imageid=image_id,
286
        flavor=flavor)
287

    
288
    try:
289
        jobID = create_instance(vm, nic, flavor, image, password, personality)
290
    except GanetiApiError:
291
        vm.delete()
292
        raise
293

    
294
    vm.backendjobid = jobID
295
    vm.save()
296

    
297
    for key, val in metadata.items():
298
        VirtualMachineMetadata.objects.create(
299
            meta_key=key,
300
            meta_value=val,
301
            vm=vm)
302

    
303
    log.info('User %s created vm with %s cpus, %s ram and %s storage',
304
             request.user_uniq, flavor.cpu, flavor.ram, flavor.disk)
305

    
306
    server = vm_to_dict(vm, detail=True)
307
    server['status'] = 'BUILD'
308
    server['adminPass'] = password
309

    
310
    respsone = render_server(request, server, status=202)
311
    transaction.commit()
312

    
313
    return respsone
314

    
315

    
316
@util.api_method('GET')
317
def get_server_details(request, server_id):
318
    # Normal Response Codes: 200, 203
319
    # Error Response Codes: computeFault (400, 500),
320
    #                       serviceUnavailable (503),
321
    #                       unauthorized (401),
322
    #                       badRequest (400),
323
    #                       itemNotFound (404),
324
    #                       overLimit (413)
325

    
326
    log.debug('get_server_details %s', server_id)
327
    vm = util.get_vm(server_id, request.user_uniq)
328
    server = vm_to_dict(vm, detail=True)
329
    return render_server(request, server)
330

    
331

    
332
@util.api_method('PUT')
333
def update_server_name(request, server_id):
334
    # Normal Response Code: 204
335
    # Error Response Codes: computeFault (400, 500),
336
    #                       serviceUnavailable (503),
337
    #                       unauthorized (401),
338
    #                       badRequest (400),
339
    #                       badMediaType(415),
340
    #                       itemNotFound (404),
341
    #                       buildInProgress (409),
342
    #                       overLimit (413)
343

    
344
    req = util.get_request_dict(request)
345
    log.debug('update_server_name %s %s', server_id, req)
346

    
347
    try:
348
        name = req['server']['name']
349
    except (TypeError, KeyError):
350
        raise faults.BadRequest("Malformed request")
351

    
352
    vm = util.get_vm(server_id, request.user_uniq)
353
    vm.name = name
354
    vm.save()
355

    
356
    return HttpResponse(status=204)
357

    
358

    
359
@util.api_method('DELETE')
360
@transaction.commit_on_success
361
def delete_server(request, server_id):
362
    # Normal Response Codes: 204
363
    # Error Response Codes: computeFault (400, 500),
364
    #                       serviceUnavailable (503),
365
    #                       unauthorized (401),
366
    #                       itemNotFound (404),
367
    #                       unauthorized (401),
368
    #                       buildInProgress (409),
369
    #                       overLimit (413)
370

    
371
    log.debug('delete_server %s', server_id)
372
    vm = util.get_vm(server_id, request.user_uniq)
373
    delete_instance(vm)
374
    return HttpResponse(status=204)
375

    
376

    
377
@util.api_method('POST')
378
def server_action(request, server_id):
379
    req = util.get_request_dict(request)
380
    log.debug('server_action %s %s', server_id, req)
381
    vm = util.get_vm(server_id, request.user_uniq)
382
    if len(req) != 1:
383
        raise faults.BadRequest("Malformed request")
384

    
385
    key = req.keys()[0]
386
    val = req[key]
387

    
388
    try:
389
        assert isinstance(val, dict)
390
        return server_actions[key](request, vm, req[key])
391
    except KeyError:
392
        raise faults.BadRequest("Unknown action")
393
    except AssertionError:
394
        raise faults.BadRequest("Invalid argument")
395

    
396

    
397
@util.api_method('GET')
398
def list_addresses(request, server_id):
399
    # Normal Response Codes: 200, 203
400
    # Error Response Codes: computeFault (400, 500),
401
    #                       serviceUnavailable (503),
402
    #                       unauthorized (401),
403
    #                       badRequest (400),
404
    #                       overLimit (413)
405

    
406
    log.debug('list_addresses %s', server_id)
407
    vm = util.get_vm(server_id, request.user_uniq)
408
    addresses = [nic_to_dict(nic) for nic in vm.nics.all()]
409

    
410
    if request.serialization == 'xml':
411
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
412
    else:
413
        data = json.dumps({'addresses': {'values': addresses}})
414

    
415
    return HttpResponse(data, status=200)
416

    
417

    
418
@util.api_method('GET')
419
def list_addresses_by_network(request, server_id, network_id):
420
    # Normal Response Codes: 200, 203
421
    # Error Response Codes: computeFault (400, 500),
422
    #                       serviceUnavailable (503),
423
    #                       unauthorized (401),
424
    #                       badRequest (400),
425
    #                       itemNotFound (404),
426
    #                       overLimit (413)
427

    
428
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
429
    machine = util.get_vm(server_id, request.user_uniq)
430
    network = util.get_network(network_id, request.user_uniq)
431
    nic = util.get_nic(machine, network)
432
    address = nic_to_dict(nic)
433

    
434
    if request.serialization == 'xml':
435
        data = render_to_string('address.xml', {'address': address})
436
    else:
437
        data = json.dumps({'network': address})
438

    
439
    return HttpResponse(data, status=200)
440

    
441

    
442
@util.api_method('GET')
443
def list_metadata(request, server_id):
444
    # Normal Response Codes: 200, 203
445
    # Error Response Codes: computeFault (400, 500),
446
    #                       serviceUnavailable (503),
447
    #                       unauthorized (401),
448
    #                       badRequest (400),
449
    #                       overLimit (413)
450

    
451
    log.debug('list_server_metadata %s', server_id)
452
    vm = util.get_vm(server_id, request.user_uniq)
453
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
454
    return util.render_metadata(request, metadata, use_values=True, status=200)
455

    
456

    
457
@util.api_method('POST')
458
def update_metadata(request, server_id):
459
    # Normal Response Code: 201
460
    # Error Response Codes: computeFault (400, 500),
461
    #                       serviceUnavailable (503),
462
    #                       unauthorized (401),
463
    #                       badRequest (400),
464
    #                       buildInProgress (409),
465
    #                       badMediaType(415),
466
    #                       overLimit (413)
467

    
468
    req = util.get_request_dict(request)
469
    log.debug('update_server_metadata %s %s', server_id, req)
470
    vm = util.get_vm(server_id, request.user_uniq)
471
    try:
472
        metadata = req['metadata']
473
        assert isinstance(metadata, dict)
474
    except (KeyError, AssertionError):
475
        raise faults.BadRequest("Malformed request")
476

    
477
    for key, val in metadata.items():
478
        meta, created = vm.metadata.get_or_create(meta_key=key)
479
        meta.meta_value = val
480
        meta.save()
481

    
482
    vm.save()
483
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
484
    return util.render_metadata(request, vm_meta, status=201)
485

    
486

    
487
@util.api_method('GET')
488
def get_metadata_item(request, server_id, key):
489
    # Normal Response Codes: 200, 203
490
    # Error Response Codes: computeFault (400, 500),
491
    #                       serviceUnavailable (503),
492
    #                       unauthorized (401),
493
    #                       itemNotFound (404),
494
    #                       badRequest (400),
495
    #                       overLimit (413)
496

    
497
    log.debug('get_server_metadata_item %s %s', server_id, key)
498
    vm = util.get_vm(server_id, request.user_uniq)
499
    meta = util.get_vm_meta(vm, key)
500
    d = {meta.meta_key: meta.meta_value}
501
    return util.render_meta(request, d, status=200)
502

    
503

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

    
517
    req = util.get_request_dict(request)
518
    log.debug('create_server_metadata_item %s %s %s', server_id, key, req)
519
    vm = util.get_vm(server_id, request.user_uniq)
520
    try:
521
        metadict = req['meta']
522
        assert isinstance(metadict, dict)
523
        assert len(metadict) == 1
524
        assert key in metadict
525
    except (KeyError, AssertionError):
526
        raise faults.BadRequest("Malformed request")
527

    
528
    meta, created = VirtualMachineMetadata.objects.get_or_create(
529
        meta_key=key,
530
        vm=vm)
531

    
532
    meta.meta_value = metadict[key]
533
    meta.save()
534
    vm.save()
535
    d = {meta.meta_key: meta.meta_value}
536
    return util.render_meta(request, d, status=201)
537

    
538

    
539
@util.api_method('DELETE')
540
@transaction.commit_on_success
541
def delete_metadata_item(request, server_id, key):
542
    # Normal Response Code: 204
543
    # Error Response Codes: computeFault (400, 500),
544
    #                       serviceUnavailable (503),
545
    #                       unauthorized (401),
546
    #                       itemNotFound (404),
547
    #                       badRequest (400),
548
    #                       buildInProgress (409),
549
    #                       badMediaType(415),
550
    #                       overLimit (413),
551

    
552
    log.debug('delete_server_metadata_item %s %s', server_id, key)
553
    vm = util.get_vm(server_id, request.user_uniq)
554
    meta = util.get_vm_meta(vm, key)
555
    meta.delete()
556
    vm.save()
557
    return HttpResponse(status=204)
558

    
559

    
560
@util.api_method('GET')
561
def server_stats(request, server_id):
562
    # Normal Response Codes: 200
563
    # Error Response Codes: computeFault (400, 500),
564
    #                       serviceUnavailable (503),
565
    #                       unauthorized (401),
566
    #                       badRequest (400),
567
    #                       itemNotFound (404),
568
    #                       overLimit (413)
569

    
570
    log.debug('server_stats %s', server_id)
571
    vm = util.get_vm(server_id, request.user_uniq)
572
    #secret = util.encrypt(vm.backend_vm_id)
573
    secret = vm.backend_vm_id      # XXX disable backend id encryption
574

    
575
    stats = {
576
        'serverRef': vm.id,
577
        'refresh': settings.STATS_REFRESH_PERIOD,
578
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
579
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
580
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
581
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
582

    
583
    if request.serialization == 'xml':
584
        data = render_to_string('server_stats.xml', stats)
585
    else:
586
        data = json.dumps({'stats': stats})
587

    
588
    return HttpResponse(data, status=200)