Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (25.3 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+)/metadata(?:.json|.xml)?$', 'metadata_demux'),
69
    (r'^/(\d+)/metadata/(.+?)(?:.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['flavor'] = vm.flavor.id
137
        d['image'] = 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'] = metadata
143

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

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

    
153
    return d
154

    
155

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

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

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

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

    
179
        entries.append(entry)
180

    
181
    return entries
182

    
183

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

    
193

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

    
200

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

    
211

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

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

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

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

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

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

    
243
    return HttpResponse(data, status=200)
244

    
245

    
246
@api.api_method(http_method='POST', user_required=True, logger=log)
247
def create_server(request):
248
    # Normal Response Code: 202
249
    # Error Response Codes: computeFault (400, 500),
250
    #                       serviceUnavailable (503),
251
    #                       unauthorized (401),
252
    #                       badMediaType(415),
253
    #                       itemNotFound (404),
254
    #                       badRequest (400),
255
    #                       serverCapacityUnavailable (503),
256
    #                       overLimit (413)
257
    req = utils.get_request_dict(request)
258
    log.info('create_server %s', req)
259
    user_id = request.user_uniq
260

    
261
    try:
262
        server = req['server']
263
        name = server['name']
264
        metadata = server.get('metadata', {})
265
        assert isinstance(metadata, dict)
266
        image_id = server['imageRef']
267
        flavor_id = server['flavorRef']
268
        personality = server.get('personality', [])
269
        assert isinstance(personality, list)
270
    except (KeyError, AssertionError):
271
        raise faults.BadRequest("Malformed request")
272

    
273
    # Verify that personalities are well-formed
274
    util.verify_personality(personality)
275
    # Get image information
276
    image = util.get_image_dict(image_id, user_id)
277
    # Get flavor (ensure it is active)
278
    flavor = util.get_flavor(flavor_id, include_deleted=False)
279
    # Generate password
280
    password = util.random_password()
281

    
282
    vm = do_create_server(user_id, name, password, flavor, image,
283
                          metadata=metadata, personality=personality)
284

    
285
    server = vm_to_dict(vm, detail=True)
286
    server['status'] = 'BUILD'
287
    server['adminPass'] = password
288

    
289
    response = render_server(request, server, status=202)
290

    
291
    return response
292

    
293

    
294
@transaction.commit_manually
295
def do_create_server(userid, name, password, flavor, image, metadata={},
296
                  personality=[], network=None, backend=None):
297
    if backend is None:
298
        # Allocate backend to host the server. Commit after allocation to
299
        # release the locks hold by the backend allocator.
300
        try:
301
            backend_allocator = BackendAllocator()
302
            backend = backend_allocator.allocate(userid, flavor)
303
            if backend is None:
304
                log.error("No available backend for VM with flavor %s", flavor)
305
                raise faults.ServiceUnavailable("No available backends")
306
        except:
307
            transaction.rollback()
308
            raise
309
        else:
310
            transaction.commit()
311

    
312
    # Fix flavor for archipelago
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 = image['checksum']
318
        image['backend_id'] = 'null'
319
    else:
320
        flavor.disk_provider = None
321
        flavor.disk_origin = None
322

    
323
    try:
324
        if network is None:
325
            # Allocate IP from public network
326
            (network, address) = util.get_public_ip(backend)
327
            nic = {'ip': address, 'network': network.backend_id}
328
        else:
329
            address = util.get_network_free_address(network)
330

    
331
        # We must save the VM instance now, so that it gets a valid
332
        # vm.backend_vm_id.
333
        vm = VirtualMachine.objects.create(
334
            name=name,
335
            backend=backend,
336
            userid=userid,
337
            imageid=image["id"],
338
            flavor=flavor,
339
            action="CREATE")
340

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

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

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

    
358
        # Also we must create the VM metadata in the same transaction.
359
        for key, val in metadata.items():
360
            VirtualMachineMetadata.objects.create(
361
                meta_key=key,
362
                meta_value=val,
363
                vm=vm)
364
        # Issue commission to Quotaholder and accept it since at the end of
365
        # this transaction the VirtualMachine object will be created in the DB.
366
        # Note: the following call does a commit!
367
        quotas.issue_and_accept_commission(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
                 userid, vm, nic, backend, str(jobID))
382
    except GanetiApiError as e:
383
        log.exception("Can not communicate to backend %s: %s.",
384
                      backend, e)
385
        # Failed while enqueuing OP_INSTANCE_CREATE to backend. Restore
386
        # already reserved quotas by issuing a negative commission
387
        vm.operstate = "ERROR"
388
        vm.backendlogmsg = "Can not communicate to backend."
389
        vm.deleted = True
390
        vm.save()
391
        quotas.issue_and_accept_commission(vm, delete=True)
392
        raise
393
    except:
394
        transaction.rollback()
395
        raise
396

    
397
    return vm
398

    
399

    
400
@api.api_method(http_method='GET', user_required=True, logger=log)
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
@api.api_method(http_method='PUT', user_required=True, logger=log)
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 = utils.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
@api.api_method(http_method='DELETE', user_required=True, logger=log)
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
@api.api_method(http_method='POST', user_required=True, logger=log)
469
def server_action(request, server_id):
470
    req = utils.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 faults.BadRequest("VirtualMachine has been deleted.")
516

    
517
    # No actions to machines being built. They may be destroyed, however.
518
    if vm.operstate == 'BUILD' and action != 'DESTROY':
519
        raise faults.BuildInProgress("Server is being build.")
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
@api.api_method(http_method='GET', user_required=True, logger=log)
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': addresses})
547

    
548
    return HttpResponse(data, status=200)
549

    
550

    
551
@api.api_method(http_method='GET', user_required=True, logger=log)
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
@api.api_method(http_method='GET', user_required=True, logger=log)
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=False,
588
                                status=200)
589

    
590

    
591
@api.api_method(http_method='POST', user_required=True, logger=log)
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 = utils.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
@api.api_method(http_method='GET', user_required=True, logger=log)
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
@api.api_method(http_method='PUT', user_required=True, logger=log)
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 = utils.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
@api.api_method(http_method='DELETE', user_required=True, logger=log)
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
@api.api_method(http_method='GET', user_required=True, logger=log)
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)