Statistics
| Branch: | Tag: | Revision:

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

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

    
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.util.rapi import GanetiApiError
50
from synnefo.logic.backend_allocator import BackendAllocator
51

    
52

    
53
from logging import getLogger
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
    d = {'id': util.construct_nic_id(nic),
111
         'network_id': str(nic.network.id),
112
         'mac_address': nic.mac,
113
         'ipv4': nic.ipv4 if nic.ipv4 else None,
114
         'ipv6': nic.ipv6 if nic.ipv6 else None}
115

    
116
    if nic.firewall_profile:
117
        d['firewallProfile'] = nic.firewall_profile
118
    return d
119

    
120

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

    
133
        metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
134
        if metadata:
135
            d['metadata'] = {'values': metadata}
136

    
137
        attachments = [nic_to_dict(nic) for nic in vm.nics.all()]
138
        if attachments:
139
            d['attachments'] = {'values': attachments}
140
    return d
141

    
142

    
143
def render_server(request, server, status=200):
144
    if request.serialization == 'xml':
145
        data = render_to_string('server.xml', {
146
            'server': server,
147
            'is_root': True})
148
    else:
149
        data = json.dumps({'server': server})
150
    return HttpResponse(data, status=status)
151

    
152

    
153
@util.api_method('GET')
154
def list_servers(request, detail=False):
155
    # Normal Response Codes: 200, 203
156
    # Error Response Codes: computeFault (400, 500),
157
    #                       serviceUnavailable (503),
158
    #                       unauthorized (401),
159
    #                       badRequest (400),
160
    #                       overLimit (413)
161

    
162
    log.debug('list_servers detail=%s', detail)
163
    user_vms = VirtualMachine.objects.filter(userid=request.user_uniq)
164

    
165
    since = util.isoparse(request.GET.get('changes-since'))
166
    if since:
167
        user_vms = user_vms.filter(updated__gte=since)
168
        if not user_vms:
169
            return HttpResponse(status=304)
170
    else:
171
        user_vms = user_vms.filter(deleted=False)
172

    
173
    servers = [vm_to_dict(server, detail) for server in user_vms]
174

    
175
    if request.serialization == 'xml':
176
        data = render_to_string('list_servers.xml', {
177
            'servers': servers,
178
            'detail': detail})
179
    else:
180
        data = json.dumps({'servers': {'values': servers}})
181

    
182
    return HttpResponse(data, status=200)
183

    
184

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

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

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

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

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

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

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

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

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

    
257
    backend_allocator = BackendAllocator()
258
    backend = backend_allocator.allocate(flavor)
259

    
260
    if backend is None:
261
        transaction.rollback()
262
        log.error("No available backends for VM with flavor %s", flavor)
263
        raise Exception
264
    transaction.commit()
265

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

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

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

    
293
    log.info("User %s created VM %s, NIC %s, Backend %s, JobID %s",
294
            request.user_uniq, vm, nic, backend, str(jobID))
295

    
296
    vm.backendjobid = jobID
297
    vm.save()
298

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

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

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

    
312
    return respsone
313

    
314

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

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

    
330

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

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

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

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

    
355
    return HttpResponse(status=204)
356

    
357

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

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

    
375

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

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

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

    
395

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

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

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

    
414
    return HttpResponse(data, status=200)
415

    
416

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

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

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

    
438
    return HttpResponse(data, status=200)
439

    
440

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

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

    
455

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

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

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

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

    
485

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

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

    
502

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

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

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

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

    
537

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

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

    
558

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

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

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

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

    
587
    return HttpResponse(data, status=200)