Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (25.2 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
@transaction.commit_manually
253
def create_server(request):
254
    # Normal Response Code: 202
255
    # Error Response Codes: computeFault (400, 500),
256
    #                       serviceUnavailable (503),
257
    #                       unauthorized (401),
258
    #                       badMediaType(415),
259
    #                       itemNotFound (404),
260
    #                       badRequest (400),
261
    #                       serverCapacityUnavailable (503),
262
    #                       overLimit (413)
263
    try:
264
        req = utils.get_request_dict(request)
265
        log.info('create_server %s', req)
266
        user_id = request.user_uniq
267

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

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

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

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

    
312
    try:
313
        # Allocate IP from public network
314
        (network, address) = util.get_public_ip(backend)
315
        nic = {'ip': address, 'network': network.backend_id}
316

    
317
        # We must save the VM instance now, so that it gets a valid
318
        # vm.backend_vm_id.
319
        vm = VirtualMachine.objects.create(
320
            name=name,
321
            backend=backend,
322
            userid=user_id,
323
            imageid=image_id,
324
            flavor=flavor,
325
            action="CREATE")
326

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

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

    
335
        # dispatch server created signal
336
        server_created.send(sender=vm, created_vm_params={
337
            'img_id': image['backend_id'],
338
            'img_passwd': password,
339
            'img_format': str(image['format']),
340
            'img_personality': json.dumps(personality),
341
            'img_properties': json.dumps(image['metadata']),
342
        })
343

    
344
        # Also we must create the VM metadata in the same transaction.
345
        for key, val in metadata.items():
346
            VirtualMachineMetadata.objects.create(
347
                meta_key=key,
348
                meta_value=val,
349
                vm=vm)
350
        # Issue commission to Quotaholder and accept it since at the end of
351
        # this transaction the VirtualMachine object will be created in the DB.
352
        # Note: the following call does a commit!
353
        quotas.issue_and_accept_commission(vm)
354
    except:
355
        transaction.rollback()
356
        raise
357
    else:
358
        transaction.commit()
359

    
360
    try:
361
        jobID = create_instance(vm, nic, flavor, image)
362
        # At this point the job is enqueued in the Ganeti backend
363
        vm.backendjobid = jobID
364
        vm.save()
365
        transaction.commit()
366
        log.info("User %s created VM %s, NIC %s, Backend %s, JobID %s",
367
                 user_id, vm, nic, backend, str(jobID))
368
    except GanetiApiError as e:
369
        log.exception("Can not communicate to backend %s: %s.",
370
                      backend, e)
371
        # Failed while enqueuing OP_INSTANCE_CREATE to backend. Restore
372
        # already reserved quotas by issuing a negative commission
373
        vm.operstate = "ERROR"
374
        vm.backendlogmsg = "Can not communicate to backend."
375
        vm.deleted = True
376
        vm.save()
377
        quotas.issue_and_accept_commission(vm, delete=True)
378
        raise
379
    except:
380
        transaction.rollback()
381
        raise
382

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

    
387
    response = render_server(request, server, status=202)
388

    
389
    return response
390

    
391

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

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

    
407

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

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

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

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

    
433
    return HttpResponse(status=204)
434

    
435

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

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

    
455

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

    
459

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

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

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

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

    
484

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

    
496

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

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

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

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

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

    
519
    vm.save()
520

    
521

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

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

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

    
540
    return HttpResponse(data, status=200)
541

    
542

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

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

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

    
564
    return HttpResponse(data, status=200)
565

    
566

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

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

    
581

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

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

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

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

    
611

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

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

    
628

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

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

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

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

    
663

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

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

    
684

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

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

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

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

    
713
    return HttpResponse(data, status=200)