Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (24.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 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 snf_django.lib.api import faults
45
from synnefo.api import util
46
from synnefo.api.actions import server_actions
47
from synnefo.api.common import method_not_allowed
48
from synnefo.db.models import (VirtualMachine, VirtualMachineMetadata,
49
                               NetworkInterface)
50
from synnefo.logic.backend import create_instance, delete_instance
51
from synnefo.logic.utils import get_rsapi_state
52
from synnefo.logic.rapi import GanetiApiError
53
from synnefo.logic.backend_allocator import BackendAllocator
54
from synnefo import quotas
55

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

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

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

    
76

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

    
85

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

    
96

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

    
105

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

    
116

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

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

    
128

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

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

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

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

    
156
    return d
157

    
158

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

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

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

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

    
182
        entries.append(entry)
183

    
184
    return entries
185

    
186

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

    
196

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

    
203

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

    
214

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

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

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

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

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

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

    
246
    return HttpResponse(data, status=200)
247

    
248

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
396
    respsone = render_server(request, server, status=202)
397

    
398
    return respsone
399

    
400

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

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

    
416

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

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

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

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

    
442
    return HttpResponse(status=204)
443

    
444

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

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

    
464

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

    
468

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

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

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

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

    
493

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

    
505

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

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

    
514
    # No actions to deleted VMs
515
    if vm.deleted:
516
        raise faults.BadRequest("VirtualMachine has been deleted.")
517

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

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

    
528
    vm.save()
529

    
530

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

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

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

    
549
    return HttpResponse(data, status=200)
550

    
551

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

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

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

    
573
    return HttpResponse(data, status=200)
574

    
575

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

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

    
590

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

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

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

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

    
620

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

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

    
637

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

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

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

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

    
672

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

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

    
693

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

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

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

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

    
722
    return HttpResponse(data, status=200)