Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (24.4 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
from base64 import b64decode
35

    
36
from django import dispatch
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 (VirtualMachine, VirtualMachineMetadata,
48
                               NetworkInterface)
49
from synnefo.logic.backend import create_instance, delete_instance
50
from synnefo.logic.utils import get_rsapi_state
51
from synnefo.logic.rapi import GanetiApiError
52
from synnefo.logic.backend_allocator import BackendAllocator
53
from synnefo import quotas
54

    
55
# server creation signal
56
server_created = dispatch.Signal(providing_args=["created_vm_params"])
57

    
58
from logging import getLogger
59
log = getLogger('synnefo.api')
60

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

    
75

    
76
def demux(request):
77
    if request.method == 'GET':
78
        return list_servers(request)
79
    elif request.method == 'POST':
80
        return create_server(request)
81
    else:
82
        return method_not_allowed(request)
83

    
84

    
85
def server_demux(request, server_id):
86
    if request.method == 'GET':
87
        return get_server_details(request, server_id)
88
    elif request.method == 'PUT':
89
        return update_server_name(request, server_id)
90
    elif request.method == 'DELETE':
91
        return delete_server(request, server_id)
92
    else:
93
        return method_not_allowed(request)
94

    
95

    
96
def metadata_demux(request, server_id):
97
    if request.method == 'GET':
98
        return list_metadata(request, server_id)
99
    elif request.method == 'POST':
100
        return update_metadata(request, server_id)
101
    else:
102
        return method_not_allowed(request)
103

    
104

    
105
def metadata_item_demux(request, server_id, key):
106
    if request.method == 'GET':
107
        return get_metadata_item(request, server_id, key)
108
    elif request.method == 'PUT':
109
        return create_metadata_item(request, server_id, key)
110
    elif request.method == 'DELETE':
111
        return delete_metadata_item(request, server_id, key)
112
    else:
113
        return method_not_allowed(request)
114

    
115

    
116
def nic_to_dict(nic):
117
    d = {'id': util.construct_nic_id(nic),
118
         'network_id': str(nic.network.id),
119
         'mac_address': nic.mac,
120
         'ipv4': nic.ipv4 if nic.ipv4 else None,
121
         'ipv6': nic.ipv6 if nic.ipv6 else None}
122

    
123
    if nic.firewall_profile:
124
        d['firewallProfile'] = nic.firewall_profile
125
    return d
126

    
127

    
128
def vm_to_dict(vm, detail=False):
129
    d = dict(id=vm.id, name=vm.name)
130
    if detail:
131
        d['status'] = get_rsapi_state(vm)
132
        d['progress'] = 100 if get_rsapi_state(vm) == 'ACTIVE' \
133
            else vm.buildpercentage
134
        d['hostId'] = vm.hostid
135
        d['updated'] = util.isoformat(vm.updated)
136
        d['created'] = util.isoformat(vm.created)
137
        d['flavorRef'] = vm.flavor.id
138
        d['imageRef'] = vm.imageid
139
        d['suspended'] = vm.suspended
140

    
141
        metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
142
        if metadata:
143
            d['metadata'] = {'values': metadata}
144

    
145
        vm_nics = vm.nics.filter(state="ACTIVE").order_by("index")
146
        attachments = map(nic_to_dict, vm_nics)
147
        if attachments:
148
            d['attachments'] = {'values': attachments}
149

    
150
        # include the latest vm diagnostic, if set
151
        diagnostic = vm.get_last_diagnostic()
152
        if diagnostic:
153
            d['diagnostics'] = diagnostics_to_dict([diagnostic])
154

    
155
    return d
156

    
157

    
158
def diagnostics_to_dict(diagnostics):
159
    """
160
    Extract api data from diagnostics QuerySet.
161
    """
162
    entries = list()
163

    
164
    for diagnostic in diagnostics:
165
        # format source date if set
166
        formatted_source_date = None
167
        if diagnostic.source_date:
168
            formatted_source_date = util.isoformat(diagnostic.source_date)
169

    
170
        entry = {
171
            'source': diagnostic.source,
172
            'created': util.isoformat(diagnostic.created),
173
            'message': diagnostic.message,
174
            'details': diagnostic.details,
175
            'level': diagnostic.level,
176
        }
177

    
178
        if formatted_source_date:
179
            entry['source_date'] = formatted_source_date
180

    
181
        entries.append(entry)
182

    
183
    return entries
184

    
185

    
186
def render_server(request, server, status=200):
187
    if request.serialization == 'xml':
188
        data = render_to_string('server.xml', {
189
            'server': server,
190
            'is_root': True})
191
    else:
192
        data = json.dumps({'server': server})
193
    return HttpResponse(data, status=status)
194

    
195

    
196
def render_diagnostics(request, diagnostics_dict, status=200):
197
    """
198
    Render diagnostics dictionary to json response.
199
    """
200
    return HttpResponse(json.dumps(diagnostics_dict), status=status)
201

    
202

    
203
@util.api_method('GET')
204
def get_server_diagnostics(request, server_id):
205
    """
206
    Virtual machine diagnostics api view.
207
    """
208
    log.debug('server_diagnostics %s', server_id)
209
    vm = util.get_vm(server_id, request.user_uniq)
210
    diagnostics = diagnostics_to_dict(vm.diagnostics.all())
211
    return render_diagnostics(request, diagnostics)
212

    
213

    
214
@util.api_method('GET')
215
def list_servers(request, detail=False):
216
    # Normal Response Codes: 200, 203
217
    # Error Response Codes: computeFault (400, 500),
218
    #                       serviceUnavailable (503),
219
    #                       unauthorized (401),
220
    #                       badRequest (400),
221
    #                       overLimit (413)
222

    
223
    log.debug('list_servers detail=%s', detail)
224
    user_vms = VirtualMachine.objects.filter(userid=request.user_uniq)
225

    
226
    since = util.isoparse(request.GET.get('changes-since'))
227

    
228
    if since:
229
        user_vms = user_vms.filter(updated__gte=since)
230
        if not user_vms:
231
            return HttpResponse(status=304)
232
    else:
233
        user_vms = user_vms.filter(deleted=False)
234

    
235
    servers = [vm_to_dict(server, detail)
236
               for server in user_vms.order_by('id')]
237

    
238
    if request.serialization == 'xml':
239
        data = render_to_string('list_servers.xml', {
240
            'servers': servers,
241
            'detail': detail})
242
    else:
243
        data = json.dumps({'servers': {'values': servers}})
244

    
245
    return HttpResponse(data, status=200)
246

    
247

    
248
@util.api_method('POST')
249
# Use manual transactions. Backend and IP pool allocations need exclusive
250
# access (SELECT..FOR UPDATE). Running create_server with commit_on_success
251
# would result in backends and public networks to be locked until the job is
252
# sent to the Ganeti backend.
253
@quotas.uses_commission
254
@transaction.commit_manually
255
def create_server(serials, request):
256
    # Normal Response Code: 202
257
    # Error Response Codes: computeFault (400, 500),
258
    #                       serviceUnavailable (503),
259
    #                       unauthorized (401),
260
    #                       badMediaType(415),
261
    #                       itemNotFound (404),
262
    #                       badRequest (400),
263
    #                       serverCapacityUnavailable (503),
264
    #                       overLimit (413)
265
    try:
266
        req = util.get_request_dict(request)
267
        log.info('create_server %s', req)
268
        user_id = request.user_uniq
269

    
270
        try:
271
            server = req['server']
272
            name = server['name']
273
            metadata = server.get('metadata', {})
274
            assert isinstance(metadata, dict)
275
            image_id = server['imageRef']
276
            flavor_id = server['flavorRef']
277
            personality = server.get('personality', [])
278
            assert isinstance(personality, list)
279
        except (KeyError, AssertionError):
280
            raise faults.BadRequest("Malformed request")
281

    
282
        # Verify that personalities are well-formed
283
        util.verify_personality(personality)
284
        # Get image information
285
        image = util.get_image_dict(image_id, user_id)
286
        # Get flavor (ensure it is active)
287
        flavor = util.get_flavor(flavor_id, include_deleted=False)
288
        # Allocate VM to backend
289
        backend_allocator = BackendAllocator()
290
        backend = backend_allocator.allocate(request.user_uniq, flavor)
291

    
292
        if backend is None:
293
            log.error("No available backends for VM with flavor %s", flavor)
294
            raise faults.ServiceUnavailable("No available backends")
295
    except:
296
        transaction.rollback()
297
        raise
298
    else:
299
        transaction.commit()
300

    
301
    # Allocate IP from public network
302
    try:
303
        (network, address) = util.get_public_ip(backend)
304
        nic = {'ip': address, 'network': network.backend_id}
305
    except:
306
        transaction.rollback()
307
        raise
308
    else:
309
        transaction.commit()
310

    
311
    # Fix flavor for archipelago
312
    password = util.random_password()
313
    disk_template, provider = util.get_flavor_provider(flavor)
314
    if provider:
315
        flavor.disk_template = disk_template
316
        flavor.disk_provider = provider
317
        flavor.disk_origin = None
318
        if provider == 'vlmc':
319
            flavor.disk_origin = image['checksum']
320
            image['backend_id'] = 'null'
321
    else:
322
        flavor.disk_provider = None
323

    
324
    try:
325
        # Issue commission
326
        serial = quotas.issue_vm_commission(user_id, flavor)
327
        serials.append(serial)
328
        # Make the commission accepted, since in the end of this
329
        # transaction the VM will have been created in the DB.
330
        serial.accepted = True
331
        serial.save()
332

    
333
        # We must save the VM instance now, so that it gets a valid
334
        # vm.backend_vm_id.
335
        vm = VirtualMachine.objects.create(
336
            name=name,
337
            backend=backend,
338
            userid=user_id,
339
            imageid=image_id,
340
            flavor=flavor,
341
            action="CREATE",
342
            serial=serial)
343

    
344
        # Create VM's public NIC. Do not wait notification form ganeti hooks to
345
        # create this NIC, because if the hooks never run (e.g. building error)
346
        # the VM's public IP address will never be released!
347
        NetworkInterface.objects.create(machine=vm, network=network, index=0,
348
                                        ipv4=address, state="Buidling")
349

    
350
        log.info("Created entry in DB for VM '%s'", vm)
351

    
352
        # dispatch server created signal
353
        server_created.send(sender=vm, created_vm_params={
354
            'img_id': image['backend_id'],
355
            'img_passwd': password,
356
            'img_format': str(image['format']),
357
            'img_personality': json.dumps(personality),
358
            'img_properties': json.dumps(image['metadata']),
359
        })
360

    
361
        # Also we must create the VM metadata in the same transaction.
362
        for key, val in metadata.items():
363
            VirtualMachineMetadata.objects.create(
364
                meta_key=key,
365
                meta_value=val,
366
                vm=vm)
367
    except:
368
        transaction.rollback()
369
        raise
370
    else:
371
        transaction.commit()
372

    
373
    try:
374
        jobID = create_instance(vm, nic, flavor, image)
375
        # At this point the job is enqueued in the Ganeti backend
376
        vm.backendjobid = jobID
377
        vm.save()
378
        transaction.commit()
379
        log.info("User %s created VM %s, NIC %s, Backend %s, JobID %s",
380
                 user_id, vm, nic, backend, str(jobID))
381
    except GanetiApiError as e:
382
        log.exception("Can not communicate to backend %s: %s. Deleting VM %s",
383
                      backend, e, vm)
384
        vm.delete()
385
        transaction.commit()
386
        raise
387
    except:
388
        transaction.rollback()
389
        raise
390

    
391
    server = vm_to_dict(vm, detail=True)
392
    server['status'] = 'BUILD'
393
    server['adminPass'] = password
394

    
395
    response = render_server(request, server, status=202)
396

    
397
    return response
398

    
399

    
400
@util.api_method('GET')
401
def get_server_details(request, server_id):
402
    # Normal Response Codes: 200, 203
403
    # Error Response Codes: computeFault (400, 500),
404
    #                       serviceUnavailable (503),
405
    #                       unauthorized (401),
406
    #                       badRequest (400),
407
    #                       itemNotFound (404),
408
    #                       overLimit (413)
409

    
410
    log.debug('get_server_details %s', server_id)
411
    vm = util.get_vm(server_id, request.user_uniq)
412
    server = vm_to_dict(vm, detail=True)
413
    return render_server(request, server)
414

    
415

    
416
@util.api_method('PUT')
417
def update_server_name(request, server_id):
418
    # Normal Response Code: 204
419
    # Error Response Codes: computeFault (400, 500),
420
    #                       serviceUnavailable (503),
421
    #                       unauthorized (401),
422
    #                       badRequest (400),
423
    #                       badMediaType(415),
424
    #                       itemNotFound (404),
425
    #                       buildInProgress (409),
426
    #                       overLimit (413)
427

    
428
    req = util.get_request_dict(request)
429
    log.info('update_server_name %s %s', server_id, req)
430

    
431
    try:
432
        name = req['server']['name']
433
    except (TypeError, KeyError):
434
        raise faults.BadRequest("Malformed request")
435

    
436
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
437
                     non_suspended=True)
438
    vm.name = name
439
    vm.save()
440

    
441
    return HttpResponse(status=204)
442

    
443

    
444
@util.api_method('DELETE')
445
@transaction.commit_on_success
446
def delete_server(request, server_id):
447
    # Normal Response Codes: 204
448
    # Error Response Codes: computeFault (400, 500),
449
    #                       serviceUnavailable (503),
450
    #                       unauthorized (401),
451
    #                       itemNotFound (404),
452
    #                       unauthorized (401),
453
    #                       buildInProgress (409),
454
    #                       overLimit (413)
455

    
456
    log.info('delete_server %s', server_id)
457
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
458
                     non_suspended=True)
459
    start_action(vm, 'DESTROY')
460
    delete_instance(vm)
461
    return HttpResponse(status=204)
462

    
463

    
464
# additional server actions
465
ARBITRARY_ACTIONS = ['console', 'firewallProfile']
466

    
467

    
468
@util.api_method('POST')
469
def server_action(request, server_id):
470
    req = util.get_request_dict(request)
471
    log.debug('server_action %s %s', server_id, req)
472

    
473
    if len(req) != 1:
474
        raise faults.BadRequest("Malformed request")
475

    
476
    # Do not allow any action on deleted or suspended VMs
477
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
478
                     non_deleted=True, non_suspended=True)
479

    
480
    try:
481
        key = req.keys()[0]
482
        if key not in ARBITRARY_ACTIONS:
483
            start_action(vm, key_to_action(key))
484
        val = req[key]
485
        assert isinstance(val, dict)
486
        return server_actions[key](request, vm, val)
487
    except KeyError:
488
        raise faults.BadRequest("Unknown action")
489
    except AssertionError:
490
        raise faults.BadRequest("Invalid argument")
491

    
492

    
493
def key_to_action(key):
494
    """Map HTTP request key to a VM Action"""
495
    if key == "shutdown":
496
        return "STOP"
497
    if key == "delete":
498
        return "DESTROY"
499
    if key in ARBITRARY_ACTIONS:
500
        return None
501
    else:
502
        return key.upper()
503

    
504

    
505
def start_action(vm, action):
506
    log.debug("Applying action %s to VM %s", action, vm)
507
    if not action:
508
        return
509

    
510
    if not action in [x[0] for x in VirtualMachine.ACTIONS]:
511
        raise faults.ServiceUnavailable("Action %s not supported" % action)
512

    
513
    # No actions to deleted VMs
514
    if vm.deleted:
515
        raise VirtualMachine.DeletedError
516

    
517
    # No actions to machines being built. They may be destroyed, however.
518
    if vm.operstate == 'BUILD' and action != 'DESTROY':
519
        raise VirtualMachine.BuildingError
520

    
521
    vm.action = action
522
    vm.backendjobid = None
523
    vm.backendopcode = None
524
    vm.backendjobstatus = None
525
    vm.backendlogmsg = None
526

    
527
    vm.save()
528

    
529

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

    
539
    log.debug('list_addresses %s', server_id)
540
    vm = util.get_vm(server_id, request.user_uniq)
541
    addresses = [nic_to_dict(nic) for nic in vm.nics.all()]
542

    
543
    if request.serialization == 'xml':
544
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
545
    else:
546
        data = json.dumps({'addresses': {'values': addresses}})
547

    
548
    return HttpResponse(data, status=200)
549

    
550

    
551
@util.api_method('GET')
552
def list_addresses_by_network(request, server_id, network_id):
553
    # Normal Response Codes: 200, 203
554
    # Error Response Codes: computeFault (400, 500),
555
    #                       serviceUnavailable (503),
556
    #                       unauthorized (401),
557
    #                       badRequest (400),
558
    #                       itemNotFound (404),
559
    #                       overLimit (413)
560

    
561
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
562
    machine = util.get_vm(server_id, request.user_uniq)
563
    network = util.get_network(network_id, request.user_uniq)
564
    nic = util.get_nic(machine, network)
565
    address = nic_to_dict(nic)
566

    
567
    if request.serialization == 'xml':
568
        data = render_to_string('address.xml', {'address': address})
569
    else:
570
        data = json.dumps({'network': address})
571

    
572
    return HttpResponse(data, status=200)
573

    
574

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

    
584
    log.debug('list_server_metadata %s', server_id)
585
    vm = util.get_vm(server_id, request.user_uniq)
586
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
587
    return util.render_metadata(request, metadata, use_values=True, status=200)
588

    
589

    
590
@util.api_method('POST')
591
def update_metadata(request, server_id):
592
    # Normal Response Code: 201
593
    # Error Response Codes: computeFault (400, 500),
594
    #                       serviceUnavailable (503),
595
    #                       unauthorized (401),
596
    #                       badRequest (400),
597
    #                       buildInProgress (409),
598
    #                       badMediaType(415),
599
    #                       overLimit (413)
600

    
601
    req = util.get_request_dict(request)
602
    log.info('update_server_metadata %s %s', server_id, req)
603
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
604
    try:
605
        metadata = req['metadata']
606
        assert isinstance(metadata, dict)
607
    except (KeyError, AssertionError):
608
        raise faults.BadRequest("Malformed request")
609

    
610
    for key, val in metadata.items():
611
        meta, created = vm.metadata.get_or_create(meta_key=key)
612
        meta.meta_value = val
613
        meta.save()
614

    
615
    vm.save()
616
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
617
    return util.render_metadata(request, vm_meta, status=201)
618

    
619

    
620
@util.api_method('GET')
621
def get_metadata_item(request, server_id, key):
622
    # Normal Response Codes: 200, 203
623
    # Error Response Codes: computeFault (400, 500),
624
    #                       serviceUnavailable (503),
625
    #                       unauthorized (401),
626
    #                       itemNotFound (404),
627
    #                       badRequest (400),
628
    #                       overLimit (413)
629

    
630
    log.debug('get_server_metadata_item %s %s', server_id, key)
631
    vm = util.get_vm(server_id, request.user_uniq)
632
    meta = util.get_vm_meta(vm, key)
633
    d = {meta.meta_key: meta.meta_value}
634
    return util.render_meta(request, d, status=200)
635

    
636

    
637
@util.api_method('PUT')
638
@transaction.commit_on_success
639
def create_metadata_item(request, server_id, key):
640
    # Normal Response Code: 201
641
    # Error Response Codes: computeFault (400, 500),
642
    #                       serviceUnavailable (503),
643
    #                       unauthorized (401),
644
    #                       itemNotFound (404),
645
    #                       badRequest (400),
646
    #                       buildInProgress (409),
647
    #                       badMediaType(415),
648
    #                       overLimit (413)
649

    
650
    req = util.get_request_dict(request)
651
    log.info('create_server_metadata_item %s %s %s', server_id, key, req)
652
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
653
    try:
654
        metadict = req['meta']
655
        assert isinstance(metadict, dict)
656
        assert len(metadict) == 1
657
        assert key in metadict
658
    except (KeyError, AssertionError):
659
        raise faults.BadRequest("Malformed request")
660

    
661
    meta, created = VirtualMachineMetadata.objects.get_or_create(
662
        meta_key=key,
663
        vm=vm)
664

    
665
    meta.meta_value = metadict[key]
666
    meta.save()
667
    vm.save()
668
    d = {meta.meta_key: meta.meta_value}
669
    return util.render_meta(request, d, status=201)
670

    
671

    
672
@util.api_method('DELETE')
673
@transaction.commit_on_success
674
def delete_metadata_item(request, server_id, key):
675
    # Normal Response Code: 204
676
    # Error Response Codes: computeFault (400, 500),
677
    #                       serviceUnavailable (503),
678
    #                       unauthorized (401),
679
    #                       itemNotFound (404),
680
    #                       badRequest (400),
681
    #                       buildInProgress (409),
682
    #                       badMediaType(415),
683
    #                       overLimit (413),
684

    
685
    log.info('delete_server_metadata_item %s %s', server_id, key)
686
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
687
    meta = util.get_vm_meta(vm, key)
688
    meta.delete()
689
    vm.save()
690
    return HttpResponse(status=204)
691

    
692

    
693
@util.api_method('GET')
694
def server_stats(request, server_id):
695
    # Normal Response Codes: 200
696
    # Error Response Codes: computeFault (400, 500),
697
    #                       serviceUnavailable (503),
698
    #                       unauthorized (401),
699
    #                       badRequest (400),
700
    #                       itemNotFound (404),
701
    #                       overLimit (413)
702

    
703
    log.debug('server_stats %s', server_id)
704
    vm = util.get_vm(server_id, request.user_uniq)
705
    #secret = util.encrypt(vm.backend_vm_id)
706
    secret = vm.backend_vm_id      # XXX disable backend id encryption
707

    
708
    stats = {
709
        'serverRef': vm.id,
710
        'refresh': settings.STATS_REFRESH_PERIOD,
711
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
712
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
713
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
714
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
715

    
716
    if request.serialization == 'xml':
717
        data = render_to_string('server_stats.xml', stats)
718
    else:
719
        data = json.dumps({'stats': stats})
720

    
721
    return HttpResponse(data, status=200)