Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (25 kB)

1
# Copyright 2011-2013 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 django import dispatch
35
from django.conf import settings
36
from django.conf.urls.defaults import patterns
37
from django.db import transaction
38
from django.http import HttpResponse
39
from django.template.loader import render_to_string
40
from django.utils import simplejson as json
41

    
42
from snf_django.lib import api
43
from snf_django.lib.api import faults, utils
44
from synnefo.api import util
45
from synnefo.api.actions import server_actions
46
from synnefo.db.models import (VirtualMachine, VirtualMachineMetadata,
47
                               NetworkInterface)
48
from synnefo.logic.backend import create_instance, delete_instance
49
from synnefo.logic.utils import get_rsapi_state
50
from synnefo.logic.rapi import GanetiApiError
51
from synnefo.logic.backend_allocator import BackendAllocator
52
from synnefo import quotas
53

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

    
57
from logging import getLogger
58
log = getLogger(__name__)
59

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

    
74

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

    
83

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

    
94

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

    
103

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

    
114

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

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

    
126

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

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

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

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

    
154
    return d
155

    
156

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

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

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

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

    
180
        entries.append(entry)
181

    
182
    return entries
183

    
184

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

    
194

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

    
201

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

    
212

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

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

    
225
    since = utils.isoparse(request.GET.get('changes-since'))
226

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

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

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

    
244
    return HttpResponse(data, status=200)
245

    
246

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

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

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

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

    
300
    # Fix flavor for archipelago
301
    password = util.random_password()
302
    disk_template, provider = util.get_flavor_provider(flavor)
303
    if provider:
304
        flavor.disk_template = disk_template
305
        flavor.disk_provider = provider
306
        flavor.disk_origin = None
307
        if provider == 'vlmc':
308
            flavor.disk_origin = image['checksum']
309
            image['backend_id'] = 'null'
310
    else:
311
        flavor.disk_provider = None
312

    
313
    try:
314
        # Issue commission
315
        serial = quotas.issue_vm_commission(user_id, flavor)
316
        serials.append(serial)
317
        # Make the commission accepted, since in the end of this
318
        # transaction the VM will have been created in the DB.
319
        serial.accepted = True
320
        serial.save()
321

    
322
        # Allocate IP from public network
323
        (network, address) = util.get_public_ip(backend)
324
        nic = {'ip': address, 'network': network.backend_id}
325

    
326
        # We must save the VM instance now, so that it gets a valid
327
        # vm.backend_vm_id.
328
        vm = VirtualMachine.objects.create(
329
            name=name,
330
            backend=backend,
331
            userid=user_id,
332
            imageid=image_id,
333
            flavor=flavor,
334
            action="CREATE",
335
            serial=serial)
336

    
337
        # Create VM's public NIC. Do not wait notification form ganeti hooks to
338
        # create this NIC, because if the hooks never run (e.g. building error)
339
        # the VM's public IP address will never be released!
340
        NetworkInterface.objects.create(machine=vm, network=network, index=0,
341
                                        ipv4=address, state="BUILDING")
342

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

    
345
        # dispatch server created signal
346
        server_created.send(sender=vm, created_vm_params={
347
            'img_id': image['backend_id'],
348
            'img_passwd': password,
349
            'img_format': str(image['format']),
350
            'img_personality': json.dumps(personality),
351
            'img_properties': json.dumps(image['metadata']),
352
        })
353

    
354
        # Also we must create the VM metadata in the same transaction.
355
        for key, val in metadata.items():
356
            VirtualMachineMetadata.objects.create(
357
                meta_key=key,
358
                meta_value=val,
359
                vm=vm)
360
    except:
361
        transaction.rollback()
362
        raise
363
    else:
364
        transaction.commit()
365

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

    
384
    server = vm_to_dict(vm, detail=True)
385
    server['status'] = 'BUILD'
386
    server['adminPass'] = password
387

    
388
    respsone = render_server(request, server, status=202)
389

    
390
    return respsone
391

    
392

    
393
@api.api_method(http_method='GET', user_required=True, logger=log)
394
def get_server_details(request, server_id):
395
    # Normal Response Codes: 200, 203
396
    # Error Response Codes: computeFault (400, 500),
397
    #                       serviceUnavailable (503),
398
    #                       unauthorized (401),
399
    #                       badRequest (400),
400
    #                       itemNotFound (404),
401
    #                       overLimit (413)
402

    
403
    log.debug('get_server_details %s', server_id)
404
    vm = util.get_vm(server_id, request.user_uniq)
405
    server = vm_to_dict(vm, detail=True)
406
    return render_server(request, server)
407

    
408

    
409
@api.api_method(http_method='PUT', user_required=True, logger=log)
410
def update_server_name(request, server_id):
411
    # Normal Response Code: 204
412
    # Error Response Codes: computeFault (400, 500),
413
    #                       serviceUnavailable (503),
414
    #                       unauthorized (401),
415
    #                       badRequest (400),
416
    #                       badMediaType(415),
417
    #                       itemNotFound (404),
418
    #                       buildInProgress (409),
419
    #                       overLimit (413)
420

    
421
    req = utils.get_request_dict(request)
422
    log.info('update_server_name %s %s', server_id, req)
423

    
424
    try:
425
        name = req['server']['name']
426
    except (TypeError, KeyError):
427
        raise faults.BadRequest("Malformed request")
428

    
429
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
430
                     non_suspended=True)
431
    vm.name = name
432
    vm.save()
433

    
434
    return HttpResponse(status=204)
435

    
436

    
437
@api.api_method(http_method='DELETE', user_required=True, logger=log)
438
@transaction.commit_on_success
439
def delete_server(request, server_id):
440
    # Normal Response Codes: 204
441
    # Error Response Codes: computeFault (400, 500),
442
    #                       serviceUnavailable (503),
443
    #                       unauthorized (401),
444
    #                       itemNotFound (404),
445
    #                       unauthorized (401),
446
    #                       buildInProgress (409),
447
    #                       overLimit (413)
448

    
449
    log.info('delete_server %s', server_id)
450
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
451
                     non_suspended=True)
452
    start_action(vm, 'DESTROY')
453
    delete_instance(vm)
454
    return HttpResponse(status=204)
455

    
456

    
457
# additional server actions
458
ARBITRARY_ACTIONS = ['console', 'firewallProfile']
459

    
460

    
461
@api.api_method(http_method='POST', user_required=True, logger=log)
462
def server_action(request, server_id):
463
    req = utils.get_request_dict(request)
464
    log.debug('server_action %s %s', server_id, req)
465

    
466
    if len(req) != 1:
467
        raise faults.BadRequest("Malformed request")
468

    
469
    # Do not allow any action on deleted or suspended VMs
470
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
471
                     non_deleted=True, non_suspended=True)
472

    
473
    try:
474
        key = req.keys()[0]
475
        if key not in ARBITRARY_ACTIONS:
476
            start_action(vm, key_to_action(key))
477
        val = req[key]
478
        assert isinstance(val, dict)
479
        return server_actions[key](request, vm, val)
480
    except KeyError:
481
        raise faults.BadRequest("Unknown action")
482
    except AssertionError:
483
        raise faults.BadRequest("Invalid argument")
484

    
485

    
486
def key_to_action(key):
487
    """Map HTTP request key to a VM Action"""
488
    if key == "shutdown":
489
        return "STOP"
490
    if key == "delete":
491
        return "DESTROY"
492
    if key in ARBITRARY_ACTIONS:
493
        return None
494
    else:
495
        return key.upper()
496

    
497

    
498
def start_action(vm, action):
499
    log.debug("Applying action %s to VM %s", action, vm)
500
    if not action:
501
        return
502

    
503
    if not action in [x[0] for x in VirtualMachine.ACTIONS]:
504
        raise faults.ServiceUnavailable("Action %s not supported" % action)
505

    
506
    # No actions to deleted VMs
507
    if vm.deleted:
508
        raise faults.BadRequest("VirtualMachine has been deleted.")
509

    
510
    # No actions to machines being built. They may be destroyed, however.
511
    if vm.operstate == 'BUILD' and action != 'DESTROY':
512
        raise faults.BuildInProgress("Server is being build.")
513

    
514
    vm.action = action
515
    vm.backendjobid = None
516
    vm.backendopcode = None
517
    vm.backendjobstatus = None
518
    vm.backendlogmsg = None
519

    
520
    vm.save()
521

    
522

    
523
@api.api_method(http_method='GET', user_required=True, logger=log)
524
def list_addresses(request, server_id):
525
    # Normal Response Codes: 200, 203
526
    # Error Response Codes: computeFault (400, 500),
527
    #                       serviceUnavailable (503),
528
    #                       unauthorized (401),
529
    #                       badRequest (400),
530
    #                       overLimit (413)
531

    
532
    log.debug('list_addresses %s', server_id)
533
    vm = util.get_vm(server_id, request.user_uniq)
534
    addresses = [nic_to_dict(nic) for nic in vm.nics.all()]
535

    
536
    if request.serialization == 'xml':
537
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
538
    else:
539
        data = json.dumps({'addresses': {'values': addresses}})
540

    
541
    return HttpResponse(data, status=200)
542

    
543

    
544
@api.api_method(http_method='GET', user_required=True, logger=log)
545
def list_addresses_by_network(request, server_id, network_id):
546
    # Normal Response Codes: 200, 203
547
    # Error Response Codes: computeFault (400, 500),
548
    #                       serviceUnavailable (503),
549
    #                       unauthorized (401),
550
    #                       badRequest (400),
551
    #                       itemNotFound (404),
552
    #                       overLimit (413)
553

    
554
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
555
    machine = util.get_vm(server_id, request.user_uniq)
556
    network = util.get_network(network_id, request.user_uniq)
557
    nic = util.get_nic(machine, network)
558
    address = nic_to_dict(nic)
559

    
560
    if request.serialization == 'xml':
561
        data = render_to_string('address.xml', {'address': address})
562
    else:
563
        data = json.dumps({'network': address})
564

    
565
    return HttpResponse(data, status=200)
566

    
567

    
568
@api.api_method(http_method='GET', user_required=True, logger=log)
569
def list_metadata(request, server_id):
570
    # Normal Response Codes: 200, 203
571
    # Error Response Codes: computeFault (400, 500),
572
    #                       serviceUnavailable (503),
573
    #                       unauthorized (401),
574
    #                       badRequest (400),
575
    #                       overLimit (413)
576

    
577
    log.debug('list_server_metadata %s', server_id)
578
    vm = util.get_vm(server_id, request.user_uniq)
579
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
580
    return util.render_metadata(request, metadata, use_values=True, status=200)
581

    
582

    
583
@api.api_method(http_method='POST', user_required=True, logger=log)
584
def update_metadata(request, server_id):
585
    # Normal Response Code: 201
586
    # Error Response Codes: computeFault (400, 500),
587
    #                       serviceUnavailable (503),
588
    #                       unauthorized (401),
589
    #                       badRequest (400),
590
    #                       buildInProgress (409),
591
    #                       badMediaType(415),
592
    #                       overLimit (413)
593

    
594
    req = utils.get_request_dict(request)
595
    log.info('update_server_metadata %s %s', server_id, req)
596
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
597
    try:
598
        metadata = req['metadata']
599
        assert isinstance(metadata, dict)
600
    except (KeyError, AssertionError):
601
        raise faults.BadRequest("Malformed request")
602

    
603
    for key, val in metadata.items():
604
        meta, created = vm.metadata.get_or_create(meta_key=key)
605
        meta.meta_value = val
606
        meta.save()
607

    
608
    vm.save()
609
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
610
    return util.render_metadata(request, vm_meta, status=201)
611

    
612

    
613
@api.api_method(http_method='GET', user_required=True, logger=log)
614
def get_metadata_item(request, server_id, key):
615
    # Normal Response Codes: 200, 203
616
    # Error Response Codes: computeFault (400, 500),
617
    #                       serviceUnavailable (503),
618
    #                       unauthorized (401),
619
    #                       itemNotFound (404),
620
    #                       badRequest (400),
621
    #                       overLimit (413)
622

    
623
    log.debug('get_server_metadata_item %s %s', server_id, key)
624
    vm = util.get_vm(server_id, request.user_uniq)
625
    meta = util.get_vm_meta(vm, key)
626
    d = {meta.meta_key: meta.meta_value}
627
    return util.render_meta(request, d, status=200)
628

    
629

    
630
@api.api_method(http_method='PUT', user_required=True, logger=log)
631
@transaction.commit_on_success
632
def create_metadata_item(request, server_id, key):
633
    # Normal Response Code: 201
634
    # Error Response Codes: computeFault (400, 500),
635
    #                       serviceUnavailable (503),
636
    #                       unauthorized (401),
637
    #                       itemNotFound (404),
638
    #                       badRequest (400),
639
    #                       buildInProgress (409),
640
    #                       badMediaType(415),
641
    #                       overLimit (413)
642

    
643
    req = utils.get_request_dict(request)
644
    log.info('create_server_metadata_item %s %s %s', server_id, key, req)
645
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
646
    try:
647
        metadict = req['meta']
648
        assert isinstance(metadict, dict)
649
        assert len(metadict) == 1
650
        assert key in metadict
651
    except (KeyError, AssertionError):
652
        raise faults.BadRequest("Malformed request")
653

    
654
    meta, created = VirtualMachineMetadata.objects.get_or_create(
655
        meta_key=key,
656
        vm=vm)
657

    
658
    meta.meta_value = metadict[key]
659
    meta.save()
660
    vm.save()
661
    d = {meta.meta_key: meta.meta_value}
662
    return util.render_meta(request, d, status=201)
663

    
664

    
665
@api.api_method(http_method='DELETE', user_required=True, logger=log)
666
@transaction.commit_on_success
667
def delete_metadata_item(request, server_id, key):
668
    # Normal Response Code: 204
669
    # Error Response Codes: computeFault (400, 500),
670
    #                       serviceUnavailable (503),
671
    #                       unauthorized (401),
672
    #                       itemNotFound (404),
673
    #                       badRequest (400),
674
    #                       buildInProgress (409),
675
    #                       badMediaType(415),
676
    #                       overLimit (413),
677

    
678
    log.info('delete_server_metadata_item %s %s', server_id, key)
679
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
680
    meta = util.get_vm_meta(vm, key)
681
    meta.delete()
682
    vm.save()
683
    return HttpResponse(status=204)
684

    
685

    
686
@api.api_method(http_method='GET', user_required=True, logger=log)
687
def server_stats(request, server_id):
688
    # Normal Response Codes: 200
689
    # Error Response Codes: computeFault (400, 500),
690
    #                       serviceUnavailable (503),
691
    #                       unauthorized (401),
692
    #                       badRequest (400),
693
    #                       itemNotFound (404),
694
    #                       overLimit (413)
695

    
696
    log.debug('server_stats %s', server_id)
697
    vm = util.get_vm(server_id, request.user_uniq)
698
    #secret = util.encrypt(vm.backend_vm_id)
699
    secret = vm.backend_vm_id      # XXX disable backend id encryption
700

    
701
    stats = {
702
        'serverRef': vm.id,
703
        'refresh': settings.STATS_REFRESH_PERIOD,
704
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
705
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
706
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
707
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
708

    
709
    if request.serialization == 'xml':
710
        data = render_to_string('server_stats.xml', stats)
711
    else:
712
        data = json.dumps({'stats': stats})
713

    
714
    return HttpResponse(data, status=200)