Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (21.2 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

    
36
from django.conf import settings
37
from django.conf.urls.defaults import patterns
38
from django.db import transaction
39
from django.http import HttpResponse
40
from django.template.loader import render_to_string
41
from django.utils import simplejson as json
42

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

    
53

    
54
from logging import getLogger
55
log = getLogger('synnefo.api')
56

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

    
69

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

    
78

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

    
89

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

    
98

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

    
109

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

    
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

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

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

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

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

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

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

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

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

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

    
259
        backend_allocator = BackendAllocator()
260
        backend = backend_allocator.allocate(request.user_uniq, flavor)
261

    
262
        if backend is None:
263
            log.error("No available backends for VM with flavor %s", flavor)
264
            raise Exception("No available backends")
265
    except:
266
        transaction.rollback()
267
        raise
268
    else:
269
        transaction.commit()
270

    
271
    try:
272
        if settings.PUBLIC_ROUTED_USE_POOL:
273
            (network, address) = util.allocate_public_address(backend)
274
            if address is None:
275
                log.error("Public networks of backend %s are full", backend)
276
                raise faults.OverLimit("Can not allocate IP for new machine."
277
                                       " Public networks are full.")
278
            nic = {'ip': address, 'network': network.backend_id}
279
        else:
280
            network = choice(list(util.backend_public_networks(backend)))
281
            nic = {'ip': 'pool', 'network': network.backend_id}
282
    except:
283
        transaction.rollback()
284
        raise
285
    else:
286
        transaction.commit()
287

    
288
    try:
289
        # We must save the VM instance now, so that it gets a valid
290
        # vm.backend_vm_id.
291
        vm = VirtualMachine.objects.create(
292
            name=name,
293
            backend=backend,
294
            userid=request.user_uniq,
295
            imageid=image_id,
296
            flavor=flavor)
297

    
298
        try:
299
            jobID = create_instance(vm, nic, flavor, image, password, personality)
300
        except GanetiApiError:
301
            vm.delete()
302
            raise
303

    
304
        log.info("User %s created VM %s, NIC %s, Backend %s, JobID %s",
305
                request.user_uniq, vm, nic, backend, str(jobID))
306

    
307
        vm.backendjobid = jobID
308
        vm.save()
309

    
310
        for key, val in metadata.items():
311
            VirtualMachineMetadata.objects.create(
312
                meta_key=key,
313
                meta_value=val,
314
                vm=vm)
315

    
316
        server = vm_to_dict(vm, detail=True)
317
        server['status'] = 'BUILD'
318
        server['adminPass'] = password
319

    
320
        respsone = render_server(request, server, status=202)
321
    except:
322
        transaction.rollback()
323
        raise
324
    else:
325
        transaction.commit()
326

    
327
    return respsone
328

    
329

    
330
@util.api_method('GET')
331
def get_server_details(request, server_id):
332
    # Normal Response Codes: 200, 203
333
    # Error Response Codes: computeFault (400, 500),
334
    #                       serviceUnavailable (503),
335
    #                       unauthorized (401),
336
    #                       badRequest (400),
337
    #                       itemNotFound (404),
338
    #                       overLimit (413)
339

    
340
    log.debug('get_server_details %s', server_id)
341
    vm = util.get_vm(server_id, request.user_uniq)
342
    server = vm_to_dict(vm, detail=True)
343
    return render_server(request, server)
344

    
345

    
346
@util.api_method('PUT')
347
def update_server_name(request, server_id):
348
    # Normal Response Code: 204
349
    # Error Response Codes: computeFault (400, 500),
350
    #                       serviceUnavailable (503),
351
    #                       unauthorized (401),
352
    #                       badRequest (400),
353
    #                       badMediaType(415),
354
    #                       itemNotFound (404),
355
    #                       buildInProgress (409),
356
    #                       overLimit (413)
357

    
358
    req = util.get_request_dict(request)
359
    log.info('update_server_name %s %s', server_id, req)
360

    
361
    try:
362
        name = req['server']['name']
363
    except (TypeError, KeyError):
364
        raise faults.BadRequest("Malformed request")
365

    
366
    vm = util.get_vm(server_id, request.user_uniq)
367
    vm.name = name
368
    vm.save()
369

    
370
    return HttpResponse(status=204)
371

    
372

    
373
@util.api_method('DELETE')
374
@transaction.commit_on_success
375
def delete_server(request, server_id):
376
    # Normal Response Codes: 204
377
    # Error Response Codes: computeFault (400, 500),
378
    #                       serviceUnavailable (503),
379
    #                       unauthorized (401),
380
    #                       itemNotFound (404),
381
    #                       unauthorized (401),
382
    #                       buildInProgress (409),
383
    #                       overLimit (413)
384

    
385
    log.info('delete_server %s', server_id)
386
    vm = util.get_vm(server_id, request.user_uniq)
387
    delete_instance(vm)
388
    return HttpResponse(status=204)
389

    
390

    
391
@util.api_method('POST')
392
def server_action(request, server_id):
393
    req = util.get_request_dict(request)
394
    log.debug('server_action %s %s', server_id, req)
395
    vm = util.get_vm(server_id, request.user_uniq)
396
    if len(req) != 1:
397
        raise faults.BadRequest("Malformed request")
398

    
399
    key = req.keys()[0]
400
    val = req[key]
401

    
402
    try:
403
        assert isinstance(val, dict)
404
        return server_actions[key](request, vm, req[key])
405
    except KeyError:
406
        raise faults.BadRequest("Unknown action")
407
    except AssertionError:
408
        raise faults.BadRequest("Invalid argument")
409

    
410

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

    
420
    log.debug('list_addresses %s', server_id)
421
    vm = util.get_vm(server_id, request.user_uniq)
422
    addresses = [nic_to_dict(nic) for nic in vm.nics.all()]
423

    
424
    if request.serialization == 'xml':
425
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
426
    else:
427
        data = json.dumps({'addresses': {'values': addresses}})
428

    
429
    return HttpResponse(data, status=200)
430

    
431

    
432
@util.api_method('GET')
433
def list_addresses_by_network(request, server_id, network_id):
434
    # Normal Response Codes: 200, 203
435
    # Error Response Codes: computeFault (400, 500),
436
    #                       serviceUnavailable (503),
437
    #                       unauthorized (401),
438
    #                       badRequest (400),
439
    #                       itemNotFound (404),
440
    #                       overLimit (413)
441

    
442
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
443
    machine = util.get_vm(server_id, request.user_uniq)
444
    network = util.get_network(network_id, request.user_uniq)
445
    nic = util.get_nic(machine, network)
446
    address = nic_to_dict(nic)
447

    
448
    if request.serialization == 'xml':
449
        data = render_to_string('address.xml', {'address': address})
450
    else:
451
        data = json.dumps({'network': address})
452

    
453
    return HttpResponse(data, status=200)
454

    
455

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

    
465
    log.debug('list_server_metadata %s', server_id)
466
    vm = util.get_vm(server_id, request.user_uniq)
467
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
468
    return util.render_metadata(request, metadata, use_values=True, status=200)
469

    
470

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

    
482
    req = util.get_request_dict(request)
483
    log.info('update_server_metadata %s %s', server_id, req)
484
    vm = util.get_vm(server_id, request.user_uniq)
485
    try:
486
        metadata = req['metadata']
487
        assert isinstance(metadata, dict)
488
    except (KeyError, AssertionError):
489
        raise faults.BadRequest("Malformed request")
490

    
491
    for key, val in metadata.items():
492
        meta, created = vm.metadata.get_or_create(meta_key=key)
493
        meta.meta_value = val
494
        meta.save()
495

    
496
    vm.save()
497
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
498
    return util.render_metadata(request, vm_meta, status=201)
499

    
500

    
501
@util.api_method('GET')
502
def get_metadata_item(request, server_id, key):
503
    # Normal Response Codes: 200, 203
504
    # Error Response Codes: computeFault (400, 500),
505
    #                       serviceUnavailable (503),
506
    #                       unauthorized (401),
507
    #                       itemNotFound (404),
508
    #                       badRequest (400),
509
    #                       overLimit (413)
510

    
511
    log.debug('get_server_metadata_item %s %s', server_id, key)
512
    vm = util.get_vm(server_id, request.user_uniq)
513
    meta = util.get_vm_meta(vm, key)
514
    d = {meta.meta_key: meta.meta_value}
515
    return util.render_meta(request, d, status=200)
516

    
517

    
518
@util.api_method('PUT')
519
@transaction.commit_on_success
520
def create_metadata_item(request, server_id, key):
521
    # Normal Response Code: 201
522
    # Error Response Codes: computeFault (400, 500),
523
    #                       serviceUnavailable (503),
524
    #                       unauthorized (401),
525
    #                       itemNotFound (404),
526
    #                       badRequest (400),
527
    #                       buildInProgress (409),
528
    #                       badMediaType(415),
529
    #                       overLimit (413)
530

    
531
    req = util.get_request_dict(request)
532
    log.info('create_server_metadata_item %s %s %s', server_id, key, req)
533
    vm = util.get_vm(server_id, request.user_uniq)
534
    try:
535
        metadict = req['meta']
536
        assert isinstance(metadict, dict)
537
        assert len(metadict) == 1
538
        assert key in metadict
539
    except (KeyError, AssertionError):
540
        raise faults.BadRequest("Malformed request")
541

    
542
    meta, created = VirtualMachineMetadata.objects.get_or_create(
543
        meta_key=key,
544
        vm=vm)
545

    
546
    meta.meta_value = metadict[key]
547
    meta.save()
548
    vm.save()
549
    d = {meta.meta_key: meta.meta_value}
550
    return util.render_meta(request, d, status=201)
551

    
552

    
553
@util.api_method('DELETE')
554
@transaction.commit_on_success
555
def delete_metadata_item(request, server_id, key):
556
    # Normal Response Code: 204
557
    # Error Response Codes: computeFault (400, 500),
558
    #                       serviceUnavailable (503),
559
    #                       unauthorized (401),
560
    #                       itemNotFound (404),
561
    #                       badRequest (400),
562
    #                       buildInProgress (409),
563
    #                       badMediaType(415),
564
    #                       overLimit (413),
565

    
566
    log.info('delete_server_metadata_item %s %s', server_id, key)
567
    vm = util.get_vm(server_id, request.user_uniq)
568
    meta = util.get_vm_meta(vm, key)
569
    meta.delete()
570
    vm.save()
571
    return HttpResponse(status=204)
572

    
573

    
574
@util.api_method('GET')
575
def server_stats(request, server_id):
576
    # Normal Response Codes: 200
577
    # Error Response Codes: computeFault (400, 500),
578
    #                       serviceUnavailable (503),
579
    #                       unauthorized (401),
580
    #                       badRequest (400),
581
    #                       itemNotFound (404),
582
    #                       overLimit (413)
583

    
584
    log.debug('server_stats %s', server_id)
585
    vm = util.get_vm(server_id, request.user_uniq)
586
    #secret = util.encrypt(vm.backend_vm_id)
587
    secret = vm.backend_vm_id      # XXX disable backend id encryption
588

    
589
    stats = {
590
        'serverRef': vm.id,
591
        'refresh': settings.STATS_REFRESH_PERIOD,
592
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
593
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
594
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
595
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
596

    
597
    if request.serialization == 'xml':
598
        data = render_to_string('server_stats.xml', stats)
599
    else:
600
        data = json.dumps({'stats': stats})
601

    
602
    return HttpResponse(data, status=200)